Skip to content

Instantly share code, notes, and snippets.

@yackermann
Last active April 4, 2024 15:09
Show Gist options
  • Save yackermann/dbeb2c2b76362052e5268224660b6fbc to your computer and use it in GitHub Desktop.
Save yackermann/dbeb2c2b76362052e5268224660b6fbc to your computer and use it in GitHub Desktop.
WebAuthn Packed attestation verification sample in NodeJS
const crypto = require('crypto');
const base64url = require('base64url');
const cbor = require('cbor');
const jsrsasign = require('jsrsasign');
const elliptic = require('elliptic');
const NodeRSA = require('node-rsa');
let COSEKEYS = {
'kty' : 1,
'alg' : 3,
'crv' : -1,
'x' : -2,
'y' : -3,
'n' : -1,
'e' : -2
}
let COSEKTY = {
'OKP': 1,
'EC2': 2,
'RSA': 3
}
let COSERSASCHEME = {
'-3': 'pss-sha256',
'-39': 'pss-sha512',
'-38': 'pss-sha384',
'-65535': 'pkcs1-sha1',
'-257': 'pkcs1-sha256',
'-258': 'pkcs1-sha384',
'-259': 'pkcs1-sha512'
}
var COSECRV = {
'1': 'p256',
'2': 'p384',
'3': 'p521'
}
var COSEALGHASH = {
'-257': 'sha256',
'-258': 'sha384',
'-259': 'sha512',
'-65535': 'sha1',
'-39': 'sha512',
'-38': 'sha384',
'-37': 'sha256',
'-260': 'sha256',
'-261': 'sha512',
'-7': 'sha256',
'-36': 'sha512'
}
let hash = (alg, message) => {
return crypto.createHash(alg).update(message).digest();
}
let base64ToPem = (b64cert) => {
let pemcert = '';
for(let i = 0; i < b64cert.length; i += 64)
pemcert += b64cert.slice(i, i + 64) + '\n';
return '-----BEGIN CERTIFICATE-----\n' + pemcert + '-----END CERTIFICATE-----';
}
var getCertificateInfo = (certificate) => {
let subjectCert = new jsrsasign.X509();
subjectCert.readCertPEM(certificate);
let subjectString = subjectCert.getSubjectString();
let subjectParts = subjectString.slice(1).split('/');
let subject = {};
for(let field of subjectParts) {
let kv = field.split('=');
subject[kv[0]] = kv[1];
}
let version = subjectCert.version;
let basicConstraintsCA = !!subjectCert.getExtBasicConstraints().cA;
return {
subject, version, basicConstraintsCA
}
}
var parseAuthData = (buffer) => {
let rpIdHash = buffer.slice(0, 32); buffer = buffer.slice(32);
let flagsBuf = buffer.slice(0, 1); buffer = buffer.slice(1);
let flagsInt = flagsBuf[0];
let flags = {
up: !!(flagsInt & 0x01),
uv: !!(flagsInt & 0x04),
at: !!(flagsInt & 0x40),
ed: !!(flagsInt & 0x80),
flagsInt
}
let counterBuf = buffer.slice(0, 4); buffer = buffer.slice(4);
let counter = counterBuf.readUInt32BE(0);
let aaguid = undefined;
let credID = undefined;
let COSEPublicKey = undefined;
if(flags.at) {
aaguid = buffer.slice(0, 16); buffer = buffer.slice(16);
let credIDLenBuf = buffer.slice(0, 2); buffer = buffer.slice(2);
let credIDLen = credIDLenBuf.readUInt16BE(0);
credID = buffer.slice(0, credIDLen); buffer = buffer.slice(credIDLen);
COSEPublicKey = buffer;
}
return {rpIdHash, flagsBuf, flags, counter, counterBuf, aaguid, credID, COSEPublicKey}
}
let verifyPackedAttestation = (webAuthnResponse) => {
let attestationBuffer = base64url.toBuffer(webAuthnResponse.response.attestationObject);
let attestationStruct = cbor.decodeAllSync(attestationBuffer)[0];
let authDataStruct = parseAuthData(attestationStruct.authData);
let clientDataHashBuf = hash('sha256', base64url.toBuffer(webAuthnResponse.response.clientDataJSON));
let signatureBaseBuffer = Buffer.concat([attestationStruct.authData, clientDataHashBuf]);
let signatureBuffer = attestationStruct.attStmt.sig
let signatureIsValid = false;
if(attestationStruct.attStmt.x5c) {
/* ----- Verify FULL attestation ----- */
let leafCert = base64ToPem(attestationStruct.attStmt.x5c[0].toString('base64'));
let certInfo = getCertificateInfo(leafCert);
if(certInfo.subject.OU !== 'Authenticator Attestation')
throw new Error('Batch certificate OU MUST be set strictly to "Authenticator Attestation"!');
if(!certInfo.subject.CN)
throw new Error('Batch certificate CN MUST no be empty!');
if(!certInfo.subject.O)
throw new Error('Batch certificate CN MUST no be empty!');
if(!certInfo.subject.C || certInfo.subject.C.length !== 2)
throw new Error('Batch certificate C MUST be set to two character ISO 3166 code!');
if(certInfo.basicConstraintsCA)
throw new Error('Batch certificate basic constraints CA MUST be false!');
if(certInfo.version !== 3)
throw new Error('Batch certificate version MUST be 3(ASN1 2)!');
signatureIsValid = crypto.createVerify('sha256')
.update(signatureBaseBuffer)
.verify(leafCert, signatureBuffer);
/* ----- Verify FULL attestation ENDS ----- */
} else if(attestationStruct.attStmt.ecdaaKeyId) {
throw new Error('ECDAA IS NOT SUPPORTED YET!');
} else {
/* ----- Verify SURROGATE attestation ----- */
let pubKeyCose = cbor.decodeAllSync(authDataStruct.COSEPublicKey)[0];
let hashAlg = COSEALGHASH[pubKeyCose.get(COSEKEYS.alg)];
if(pubKeyCose.get(COSEKEYS.kty) === COSEKTY.EC2) {
let x = pubKeyCose.get(COSEKEYS.x);
let y = pubKeyCose.get(COSEKEYS.y);
let ansiKey = Buffer.concat([Buffer.from([0x04]), x, y]);
let signatureBaseHash = hash(hashAlg, signatureBaseBuffer);
let ec = new elliptic.ec(COSECRV[pubKeyCose.get(COSEKEYS.crv)]);
let key = ec.keyFromPublic(ansiKey);
signatureIsValid = key.verify(signatureBaseHash, signatureBuffer)
} else if(pubKeyCose.get(COSEKEYS.kty) === COSEKTY.RSA) {
let signingScheme = COSERSASCHEME[pubKeyCose.get(COSEKEYS.alg)];
let key = new NodeRSA(undefined, { signingScheme });
key.importKey({
n: pubKeyCose.get(COSEKEYS.n),
e: 65537,
}, 'components-public');
signatureIsValid = key.verify(signatureBaseBuffer, signatureBuffer)
} else if(pubKeyCose.get(COSEKEYS.kty) === COSEKTY.OKP) {
let x = pubKeyCose.get(COSEKEYS.x);
let signatureBaseHash = hash(hashAlg, signatureBaseBuffer);
let key = new elliptic.eddsa('ed25519');
key.keyFromPublic(x)
signatureIsValid = key.verify(signatureBaseHash, signatureBuffer)
}
/* ----- Verify SURROGATE attestation ENDS ----- */
}
if(!signatureIsValid)
throw new Error('Failed to verify the signature!');
return true
}
let packedFullAttestationWebAuthnSample = {
"rawId": "wsLryOAxXMU54s2fCSWPzWjXHOBKPploN-UHftj4_rpIu6BZxNXppm82f7Y6iX9FEOKKeS5-N2TALeyzLnJfAA",
"id": "wsLryOAxXMU54s2fCSWPzWjXHOBKPploN-UHftj4_rpIu6BZxNXppm82f7Y6iX9FEOKKeS5-N2TALeyzLnJfAA",
"response": {
"clientDataJSON": "eyJjaGFsbGVuZ2UiOiJZTVdFVGYtUDc5aU1iLUJxZFRreVNOUmVPdmE3bksyaVZDOWZpQzhpR3ZZeXB1bkVPQ1pHWjYtWTVPVjFydk1pRGdBaldmRmk2VUMwV3lLR3NqQS1nQSIsIm9yaWdpbiI6Imh0dHBzOi8vd2ViYXV0aG4ub3JnIiwidHlwZSI6IndlYmF1dGhuLmNyZWF0ZSJ9",
"attestationObject": "o2NmbXRmcGFja2VkZ2F0dFN0bXSjY2FsZyZjc2lnWEcwRQIhAIzOihC6Ba80o5JnoYOJJ_EtEVmWQcAvxVCnsCFnVRQZAiAfeIddLPsPl1FeSX8B5xZANcQKGNoO7pb0TZPnuJdebGN4NWOBWQKzMIICrzCCAZegAwIBAgIESFs9tjANBgkqhkiG9w0BAQsFADAhMR8wHQYDVQQDDBZZdWJpY28gRklETyBQcmV2aWV3IENBMB4XDTE4MDQxMjEwNTcxMFoXDTE4MTIzMTEwNTcxMFowbzELMAkGA1UEBhMCU0UxEjAQBgNVBAoMCVl1YmljbyBBQjEiMCAGA1UECwwZQXV0aGVudGljYXRvciBBdHRlc3RhdGlvbjEoMCYGA1UEAwwfWXViaWNvIFUyRiBFRSBTZXJpYWwgMTIxMzkzOTEyNjBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABPss3TBDKMVySlDM5vYLrX0nqRtZ4eZvKXuJydQ9wrLHeIm08P-dAijLlG384BsZWJtngEqsl38oGJzNsyV0yiijbDBqMCIGCSsGAQQBgsQKAgQVMS4zLjYuMS40LjEuNDE0ODIuMS42MBMGCysGAQQBguUcAgEBBAQDAgQwMCEGCysGAQQBguUcAQEEBBIEEPigEfOMCk0VgAYXER-e3H0wDAYDVR0TAQH_BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAMvPkvVjXQiuvSZmGCB8NqTvGqhxyEfkoU-vz63PaaTsG3jEzjl0C7PZ26VxCvqWPJdM3P3e7Kp18sj4RjEHUmkya2PPipOwBd3p0qMQSQ8MeziCPLQ9uvGGb4YShcvaprMv4c21b4piza-znHneNCmmq-ZS4Y23o-vYv085_BEwyLPcmPjSZ5qWysCq7rVvZ7OWwcU1zu5RhSZyUKl8dzK9lAzs5OdRH2fzEewsW2OkB_Ow_jBvAxqwLXXTHuwMFaRfpmBoZuQlcofSrnwJ8KA-K-e0dKTz2zC8EbZrWYrSpbrHKyqxeBT6DkUd8H4tgAd5lOr_yqrtVmIaRfq07NmhhdXRoRGF0YVjElWkIjx7O4yMpVANdvRDXyuORMFonUbVZu4_Xy7IpvdRBAAAAAPigEfOMCk0VgAYXER-e3H0AQMLC68jgMVzFOeLNnwklj81o1xzgSj6ZaDflB37Y-P66SLugWcTV6aZvNn-2Ool_RRDiinkufjdkwC3ssy5yXwClAQIDJiABIVggAYD1TSpf120DSVxen8ki56kF1bmT4EXO-P0JnSk5mMwiWCB3TlMZBRqPY6llzDcfHd-oW0EHdaFNgBdlGGFobpHKlw"
}
}
let packedSurrogateAttestationWebAuthnSample = {
"id": "H6X2BnnjgOzu_Oj87vpRnwMJeJYVzwM3wtY1lhAfQ14",
"rawId": "H6X2BnnjgOzu_Oj87vpRnwMJeJYVzwM3wtY1lhAfQ14",
"response": {
"attestationObject": "o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZzn__mNzaWdZAQCPypMLXWqtCZ1sc5QdjhH-pAzm8-adpfbemd5zsym2krscwV0EeOdTrdUOdy3hWj5HuK9dIX_OpNro2jKrHfUj_0Kp-u87iqJ3MPzs-D9zXOqkbWqcY94Zh52wrPwhGfJ8BiQp5T4Q97E042hYQRDKmtv7N-BT6dywiuFHxfm1sDbUZ_yyEIN3jgttJzjp_wvk_RJmb78bLPTlym83Y0Ws73K6FFeiqFNqLA_8a4V0I088hs_IEPlj8PWxW0wnIUhI9IcRf0GEmUwTBpbNDGpIFGOudnl_C3YuXuzK3R6pv2r7m9-9cIIeeYXD9BhSMBQ0A8oxBbVF7j-0xXDNrXHZaGF1dGhEYXRhWQFnSZYN5YgOjGh0NBcPZHZgW4_krrmihjLHmVzzuoMdl2NBAAAAOKjVmSRjt0nqud40p1PeHgEAIB-l9gZ544Ds7vzo_O76UZ8DCXiWFc8DN8LWNZYQH0NepAEDAzn__iBZAQDAIqzybPPmgeL5OR6JKq9bWDiENJlN_LePQEnf1_sgOm4FJ9kBTbOTtWplfoMXg40A7meMppiRqP72A3tmILwZ5xKIyY7V8Y2t8X1ilYJol2nCKOpAEqGLTRJjF64GQxen0uFpi1tA6l6N-ZboPxjky4aidBdUP22YZuEPCO8-9ZTha8qwvTgZwMHhZ40TUPEJGGWOnHNlYmqnfFfk0P-UOZokI0rqtqqQGMwzV2RrH2kjKTZGfyskAQnrqf9PoJkye4KUjWkWnZzhkZbrDoLyTEX2oWvTTflnR5tAVMQch4UGgEHSZ00G5SFoc19nGx_UJcqezx5cLZsny-qQYDRjIUMBAAE",
"clientDataJSON": "eyJvcmlnaW4iOiJodHRwOi8vbG9jYWxob3N0OjMwMDAiLCJjaGFsbGVuZ2UiOiJBWGtYV1hQUDNnTHg4T0xscGtKM2FSUmhGV250blNFTmdnbmpEcEJxbDFuZ0tvbDd4V3dldlVZdnJwQkRQM0xFdmRyMkVPU3RPRnBHR3huTXZYay1WdyIsInR5cGUiOiJ3ZWJhdXRobi5jcmVhdGUifQ"
},
"type": "public-key"
}
verifyPackedAttestation(packedFullAttestationWebAuthnSample)
verifyPackedAttestation(packedSurrogateAttestationWebAuthnSample)
@yackermann
Copy link
Author

Don't forget to install dependencies:

npm i base64url cbor jsrsasign node-rsa elliptic

@raullucero
Copy link

raullucero commented Apr 18, 2021

thanks for this 👍

@antonymott
Copy link

Thanks Yuriy. So useful! So happy I found your stuff. Many guides that walk devs through registering and authentication leave assertion and attestation only at a high level. You've curated, and authored, a wonderful set of resources. I wonder if the publishers of https://webauthn.guide/ for example might add a link to your stuff at the end of their guide.

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