I’m digging around passkeys and can’t find a way to properly extract the Public Key from the attestationData sent from the client during the attestation (rigistration) step. Most tutorials I found refer to using the web api with javascript and calling the getPublicKey() method, but I’m using flutter and the library I’m using for passkeys does not have such method, so I must extract the public key manually on my server, here is what I have tried:

const attestation = {
    "attestationData": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVikSZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2NFAAAAAK3OAAI1vMYKZIsLJfHwVQMAILxBOipox2vwkvO0SAE7mme20vs2iVsGk8VPFu/6lQPxpQECAyYgASFYIIGsoPOPLUt8AB40ssEf95YNmqgO16rKvXydLpU+A3TSIlggLr7aHpKAoRMWN1lGVRiBsMS1kdB10QEf1pxryoDQZ8A=",
    "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoib202bjVIdFVjZ1Q4T0NiTjJUZXQ4OWJtU3BFaUhpNUY3aWlZOVhQZmFUSSIsIm9yaWdpbiI6Imh0dHA6Ly9sb2NhbGhvc3Q6MzAwMCIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3RoZXJfa2V5c19jYW5fYmVfYWRkZWRfaGVyZSI6ImRvIG5vdCBjb21wYXJlIGNsaWVudERhdGFKU09OIGFnYWluc3QgYSB0ZW1wbGF0ZS4gU2VlIGh0dHBzOi8vZ29vLmdsL3lhYlBleCJ9",
    "publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEgayg848tS3wAHjSywR/3lg2aqA7Xqsq9fJ0ulT4DdNIuvtoekoChExY3WUZVGIGwxLWR0HXRAR/WnGvKgNBnwA=="
}

const assertion = {
    "signature": "MEYCIQCaO2mh+E8SEWUOGW1XLMPq3z/LofM67/vUr6ut/Z9apgIhALWuIhawe+nzWjd//Zd670IxrP9gksMW0o7Gh/FYHkcG",
    "authenticatorData": "SZYN5YgOjGh0NBcPZHZgW4/krrmihjLHmVzzuoMdl2MFAAAAAA=="
}

To make things easier I have used a simple web project with passkeys, all the values were encoded using base64 as follow:

function decodeBase64(data) {
    return Uint8Array.from(atob(data).split(""), (x) => x.charCodeAt(0));
}

function encodeBase64(data) {
    return btoa(String.fromCharCode(...new Uint8Array(data)));
}

Now for the NodeJS part where I need to recreate the public key:

  • Decode attestation using cbor:
const ctapMakeCredResp        = cbor.decodeFirstSync(decodeBase64(attestation.attestationData));
const authData = ctapMakeCredResp.authData;
const rpidHash = authData.subarray(0, 32);
const flags = authData.subarray(32, 33);
const counter = authData.subarray(33, 37).readUint32BE(0);

const aaguid = authData.subarray(37, 53);
const credIdLength = authData.subarray(53, 55).readUInt16BE(0);
const credID = authData.subarray(55, 55 + credIdLength);
const COSEPublicKey = authData.subarray(55  + credIdLength);
  • Decode the COSEPublicKey to obtain the CBOR Map:
const publicKeyMap = cbor.decodeFirstSync(COSEPublicKey);
const publicKeyData = {
    kty: publicKeyMap.get(1),
    alg: publicKeyMap.get(3),
    crv: publicKeyMap.get(-1),
    x: publicKeyMap.get(-2),
    y: publicKeyMap.get(-3),
};

Here for debugging purposes, I also have the base64 encode of the public key generated on the frontend and when I extract the X and Y values from original public key, Y results to aways be the same the COSEPublicKey, but X is always different.

From now on, I’m not sure if I’m handling the signature verification correctly, I have tried creating the public key in the following methods:

const publicKey = crypto.createPublicKey({
    key: Buffer.from([0x04, ...publicKeyData.x, ...publicKeyData.y]),
    format: 'der',
    type: 'spki',
});

The above method results in an Error saying: Error: error:0680009B:asn1 encoding routines::too long.

And also tried like this, which results in the same error:

const publicKey = await crypto.subtle.importKey(
    'spki',
    Buffer.from([0x04, publicKeyData.x, publicKeyData.y]),
    { name: 'ECDSA, namedCurve: 'P-256 },
    true,
    ['verify']
);

And for verification:

const signature = decodeBase64(authSignature);
const authDataDecoded = decodeBase64(authenticatorData);
const clientData = decodeBase64(clientDataJSON);

const rawSig = fromAsn1DERtoRSSignature(signature, 256);

const digest = concatBuffer(
    authDataDecoded,
    await crypto.subtle.digest('SHA-256', clientData)
);

const result = await crypto.subtle.verify(
    { name: 'ECDSA', hash: { name: 'SHA-256' } },
    publicKey,
    rawSig,
    digest
);

I have also tried recreating the public key using the original b64 encoded public key like this:

const publicKey = await crypto.subtle.importKey(
    'spki',
    base64Decode(attestation.publicKey),
    { name: 'ECDSA, namedCurve: 'P-256 },
    true,
    ['verify']
);

But the result of verify is false, so there must be something wrong with my code.

The source code for the fromAsn1DERtoRSSignature can be found here.

For most tutorials I’ve found are always in javascript and run within the browser as a given, but now also to learn better about the topic I want to learn how to handle such use case.

I’ve tried many solution that I’ve seen online and also using code assistants, none of them were of my help, they either gives an error or results into a false assertion.