Skip to content

Instantly share code, notes, and snippets.

@philholden
Last active March 21, 2024 05:49
Show Gist options
  • Star 14 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save philholden/50120652bfe0498958fd5926694ba354 to your computer and use it in GitHub Desktop.
Save philholden/50120652bfe0498958fd5926694ba354 to your computer and use it in GitHub Desktop.
// paste in console of any https site to run (e.g. this page)
// sample arguments for registration
// https://fidoalliance.org/specs/fido-u2f-v1.1-id-20160915/fido-u2f-raw-message-formats-v1.1-id-20160915.html#authentication-response-message-success
var createCredentialDefaultArgs = {
publicKey: {
// Relying Party (a.k.a. - Service):
rp: {
name: "Acme"
},
// User:
user: {
id: new Uint8Array(16),
name: "john.p.smith@example.com",
displayName: "John P. Smith"
},
pubKeyCredParams: [{
type: "public-key",
alg: -7
}],
attestation: "direct",
timeout: 60000,
challenge: new Uint8Array([ // must be a cryptographically random number sent from a server
0x8C, 0x0A, 0x26, 0xFF, 0x22, 0x91, 0xC1, 0xE9, 0xB9, 0x4E, 0x2E, 0x17, 0x1A, 0x98, 0x6A, 0x73,
0x71, 0x9D, 0x43, 0x48, 0xD5, 0xA7, 0x6A, 0x15, 0x7E, 0x38, 0x94, 0x52, 0x77, 0x97, 0x0F, 0xEF
]).buffer
}
};
// sample arguments for login
var getCredentialDefaultArgs = {
publicKey: {
timeout: 60000,
// allowCredentials: [newCredential] // see below
challenge: new Uint8Array([ // must be a cryptographically random number sent from a server
0x79, 0x50, 0x68, 0x71, 0xDA, 0xEE, 0xEE, 0xB9, 0x94, 0xC3, 0xC2, 0x15, 0x67, 0x65, 0x26, 0x22,
0xE3, 0xF3, 0xAB, 0x3B, 0x78, 0x2E, 0xD5, 0x6F, 0x81, 0x26, 0xE2, 0xA6, 0x01, 0x7D, 0x74, 0x50
]).buffer
},
};
// register / create a new credential
var cred = await navigator.credentials.create(createCredentialDefaultArgs)
console.log("NEW CREDENTIAL", cred);
// normally the credential IDs available for an account would come from a server
// but we can just copy them from above...
var idList = [{
id: cred.rawId,
transports: ["usb", "nfc", "ble"],
type: "public-key"
}];
getCredentialDefaultArgs.publicKey.allowCredentials = idList;
var assertation = await navigator.credentials.get(getCredentialDefaultArgs);
console.log("ASSERTION", assertation);
// verify signature on server
var signature = await assertation.response.signature;
console.log("SIGNATURE", signature)
var clientDataJSON = await assertation.response.clientDataJSON;
console.log("clientDataJSON", clientDataJSON)
var authenticatorData = new Uint8Array(await assertation.response.authenticatorData);
console.log("authenticatorData", authenticatorData)
var clientDataHash = new Uint8Array(await crypto.subtle.digest("SHA-256", clientDataJSON));
console.log("clientDataHash", clientDataHash)
// concat authenticatorData and clientDataHash
var signedData = new Uint8Array(authenticatorData.length + clientDataHash.length);
signedData.set(authenticatorData);
signedData.set(clientDataHash, authenticatorData.length);
console.log("signedData", signedData)
// import key
var key = await crypto.subtle.importKey(
// The getPublicKey() operation thus returns the credential public key as a SubjectPublicKeyInfo. See:
//
// https://w3c.github.io/webauthn/#sctn-public-key-easy
//
// crypto.subtle can import the spki format:
//
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey
"spki", // "spki" Simple Public Key Infrastructure rfc2692
cred.response.getPublicKey(),
{
// these are the algorithm options
// await cred.response.getPublicKeyAlgorithm() // returns -7
// -7 is ES256 with P-256 // search -7 in https://w3c.github.io/webauthn
// the W3C webcrypto docs:
//
// https://www.w3.org/TR/WebCryptoAPI/#informative-references (scroll down a bit)
//
// ES256 corrisponds with the following AlgorithmIdentifier:
name: "ECDSA",
namedCurve: "P-256",
hash: { name: "SHA-256" }
},
false, //whether the key is extractable (i.e. can be used in exportKey)
["verify"] //"verify" for public key import, "sign" for private key imports
);
// check signature with public key and signed data
var verified = await crypto.subtle.verify(
{ name: "ECDSA", namedCurve: "P-256", hash: { name: "SHA-256" } },
key,
signature,
signedData.buffer
);
// verified is false I want it to be true
console.log('verified', verified)
@emlun
Copy link

emlun commented May 12, 2021

I think what you might be saying is I should really not trust the public key until I check it has been signed (I guess by the device). Is that correct?

Well, yes and no - most likely no. That is correct if you care about attestation, meaning you have a well-defined policy of what attestation root certificates you trust and under what circumstances. If you don't have that (most RPs won't), then you don't really need to verify the attestation signature at all (though you may still want to store it in case you want to verify it in the future). The attestation signature says nothing about what user created it, only about what model of authenticator they were using.

So both with and without attestation, you're initially just trusting that the credential public key you got is the real one from the legitimate user. Future assertions by that key will equally well prove that the user has possession of that same private key. All attestation does is (maybe) give a stronger assurance that that private key cannot be cloned or exported, for example.

@philholden
Copy link
Author

philholden commented May 12, 2021

Thanks for trying in Java. The thing I am a little unsure of in the importKey is that it seems I need to specify:

hash: { name: "SHA-256" }

I can't see any mention of the overall signature involving a hash in the webauthn spec (just the clientDataJSON is SHA-256 hashed before being signed). However if I do not specify the hash on the import I get an error saying I need to:

EcdsaParams: hash: Missing or not an AlgorithmIdentifier

The web crypto spec says what algorithms clients should support:

https://www.w3.org/TR/WebCryptoAPI/#algorithm-recommendations-implementers

  • ECDSA using P-256 curve and SHA-256

This is the only mandatory ECDSA algorithm clients must implement. I am not sure how the hash relates to the signature but guessing the signature must involve a hash or it would be as long a the signed data.

@philholden
Copy link
Author

philholden commented May 13, 2021

@emlun Thanks for validating with Java. I hope this allows me to restate in a simpler form. Please could you check the following signature validates for the stated privateKey and signed data.

var signedData = new Uint8Array([176, 159, 13, 66, 152, 86, 110, 80, 108, 152, 136, 56, 165, 240, 102, 45, 91, 45, 64, 51, 77, 153, 6, 243, 202, 229, 161, 198, 56, 172, 27, 146, 1, 0, 0, 0, 5, 248, 27, 20, 88, 145, 38, 229, 234, 204, 54, 157, 212, 162, 139, 37, 204, 78, 141, 191, 70, 123, 2, 12, 193, 2, 227, 116, 201, 46, 65, 4, 96]);

var signature = new Uint8Array([48, 69, 2, 32, 9, 224, 199, 46, 110, 2, 12, 101, 194, 145, 255, 57, 63, 110, 169, 217, 151, 152, 156, 101, 70, 82, 109, 188, 191, 162, 133, 20, 242, 219, 65, 72, 2, 33, 0, 139, 99, 85, 247, 254, 15, 65, 173, 62, 5, 138, 171, 16, 28, 185, 167, 244, 32, 56, 71, 86, 17, 146, 144, 73, 86, 52, 150, 155, 228, 1, 7]);

var credientialPublicKey = new Uint8Array([48, 89, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42, 134, 72, 206, 61, 3, 1, 7, 3, 66, 0, 4, 221, 93, 192, 254, 112, 50, 120, 12, 249, 135, 157, 72, 63, 50, 199, 187, 254, 203, 216, 180, 104, 197, 18, 221, 159, 254, 150, 102, 120, 252, 4, 181, 40, 39, 226, 117, 170, 21, 109, 153, 232, 202, 94, 172, 143, 251, 86, 61, 182, 104, 141, 145, 40, 113, 249, 224, 110, 218, 22, 39, 50, 255, 190, 96]);

If this is so then the question is simply how can this be validated in JS using WebCrypto / crypto.subtle (note this will be done server side in a Cloudflare Worker but guessing this could also be used for peer to peer decentralised browser apps).

@philholden
Copy link
Author

Copying in this reply I got from @MasterKale SimpleWebAuthn author:


Hello Phil,

TIL that crypto.subtle is a thing, I had no idea! I played around with your gist this morning and found a clue as to what’s going wrong:

I manipulated your script to inject some of SimpleWebAuthn’s logic to convert navigator.credentials responses to a JSON structure. This made it easier to copy/paste attestations and assertions into my editor to create a similar script to run Node-based verification for comparison. When I performed this Uint8Array -> Base64URL -> Buffer I ended up with the following Buffer (which I then case to the following Uint8Array):

Uint8Array(37)[176, 159, 13, 66, 152, 86, 110, 80, 108, 152, 136, 56, 165, 240, 102,
  45, 91, 45, 64, 51, 77, 153, 6, 243, 202, 229, 161, 198, 56, 172, 27, 146, 1, 0, 0, 4, 30]

However, when I compared it to console.log("authenticatorData", authenticatorData) on L71 of your gist, I got the following Uint8Array:

Uint8Array(37)[176, 159, 13, 66, 152, 86, 110, 80, 108, 152, 136, 56, 165, 240, 102,
  45, 91, 45, 64, 51, 77, 153, 6, 243, 202, 229, 161, 198, 56, 172, 27, 146, 1, 0, 0, 3, 99]

Check out those last two bytes - they’re different, for some reason. Something about the act of converting Uint8Arrays to Base64URLs in the Browser before decoding back to Buffers in Node resulted in slightly different value for the assertion’s authenticatorData that allowed the response to be verified! With everything else being equivalent, those last two bytes generate a different signature base that can’t be verified.

Now, I’m out of time and am still not sure what this all means. All I know at this point is that when I take those same attestation and assertion credentials, massage them into JSON, then convert back to buffer and feed into Node’s crypto functionality, authenticatorData changes to a value that forms a proper signature and can be verified.

I tried validating those three values you provided in your latest gist comment with Node code, but I’m not sure how to convert getPublicKey() spki output (which I assume credientialPublicKey since my code couldn’t handle it) to a COSE structure SimpleWebAuthn expects (it use the credential public key from within authData). Node needs the public key in PEM format so if you know how to convert getPublicKey() output to PEM then I can test on Node and compare apples to apples without all of the converting between data encodings.

Wish I could help more,
-Matthew

@MasterKale
Copy link

Hey, that was me. It's such a pain to copy-paste output navigator.credentials.create() and navigator.credentials.get() that I always reach for base64url encoding and decoding tools (that I accumulated while building SimpleWebAuthn) to massage the credentials into something I can c/p into an editor to work with. I still have no idea why Browser Uint8Array -> Base64URL -> Node Buffer would cause those two bytes to change, nor why that would lead to a signature that could be verified in Node but not in the browser.

As for the bit about converting cred.response.getPublicKey() output to PEM, I have code that goes from COSE to PEM that works fine with the public key in authData, but I've never worked with getPublicKey() before so I don't have anything handy to process that. If someone can point me towards the structure of the getPublicKey buffer I might have a shot of getting it into PEM without needing to get base64url encoding/decoding in the mix in case that's subtly tweaking the data structure.

@philholden can you try grabbing the public key from authData and use that in crypto.subtle.importKey() instead? I have no idea if importKey() can support COSE, but the use of cred.response.getPublicKey() in there is another variable I wanted to try and remove from all of this.

@philholden
Copy link
Author

philholden commented May 14, 2021

Hi @MasterKale,

Thanks so much for doing that it really sheds a lot of light on where the problem is coming from (I wonder if it affects other byte arrays: publicKey and clientDataJSON). In base64URL the last two characters can be padding characters = or == I wonder if they are absent or present incorrectly.

These are the docs for crypto.subtle.exportKey it shows how to create a PEM:

https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/exportKey

However probably the most helpful thing to do would be if you can check if the following will verify:

cred.response.attestationObject:

new Uint8Array([163,99,102,109,116,102,112,97,99,107,101,100,103,97,116,116,83,116,109,116,163,99,97,108,103,38,99,115,105,103,88,70,48,68,2,32,55,223,213,83,119,179,111,120,177,93,0,163,136,12,210,161,31,184,193,39,161,252,231,195,4,243,67,92,184,9,50,170,2,32,7,252,90,246,249,57,79,162,23,66,13,115,120,168,185,62,152,13,65,37,89,80,228,178,232,169,53,109,111,20,162,123,99,120,53,99,129,89,2,194,48,130,2,190,48,130,1,166,160,3,2,1,2,2,4,72,136,12,202,48,13,6,9,42,134,72,134,247,13,1,1,11,5,0,48,46,49,44,48,42,6,3,85,4,3,19,35,89,117,98,105,99,111,32,85,50,70,32,82,111,111,116,32,67,65,32,83,101,114,105,97,108,32,52,53,55,50,48,48,54,51,49,48,32,23,13,49,52,48,56,48,49,48,48,48,48,48,48,90,24,15,50,48,53,48,48,57,48,52,48,48,48,48,48,48,90,48,111,49,11,48,9,6,3,85,4,6,19,2,83,69,49,18,48,16,6,3,85,4,10,12,9,89,117,98,105,99,111,32,65,66,49,34,48,32,6,3,85,4,11,12,25,65,117,116,104,101,110,116,105,99,97,116,111,114,32,65,116,116,101,115,116,97,116,105,111,110,49,40,48,38,6,3,85,4,3,12,31,89,117,98,105,99,111,32,85,50,70,32,69,69,32,83,101,114,105,97,108,32,49,50,49,54,56,55,53,55,50,50,48,89,48,19,6,7,42,134,72,206,61,2,1,6,8,42,134,72,206,61,3,1,7,3,66,0,4,244,6,14,106,111,153,129,124,80,4,141,33,135,9,129,14,182,52,60,104,29,193,103,103,196,217,93,136,136,189,154,31,169,75,97,246,214,168,174,47,204,192,16,146,221,49,53,114,34,58,31,4,6,66,215,207,151,191,148,208,72,208,238,2,163,108,48,106,48,34,6,9,43,6,1,4,1,130,196,10,2,4,21,49,46,51,46,54,46,49,46,52,46,49,46,52,49,52,56,50,46,49,46,55,48,19,6,11,43,6,1,4,1,130,229,28,2,1,1,4,4,3,2,5,32,48,33,6,11,43,6,1,4,1,130,229,28,1,1,4,4,18,4,16,238,136,40,121,114,28,73,19,151,117,61,252,206,151,7,42,48,12,6,3,85,29,19,1,1,255,4,2,48,0,48,13,6,9,42,134,72,134,247,13,1,1,11,5,0,3,130,1,1,0,104,212,179,108,133,70,66,165,189,21,247,37,231,119,223,157,204,97,0,59,9,177,26,252,83,163,4,201,183,112,56,227,164,233,49,70,54,84,204,140,230,241,8,205,211,39,200,156,195,64,43,171,244,92,246,107,43,13,246,225,123,141,165,33,62,39,238,46,96,215,138,138,158,164,135,58,139,85,56,118,101,87,181,194,131,59,231,35,47,184,163,159,110,98,227,218,94,245,182,124,96,244,174,125,45,101,182,114,144,112,83,122,235,106,59,151,116,51,216,248,206,166,228,115,247,137,48,43,126,138,168,183,21,138,122,201,192,0,108,157,3,24,212,91,248,192,147,201,29,99,68,250,128,202,43,128,148,197,151,149,21,124,11,27,170,201,188,8,140,0,36,135,64,151,156,222,229,106,217,39,214,140,147,46,7,244,146,212,95,30,13,198,88,103,147,147,25,237,240,69,223,245,37,217,59,56,248,106,73,72,185,30,79,124,182,59,242,40,212,200,46,117,164,255,132,102,28,40,7,147,170,47,117,172,199,24,68,85,213,111,155,31,72,48,2,29,228,13,132,192,227,15,175,205,45,32,104,97,117,116,104,68,97,116,97,88,196,176,159,13,66,152,86,110,80,108,152,136,56,165,240,102,45,91,45,64,51,77,153,6,243,202,229,161,198,56,172,27,146,65,0,0,0,4,238,136,40,121,114,28,73,19,151,117,61,252,206,151,7,42,0,64,186,250,16,1,99,9,147,158,84,58,226,15,203,252,36,121,254,0,143,7,235,157,145,248,195,224,41,56,196,164,54,22,144,237,145,8,75,33,15,13,211,143,51,34,30,145,11,69,112,242,10,194,177,250,108,148,113,94,27,15,255,76,204,163,165,1,2,3,38,32,1,33,88,32,7,139,126,200,212,91,16,65,47,39,134,119,96,57,107,129,82,117,163,213,3,20,126,137,190,75,64,102,50,1,152,0,34,88,32,139,140,87,9,5,209,28,203,221,186,243,139,117,243,254,129,96,237,149,26,172,7,119,172,52,196,72,46,31,245,45,195]);

cred.getPublicKey():

new Uint8Array([48,89,48,19,6,7,42,134,72,206,61,2,1,6,8,42,134,72,206,61,3,1,7,3,66,0,4,7,139,126,200,212,91,16,65,47,39,134,119,96,57,107,129,82,117,163,213,3,20,126,137,190,75,64,102,50,1,152,0,139,140,87,9,5,209,28,203,221,186,243,139,117,243,254,129,96,237,149,26,172,7,119,172,52,196,72,46,31,245,45,195])

as PEM but still spki:

"-----BEGIN PRIVATE KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEB4t+yNRbEEEvJ4Z3YDlrgVJ1o9UDFH6JvktAZjIBmACLjFcJBdEcy92684t18/6BYO2VGqwHd6w0xEguH/Utww==\n-----END PRIVATE KEY-----"
function exportCryptoKeyAsPem(buff) {
  const str = String.fromCharCode.apply(null, new Uint8Array(buf));
  const exportedAsString = ab2str(str);
  const exportedAsBase64 = window.btoa(exportedAsString);
  return `-----BEGIN PRIVATE KEY-----\n${exportedAsBase64}\n-----END PRIVATE KEY-----`;
}

assertion.response.authenticatorData:

new Uint8Array([176,159,13,66,152,86,110,80,108,152,136,56,165,240,102,45,91,45,64,51,77,153,6,243,202,229,161,198,56,172,27,146,1,0,0,0,7])

assertion.response.clientDataJson:

new Uint8Array([123,34,116,121,112,101,34,58,34,119,101,98,97,117,116,104,110,46,103,101,116,34,44,34,99,104,97,108,108,101,110,103,101,34,58,34,101,86,66,111,99,100,114,117,55,114,109,85,119,56,73,86,90,50,85,109,73,117,80,122,113,122,116,52,76,116,86,118,103,83,98,105,112,103,70,57,100,70,65,34,44,34,111,114,105,103,105,110,34,58,34,104,116,116,112,115,58,47,47,103,105,115,116,46,103,105,116,104,117,98,46,99,111,109,34,44,34,99,114,111,115,115,79,114,105,103,105,110,34,58,102,97,108,115,101,125])

clientDataHash

new Uint8Array([248, 27, 20, 88, 145, 38, 229, 234, 204, 54, 157, 212, 162, 139, 37, 204, 78, 141, 191, 70, 123, 2, 12, 193, 2, 227, 116, 201, 46, 65, 4, 96])

signature

new Uint8Array(["48,68,2,32,37,29,94,9,216,255,70,79,193,246,236,201,24,169,205,238,63,95,146,132,89,226,237,166,119,237,29,20,222,79,88,129,2,32,14,164,49,157,222,147,137,227,230,74,45,69,186,86,18,214,111,81,2,113,150,7,159,215,51,174,13,123,5,51,17,111"])

@philholden
Copy link
Author

I tried using bufferToBase64URLString followed by base64URLStringToBuffer on authenticatorData but it came out the same with no changed bytes.

@philholden
Copy link
Author

philholden commented May 15, 2021

authenticatorData for several runs of navigator.credentials.create on same domain:

25,19,71,191,229,93,12,169,165,116,219,119,188,134,72,39,92,226,88,70,20,80,231,147,82,142,12,198,210,220,248,245,1,0,0,0,6
25,19,71,191,229,93,12,169,165,116,219,119,188,134,72,39,92,226,88,70,20,80,231,147,82,142,12,198,210,220,248,245,1,0,0,0,7
25,19,71,191,229,93,12,169,165,116,219,119,188,134,72,39,92,226,88,70,20,80,231,147,82,142,12,198,210,220,248,245,1,0,0,0,8
25,19,71,191,229,93,12,169,165,116,219,119,188,134,72,39,92,226,88,70,20,80,231,147,82,142,12,198,210,220,248,245,1,0,0,0,4
25,19,71,191,229,93,12,169,165,116,219,119,188,134,72,39,92,226,88,70,20,80,231,147,82,142,12,198,210,220,248,245,1,0,0,0,3
25,19,71,191,229,93,12,169,165,116,219,119,188,134,72,39,92,226,88,70,20,80,231,147,82,142,12,198,210,220,248,245,1,0,0,0,6
25,19,71,191,229,93,12,169,165,116,219,119,188,134,72,39,92,226,88,70,20,80,231,147,82,142,12,198,210,220,248,245,1,0,0,0,5
25,19,71,191,229,93,12,169,165,116,219,119,188,134,72,39,92,226,88,70,20,80,231,147,82,142,12,198,210,220,248,245,1,0,0,0,7

on various domains:

113,203,150,83,209,173,217,141,148,155,143,24,94,112,57,155,118,57,201,70,24,39,44,205,20,152,111,15,1,77,157,217,1,0,0,0,8
173,170,253,141,104,51,18,56,30,2,22,192,97,109,210,163,16,82,88,42,203,29,88,75,157,115,123,224,12,81,58,203,1,0,0,0,7
26,46,54,204,105,45,17,157,240,161,132,158,151,170,251,0,86,74,243,246,56,187,95,190,76,99,190,28,182,184,35,25,1,0,0,0,4

Looks like last four digits are part of a clock or counter or random number. Are you sure your different numbers are for the same registration event? If so could you try again and paste the following into a comment:

  • cred.response.getPublicKey()
  • assertation.response.signature
  • assertation.response.authenticatorData
  • assertation.response.authenticatorData (as seen on server with last bytes changed)
  • assertation.response.clientDataJSON

I can then import the key and check the signature with your server assertation.response.authenticatorData

@philholden
Copy link
Author

All bytes from the spki appear in the cred.response.attestationObject in three chunks:

163,99,102,109,116,102,112,97,99,107,101,100,103,97,116,116,83,116,109,116,163,99,97,108,103,38,99,115,105,103,88,70,48,68,2,32,108,33,182,70,74,202,157,40,206,209,54,56,96,187,91,48,126,90,123,78,239,111,166,235,26,114,48,82,187,218,32,229,2,32,102,195,34,73,155,131,80,251,1,154,102,139,82,128,162,93,120,175,16,148,217,144,207,197,73,211,226,11,64,23,82,169,99,120,53,99,129,89,2,194,48,130,2,190,48,130,1,166,160,3,2,1,2,2,4,72,136,12,202,48,13,6,9,42,134,72,134,247,13,1,1,11,5,0,48,46,49,44,48,42,6,3,85,4,3,19,35,89,117,98,105,99,111,32,85,50,70,32,82,111,111,116,32,67,65,32,83,101,114,105,97,108,32,52,53,55,50,48,48,54,51,49,48,32,23,13,49,52,48,56,48,49,48,48,48,48,48,48,90,24,15,50,48,53,48,48,57,48,52,48,48,48,48,48,48,90,48,111,49,11,48,9,6,3,85,4,6,19,2,83,69,49,18,48,16,6,3,85,4,10,12,9,89,117,98,105,99,111,32,65,66,49,34,48,32,6,3,85,4,11,12,25,65,117,116,104,101,110,116,105,99,97,116,111,114,32,65,116,116,101,115,116,97,116,105,111,110,49,40,48,38,6,3,85,4,3,12,31,89,117,98,105,99,111,32,85,50,70,32,69,69,32,83,101,114,105,97,108,32,49,50,49,54,56,55,53,55,50,50,48,89,48,19,6,7,42,134,72,206,61,2,1,6,8,42,134,72,206,61,3,1,7,3,66,0,4,244,6,14,106,111,153,129,124,80,4,141,33,135,9,129,14,182,52,60,104,29,193,103,103,196,217,93,136,136,189,154,31,169,75,97,246,214,168,174,47,204,192,16,146,221,49,53,114,34,58,31,4,6,66,215,207,151,191,148,208,72,208,238,2,163,108,48,106,48,34,6,9,43,6,1,4,1,130,196,10,2,4,21,49,46,51,46,54,46,49,46,52,46,49,46,52,49,52,56,50,46,49,46,55,48,19,6,11,43,6,1,4,1,130,229,28,2,1,1,4,4,3,2,5,32,48,33,6,11,43,6,1,4,1,130,229,28,1,1,4,4,18,4,16,238,136,40,121,114,28,73,19,151,117,61,252,206,151,7,42,48,12,6,3,85,29,19,1,1,255,4,2,48,0,48,13,6,9,42,134,72,134,247,13,1,1,11,5,0,3,130,1,1,0,104,212,179,108,133,70,66,165,189,21,247,37,231,119,223,157,204,97,0,59,9,177,26,252,83,163,4,201,183,112,56,227,164,233,49,70,54,84,204,140,230,241,8,205,211,39,200,156,195,64,43,171,244,92,246,107,43,13,246,225,123,141,165,33,62,39,238,46,96,215,138,138,158,164,135,58,139,85,56,118,101,87,181,194,131,59,231,35,47,184,163,159,110,98,227,218,94,245,182,124,96,244,174,125,45,101,182,114,144,112,83,122,235,106,59,151,116,51,216,248,206,166,228,115,247,137,48,43,126,138,168,183,21,138,122,201,192,0,108,157,3,24,212,91,248,192,147,201,29,99,68,250,128,202,43,128,148,197,151,149,21,124,11,27,170,201,188,8,140,0,36,135,64,151,156,222,229,106,217,39,214,140,147,46,7,244,146,212,95,30,13,198,88,103,147,147,25,237,240,69,223,245,37,217,59,56,248,106,73,72,185,30,79,124,182,59,242,40,212,200,46,117,164,255,132,102,28,40,7,147,170,47,117,172,199,24,68,85,213,111,155,31,72,48,2,29,228,13,132,192,227,15,175,205,45,32,104,97,117,116,104,68,97,116,97,88,196,25,19,71,191,229,93,12,169,165,116,219,119,188,134,72,39,92,226,88,70,20,80,231,147,82,142,12,198,210,220,248,245,65,0,0,0,4,238,136,40,121,114,28,73,19,151,117,61,252,206,151,7,42,0,64,31,131,93,155,93,8,128,157,227,32,214,168,152,28,5,121,73,128,136,167,38,156,162,93,153,218,52,240,188,230,216,41,105,32,111,242,200,56,1,164,183,159,125,13,5,144,147,63,64,232,112,167,2,50,141,51,226,58,10,229,67,63,40,114,165,1,2,3,38,32,1,33,88,32,9,83,157,75,246,67,105,157,143,187,218,185,169,83,66,36,239,91,138,89,101,208,208,36,190,12,58,59,254,175,140,159,34,88,32,66,75,5,72,29,219,122,251,91,115,104,255,41,48,216,207,0,73,152,222,73,208,200,206,108,151,186,250,213,242,235,79

cred.response.getPublicKey():

48,89,48,19,6,7,42,134,72,206,61,2,1,6,8,42,134,72,206,61,3,1,7,3,66,0,4, 9,83,157,75,246,67,105,157,143,187,218,185,169,83,66,36,239,91,138,89,101,208,208,36,190,12,58,59,254,175,140,159, 166,75,5,72,29,219,122,251,91,115,104,255,41,48,216,207,0,73,152,222,73,208,200,206,108,151,186,250,213,242,235,79

@MasterKale
Copy link

MasterKale commented May 15, 2021

However probably the most helpful thing to do would be if you can check if the following will verify:

I found success (in Node) with those values you posted!

const authenticatorData = new Uint8Array([
  176, 159, 13, 66, 152, 86, 110, 80, 108, 152, 136, 56, 165,
  240, 102, 45, 91, 45, 64, 51, 77, 153, 6, 243, 202, 229,
  161, 198, 56, 172, 27, 146, 1, 0, 0, 0, 7
]);
const clientDataHash = new Uint8Array([
  248, 27, 20, 88, 145, 38, 229, 234, 204, 54, 157, 212, 162,
  139, 37, 204, 78, 141, 191, 70, 123, 2, 12, 193, 2, 227,
  116, 201, 46, 65, 4, 96
]);
const signature = new Uint8Array([
  48, 68, 2, 32, 37, 29, 94, 9, 216, 255, 70, 79, 193, 246,
  236, 201, 24, 169, 205, 238, 63, 95, 146, 132, 89, 226,
  237, 166, 119, 237, 29, 20, 222, 79, 88, 129, 2, 32, 14,
  164, 49, 157, 222, 147, 137, 227, 230, 74, 45, 69, 186, 86,
  18, 214, 111, 81, 2, 113, 150, 7, 159, 215, 51, 174, 13, 123,
  5, 51, 17, 111
]);
const pubKeyBytes = new Uint8Array([
  48, 89, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42,
  134, 72, 206, 61, 3, 1, 7, 3, 66, 0, 4, 7, 139, 126, 200,
  212, 91, 16, 65, 47, 39, 134, 119, 96, 57, 107, 129, 82, 117,
  163, 213, 3, 20, 126, 137, 190, 75, 64, 102, 50, 1, 152, 0,
  139, 140, 87, 9, 5, 209, 28, 203, 221, 186, 243, 139, 117,
  243, 254, 129, 96, 237, 149, 26, 172, 7, 119, 172, 52, 196,
  72, 46, 31, 245, 45, 195
]);

const signatureBase = new Uint8Array(Buffer.concat([authenticatorData, clientDataHash]));
const credPubKey = `-----BEGIN PUBLIC KEY-----\n${Buffer.from(pubKeyBytes).toString('base64')}\n-----END PUBLIC KEY-----`

console.log(
  crypto.createVerify('sha256').update(signatureBase).verify(
    credPubKey,
    signature,
  )
); // true

The only change I had to make was in your PEM-formatted public key - RP's get public keys so I had to change PRIVATE KEY to PUBLIC KEY before the call to .verify() would succeed.

So now I suppose the thing to do is to try and get these Uint8Arrays to verify in the browser with crypto.subtle.

@MasterKale
Copy link

I tried using bufferToBase64URLString followed by base64URLStringToBuffer on authenticatorData but it came out the same with no changed bytes.

I used the same authenticator for testing yesterday; your comment about the last two bytes maybe being a time component got me thinking that maybe I inadvertently compared data from two separate attestations while I was juggling all these values yesterday...that would explain why the two bytes "changed" when I encoded to base64url and back 😅

@MasterKale
Copy link

MasterKale commented May 15, 2021

Here's the browser version of my Node code:

var authenticatorData = new Uint8Array([
  176, 159, 13, 66, 152, 86, 110, 80, 108, 152, 136, 56, 165,
  240, 102, 45, 91, 45, 64, 51, 77, 153, 6, 243, 202, 229,
  161, 198, 56, 172, 27, 146, 1, 0, 0, 0, 7
]);
var clientDataHash = new Uint8Array([
  248, 27, 20, 88, 145, 38, 229, 234, 204, 54, 157, 212, 162,
  139, 37, 204, 78, 141, 191, 70, 123, 2, 12, 193, 2, 227,
  116, 201, 46, 65, 4, 96
]);
var signature = new Uint8Array([
  48, 68, 2, 32, 37, 29, 94, 9, 216, 255, 70, 79, 193, 246,
  236, 201, 24, 169, 205, 238, 63, 95, 146, 132, 89, 226,
  237, 166, 119, 237, 29, 20, 222, 79, 88, 129, 2, 32, 14,
  164, 49, 157, 222, 147, 137, 227, 230, 74, 45, 69, 186, 86,
  18, 214, 111, 81, 2, 113, 150, 7, 159, 215, 51, 174, 13, 123,
  5, 51, 17, 111
]);
// cred.response.getPublicKey()
var pubKeyBytes = new Uint8Array([
  48, 89, 48, 19, 6, 7, 42, 134, 72, 206, 61, 2, 1, 6, 8, 42,
  134, 72, 206, 61, 3, 1, 7, 3, 66, 0, 4, 7, 139, 126, 200,
  212, 91, 16, 65, 47, 39, 134, 119, 96, 57, 107, 129, 82, 117,
  163, 213, 3, 20, 126, 137, 190, 75, 64, 102, 50, 1, 152, 0,
  139, 140, 87, 9, 5, 209, 28, 203, 221, 186, 243, 139, 117,
  243, 254, 129, 96, 237, 149, 26, 172, 7, 119, 172, 52, 196,
  72, 46, 31, 245, 45, 195
]);

// concat authenticatorData and clientDataHash
var signedData = new Uint8Array(authenticatorData.length + clientDataHash.length);
signedData.set(authenticatorData);
signedData.set(clientDataHash, authenticatorData.length);
console.log('signedData:', signedData);

// import key
var key = await crypto.subtle.importKey(
  "spki",
  pubKeyBytes,
  { name: "ECDSA", namedCurve: "P-256", hash: { name: "SHA-256" } },
  false,
  ["verify"]
);

// check signature with public key and signed data 
var verified = await crypto.subtle.verify(
  { name: "ECDSA", namedCurve: "P-256", hash: { name: "SHA-256" } },
  key,
  signature.buffer,
  signedData.buffer,
);
// verified is false I want it to be true
console.log('verified', verified); // false

Still comes out false, though 😬

@philholden
Copy link
Author

Thanks again @MasterKale. So in conclusion it looks like the following are good:

  • getPublicKey works as expected
  • clientDataJSON
  • authenticatorData
  • signature

Looks like the fault must be with importKey or verify:

authenticatorData
{ name: "ECDSA", namedCurve: "P-256", hash: { name: "SHA-256" } },

What is interesting is if I generate a key pair with the above settings and then sign some data the signature is always 64 bytes where as the signature from the authenticator is 70 bytes:

var signedData = new Uint8Array([176, 159, 13, 66, 152, 86, 110, 80, 108, 152, 136, 56, 165, 240, 102, 45, 91, 45, 64, 51, 77, 153, 6, 243, 202, 229, 161, 198, 56, 172, 27, 146, 1, 0, 0, 0, 5, 248, 27, 20, 88, 145, 38, 229, 234, 204, 54, 157, 212, 162, 139, 37, 204, 78, 141, 191, 70, 123, 2, 12, 193, 2, 227, 116, 201, 46, 65, 4, 96]).buffer;

var algo = { name: "ECDSA", namedCurve: "P-256", hash: { name: "SHA-256" }}
var {publicKey, privateKey} = await crypto.subtle.generateKey(algo,false,['verify', 'sign'])
var signature = await crypto.subtle.sign(algo,privateKey,signedData)
console.log(signature)

var verified = await crypto.subtle.verify(
  algo,
  publicKey,
  signature,
  signedData
);
console.log(verified)

So looks like { name: "ECDSA", namedCurve: "P-256", hash: { name: "SHA-256" } } is the problem. Anyone know what I should be using? have I missed a prop?

@emlun
Copy link

emlun commented May 16, 2021

I solved it. It's the signature format.

What is interesting is if I generate a key pair with the above settings and then sign some data the signature is always 64 bytes where as the signature from the authenticator is 70 bytes:

Well spotted - this was the key! It seems like SubtleCrypto emits and expects signatures in "raw" format - in the case of ECDSA signatures, that means just the r and s parts of the signature as two simply concatenated bare 32-byte integers. FIDO authenticators, however, emit signatures in an ASN.1 encoded format: an ASN.1 sequence containing two ASN.1 integers, which incurs between 6 and 8 bytes of overhead (depending on leading zeroes). You can use this function to hex-encode byte arrays and put them into this online ASN.1 decoder to see for yourself:

function btoh(bytes /* Uint8Array */) {
  return [...bytes].map(b => b.toString(16).padStart(2, "0")).join("");
}

For example, try it with this signature value:

// 3045022100e714b457530bd6698902ad9b82d5ef1fbc517e561e83506d86d567657de8b78002207cdf875cce11e6b446551d91203e842ad57c77c82105453c93f5d614b4a5ff02
new Uint8Array([48, 69, 2, 33, 0, 231, 20, 180, 87, 83, 11, 214, 105, 137, 2, 173, 155, 130, 213, 239, 31, 188, 81, 126, 86, 30, 131, 80, 109, 134, 213, 103, 101, 125, 232, 183, 128, 2, 32, 124, 223, 135, 92, 206, 17, 230, 180, 70, 85, 29, 145, 32, 62, 132, 42, 213, 124, 119, 200, 33, 5, 69, 60, 147, 245, 214, 20, 180, 165, 255, 2])

It's easy enough to "unpack" special cases like this and pack them into the "raw" form. The main pitfall here is that a leading zero is added if the most significant bit happens to be 1:

var usignature = new Uint8Array(signature);
var rStart = usignature[4] === 0 ? 5 : 4;
var rEnd = rStart + 32;
var sStart = usignature[rEnd + 2] === 0 ? rEnd + 3 : rEnd + 2;
var r = usignature.slice(rStart, rEnd);
var s = usignature.slice(sStart);
var rawSignature = new Uint8Array([...r, ...s]);

So if we expand the original example with this, we get:

// paste in console of any https site to run (e.g. this page)
// sample arguments for registration
// https://fidoalliance.org/specs/fido-u2f-v1.1-id-20160915/fido-u2f-raw-message-formats-v1.1-id-20160915.html#authentication-response-message-success

var createCredentialDefaultArgs = {
    publicKey: {
        // Relying Party (a.k.a. - Service):
        rp: {
            name: "Acme"
        },

        // User:
        user: {
            id: new Uint8Array(16),
            name: "john.p.smith@example.com",
            displayName: "John P. Smith"
        },

        pubKeyCredParams: [{
            type: "public-key",
            alg: -7
        }],

        attestation: "direct",

        timeout: 60000,

        challenge: new Uint8Array([ // must be a cryptographically random number sent from a server
            0x8C, 0x0A, 0x26, 0xFF, 0x22, 0x91, 0xC1, 0xE9, 0xB9, 0x4E, 0x2E, 0x17, 0x1A, 0x98, 0x6A, 0x73,
            0x71, 0x9D, 0x43, 0x48, 0xD5, 0xA7, 0x6A, 0x15, 0x7E, 0x38, 0x94, 0x52, 0x77, 0x97, 0x0F, 0xEF
        ]).buffer
    }
};

// sample arguments for login
var getCredentialDefaultArgs = {
    publicKey: {
        timeout: 60000,
        // allowCredentials: [newCredential] // see below
        challenge: new Uint8Array([ // must be a cryptographically random number sent from a server
            0x79, 0x50, 0x68, 0x71, 0xDA, 0xEE, 0xEE, 0xB9, 0x94, 0xC3, 0xC2, 0x15, 0x67, 0x65, 0x26, 0x22,
            0xE3, 0xF3, 0xAB, 0x3B, 0x78, 0x2E, 0xD5, 0x6F, 0x81, 0x26, 0xE2, 0xA6, 0x01, 0x7D, 0x74, 0x50
        ]).buffer
    },
};

// register / create a new credential
var cred = await navigator.credentials.create(createCredentialDefaultArgs)
console.log("NEW CREDENTIAL", cred);

// normally the credential IDs available for an account would come from a server
// but we can just copy them from above...
var idList = [{
    id: cred.rawId,
    transports: ["usb", "nfc", "ble"],
    type: "public-key"
}];
getCredentialDefaultArgs.publicKey.allowCredentials = idList;

var assertation = await navigator.credentials.get(getCredentialDefaultArgs);
console.log("ASSERTION", assertation);

// verify signature on server
var signature = await assertation.response.signature;
console.log("SIGNATURE", signature)

var clientDataJSON = await assertation.response.clientDataJSON;
console.log("clientDataJSON", clientDataJSON)

var authenticatorData = new Uint8Array(await assertation.response.authenticatorData);
console.log("authenticatorData", authenticatorData)

var clientDataHash = new Uint8Array(await crypto.subtle.digest("SHA-256", clientDataJSON));
console.log("clientDataHash", clientDataHash)

// concat authenticatorData and clientDataHash
var signedData = new Uint8Array(authenticatorData.length + clientDataHash.length);
signedData.set(authenticatorData);
signedData.set(clientDataHash, authenticatorData.length);
console.log("signedData", signedData)

// import key
var key = await crypto.subtle.importKey(
// The getPublicKey() operation thus returns the credential public key as a SubjectPublicKeyInfo. See:
// 
// https://w3c.github.io/webauthn/#sctn-public-key-easy
//
// crypto.subtle can import the spki format:
// 
// https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey
  "spki", // "spki" Simple Public Key Infrastructure rfc2692

  cred.response.getPublicKey(),
  {
    // these are the algorithm options
    // await cred.response.getPublicKeyAlgorithm() // returns -7
    // -7 is ES256 with P-256 // search -7 in https://w3c.github.io/webauthn
    // the W3C webcrypto docs:
    //
    // https://www.w3.org/TR/WebCryptoAPI/#informative-references (scroll down a bit)
    //
    // ES256 corrisponds with the following AlgorithmIdentifier:
    name: "ECDSA",
    namedCurve: "P-256",
    hash: { name: "SHA-256" }
  },
  false, //whether the key is extractable (i.e. can be used in exportKey)
  ["verify"] //"verify" for public key import, "sign" for private key imports
);

// Convert signature from ASN.1 sequence to "raw" format
var usignature = new Uint8Array(signature);
var rStart = usignature[4] === 0 ? 5 : 4;
var rEnd = rStart + 32;
var sStart = usignature[rEnd + 2] === 0 ? rEnd + 3 : rEnd + 2;
var r = usignature.slice(rStart, rEnd);
var s = usignature.slice(sStart);
var rawSignature = new Uint8Array([...r, ...s]);

// check signature with public key and signed data 
var verified = await crypto.subtle.verify(
  { name: "ECDSA", namedCurve: "P-256", hash: { name: "SHA-256" } },
  key,
  rawSignature,
  signedData.buffer
);
// verified is now true!
console.log('verified', verified)

which has successfully verified every time I tried it. But you should probably get a real ASN.1 decoder if you need to actually do transformations like this.

@philholden
Copy link
Author

Amazing thanks so much! It was huge puzzle and I could not stop thinking about it. So great to see:

verified true

I was beginning to think I never would.

Passwordless auth should be prevalent by now but fells like we turned up to the party a few hours early. I was surprised to see my Galaxy S9 can do Webauthn using the touch sensor for test of presence. This makes my me think a large number of people without a Yubikey could login to a desktop site by scanning a QR with the challenge.

Look forward to playing with the UX for this.

Thanks again for the help and your dedication.

@rithvikvibhu
Copy link

Oh my god I've spent hours on this finally found this gist, reading this and "I have the exact same problem!" till the end.
The last bit about raw signatures worked for me too, just wanted to say really appreciate the time taken to figure this out. Thanks @philholden, @emlun and @MasterKale!

btw, looking this up, also found https://crypto.stackexchange.com/questions/1795/how-can-i-convert-a-der-ecdsa-signature-to-asn-1 explaining the same what you discussed.

@enricobottazzi
Copy link

Thanks guys! Amazing discussion and help are provided here. I felt your pain reading through it :)

@nikosft
Copy link

nikosft commented Nov 16, 2022

@emlun You are a hero. @philholden Thanks for pushing this forward.

@dagnelies
Copy link

dagnelies commented Nov 23, 2022

🎉 Poah, that was a tough nut to crack!

Moreover, I would like to emphasise that whether the signature is ASN.1 wrapped or not depends on the algorithm used according to the spec https://w3c.github.io/webauthn/#sctn-signature-attestation-types

6.5.6. Signature Formats for Packed Attestation, FIDO U2F Attestation, and Assertion Signatures

[...] For COSEAlgorithmIdentifier -7 (ES256) [...] the sig value MUST be encoded as an ASN.1 [...]
[...] For COSEAlgorithmIdentifier -257 (RS256) [...] The signature is not ASN.1 wrapped.
[...] For COSEAlgorithmIdentifier -37 (PS256) [...] The signature is not ASN.1 wrapped.

What about the -8 algo that is also recommended? ASN.1 wrapped or not? I guess that's simply missing in the specs right now. ;)

@emlun
Copy link

emlun commented Nov 23, 2022 via email

@dagnelies
Copy link

Sure @emlun , here you go: w3c/webauthn#1829

@dagnelies
Copy link

And one more thing, I made a tool to verify / validate webauthn signatures: https://webauthn.passwordless.id/demos/playground.html
Just scroll at the bottom and you can input your base64 encoded raw values. Maybe this is helpful for someone.

@mmv08
Copy link

mmv08 commented Jan 10, 2024

Thanks! It took me ages to figure out how to decode the public key from getPublicKey(). You're truly a legend.

@sarvagnakadiya
Copy link

Big shoutout to @philholden for bringing up the topic! 🙌
And a massive thanks to @emlun for cracking the code like a boss! 🚀
You guys rock!

@sarvagnakadiya
Copy link

sarvagnakadiya commented Jan 31, 2024

Have you guys checked this webauthn' verification

const signatureIsValid = storedCredential.publicKey.verify(
    signature, signedData);

I tried it, but it was saying verify function not found.

if you guys know how to use this function properly will be a great help!

I tried like this

//setting up public key onto useState after creating
 const credential = await navigator.credentials.create({
        publicKey: publicKeyCredentialCreationOptions,
 });
setPubKeyObj(credential);

and now verifying:

try {
      const assertion = await navigator.credentials.get({
        publicKey: {
          challenge: Uint8Array.from("randomStringFromServer", (c) =>
            c.charCodeAt(0)
          ),
          rpId: "localhost",
        },
        mediation: "optional",
      });

      console.log(assertion);

      console.log("authenticator:", assertion.response.authenticatorData);
      console.log("client:", assertion.response.clientDataJSON);

      const authenticatorDataBytes = assertion.response.authenticatorData;

      var hashedClientDataJSON = new Uint8Array(
        await crypto.subtle.digest("SHA-256", assertion.response.clientDataJSON)
      );
      console.log("clientDataHash", hashedClientDataJSON);

      const signedData = authenticatorDataBytes + hashedClientDataJSON;

      const signatureIsValid = pubKeyObj.verify(
        assertion.response.signature,
        signedData
      );

      console.log(signatureIsValid);

      if (signatureIsValid) {
        return "Hooray! User is authenticated! 🎉";
      } else {
        return "Verification failed. 😭";
      }
    } catch (error) {
      console.error("Error during login:", error);
      return "Error during login.";
    } 

but it says:

Error during login: TypeError: pubKeyObj.verify is not a function
    at signIn

@getify
Copy link

getify commented Mar 9, 2024

Thank you for this thread! Was the only way I found to solve the problem I was facing!

I was just a bit nervous about the lower-level code and wanted a bit more assurances from using a quality/compliant ASN.1 parser.

I just wanted to report that I found a reasonably sized, and capable for this task, ASN.1 parser lib (in JS): https://www.npmjs.com/package/@yoursunny/asn1 It's only 7k minified (and gzips to ~2.5k), and comes in UMD/CJS format so it will work in a lot of environments including even somewhat older browsers and older Node instances.

So instead of the above code (adapted into):

function parseSignature(sig) {
   var usignature = new Uint8Array(sig);
   var rStart = usignature[4] === 0 ? 5 : 4;
   var rEnd = rStart + 32;
   var sStart = usignature[rEnd + 2] === 0 ? rEnd + 3 : rEnd + 2;
   var r = usignature.slice(rStart, rEnd);
   var s = usignature.slice(sStart);
   return new Uint8Array([...r, ...s]);
}

I was able to use the library like this:

function parseSignature(sig) {
   var der = ASN1.parseVerbose(new Uint8Array(sig));
   return new Uint8Array([ ...der.children[0].value, ...der.children[1].value, ]);
}

Perhaps that helps others who land here.

@philholden
Copy link
Author

philholden commented Mar 9, 2024 via email

@dagnelies
Copy link

@getify Pay attention that depending on the algorithm used by the credential, the signature may or may not be ASN.1 wrapped!

Moreover, I would like to emphasise that whether the signature is ASN.1 wrapped or not depends on the algorithm used according to the spec https://w3c.github.io/webauthn/#sctn-signature-attestation-types

6.5.6. Signature Formats for Packed Attestation, FIDO U2F Attestation, and Assertion Signatures
[...] For COSEAlgorithmIdentifier -7 (ES256) [...] the sig value MUST be encoded as an ASN.1 [...]
[...] For COSEAlgorithmIdentifier -257 (RS256) [...] The signature is not ASN.1 wrapped.
[...] For COSEAlgorithmIdentifier -37 (PS256) [...] The signature is not ASN.1 wrapped.

What about the -8 algo that is also recommended? ASN.1 wrapped or not? I guess that's simply missing in the specs right now. ;)

I recommend using a lib like mine to avoid such issues: https://webauthn.passwordless.id/demos/playground.html , you can also verify signatures there.

@getify
Copy link

getify commented Mar 10, 2024

the signature may or may not be ASN.1 wrapped!

yep... that's true. the spec basically says that -7 (ECDSA P-256) is the only one that's wrapped, and it says that other algorithms "should not" be wrapped. so I only call that function on that specific algorithm.

@getify
Copy link

getify commented Mar 12, 2024

@dagnelies

I recommend using a lib like mine

Your lib is great, and quite comprehensive. I've been working on a smaller lib for a more narrow purpose (and I link to yours as an alternative option):

https://github.com/mylofi/webauthn-local-client

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment