Skip to content

Instantly share code, notes, and snippets.

@herrjemand
Last active August 14, 2021 07:18
Show Gist options
  • Save herrjemand/c5a84de5c04ef41b3ac7fd12d0cbceae to your computer and use it in GitHub Desktop.
Save herrjemand/c5a84de5c04ef41b3ac7fd12d0cbceae to your computer and use it in GitHub Desktop.
WebAuthn Android Keystore attestation verification sample in NodeJS
const crypto = require('crypto');
const base64url = require('base64url');
const cbor = require('cbor');
const asn1 = require('@lapo/asn1js');
const jsrsasign = require('jsrsasign');
/* Android Keystore Root is not published anywhere.
* This certificate was extracted from one of the attestations
* The last certificate in x5c must match this certificate
* This needs to be checked to ensure that malicious party wont generate fake attestations
*/
let androidkeystoreroot = 'MIICizCCAjKgAwIBAgIJAKIFntEOQ1tXMAoGCCqGSM49BAMCMIGYMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91bnRhaW4gVmlldzEVMBMGA1UECgwMR29vZ2xlLCBJbmMuMRAwDgYDVQQLDAdBbmRyb2lkMTMwMQYDVQQDDCpBbmRyb2lkIEtleXN0b3JlIFNvZnR3YXJlIEF0dGVzdGF0aW9uIFJvb3QwHhcNMTYwMTExMDA0MzUwWhcNMzYwMTA2MDA0MzUwWjCBmDELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxFTATBgNVBAoMDEdvb2dsZSwgSW5jLjEQMA4GA1UECwwHQW5kcm9pZDEzMDEGA1UEAwwqQW5kcm9pZCBLZXlzdG9yZSBTb2Z0d2FyZSBBdHRlc3RhdGlvbiBSb290MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE7l1ex+HA220Dpn7mthvsTWpdamguD/9/SQ59dx9EIm29sa/6FsvHrcV30lacqrewLVQBXT5DKyqO107sSHVBpKNjMGEwHQYDVR0OBBYEFMit6XdMRcOjzw0WEOR5QzohWjDPMB8GA1UdIwQYMBaAFMit6XdMRcOjzw0WEOR5QzohWjDPMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgKEMAoGCCqGSM49BAMCA0cAMEQCIDUho++LNEYenNVg8x1YiSBq3KNlQfYNns6KGYxmSGB7AiBNC/NR2TB8fVvaNTQdqEcbY6WFZTytTySn502vQX3xvw==';
let COSEKEYS = {
'kty' : 1,
'alg' : 3,
'crv' : -1,
'x' : -2,
'y' : -3,
'n' : -1,
'e' : -2
}
var hash = (alg, message) => {
return crypto.createHash(alg).update(message).digest();
}
var 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 findOID = (asn1object, oid) => {
if(!asn1object.sub)
return
for(let sub of asn1object.sub) {
if(sub.typeName() !== 'OBJECT_IDENTIFIER' || sub.content() !== oid) {
let result = findOID(sub, oid);
if(result)
return result
} else {
return asn1object
}
}
}
let asn1ObjectToJSON = (asn1object) => {
let JASN1 = {
'type': asn1object.typeName()
}
if(!asn1object.sub) {
if(asn1object.typeName() === 'BIT_STRING' || asn1object.typeName() === 'OCTET_STRING')
JASN1.data = asn1object.stream.enc.slice(asn1object.posContent(), asn1object.posEnd());
else
JASN1.data = asn1object.content();
return JASN1
}
JASN1.data = [];
for(let sub of asn1object.sub) {
JASN1.data.push(asn1ObjectToJSON(sub));
}
return JASN1
}
let containsASN1Tag = (seq, tag) => {
for(let member of seq)
if(member.type === '[' + tag + ']')
return true
return false
}
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}
}
var getCertificateSubject = (certificate) => {
let subjectCert = new jsrsasign.X509();
subjectCert.readCertPEM(certificate);
let subjectString = subjectCert.getSubjectString();
let subjectFields = subjectString.slice(1).split('/');
let fields = {};
for(let field of subjectFields) {
let kv = field.split('=');
fields[kv[0]] = kv[1];
}
return fields
}
var validateCertificatePath = (certificates) => {
if((new Set(certificates)).size !== certificates.length)
throw new Error('Failed to validate certificates path! Dublicate certificates detected!');
for(let i = 0; i < certificates.length; i++) {
let subjectPem = certificates[i];
let subjectCert = new jsrsasign.X509();
subjectCert.readCertPEM(subjectPem);
let issuerPem = '';
if(i + 1 >= certificates.length)
issuerPem = subjectPem;
else
issuerPem = certificates[i + 1];
let issuerCert = new jsrsasign.X509();
issuerCert.readCertPEM(issuerPem);
if(subjectCert.getIssuerString() !== issuerCert.getSubjectString())
throw new Error('Failed to validate certificate path! Issuers dont match!');
let subjectCertStruct = jsrsasign.ASN1HEX.getTLVbyList(subjectCert.hex, 0, [0]);
let algorithm = subjectCert.getSignatureAlgorithmField();
let signatureHex = subjectCert.getSignatureValueHex()
let Signature = new jsrsasign.crypto.Signature({alg: algorithm});
Signature.init(issuerPem);
Signature.updateHex(subjectCertStruct);
if(!Signature.verify(signatureHex))
throw new Error('Failed to validate certificate path!')
}
return true
}
let verifyAndroidKeyAttestation = (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));
/* ----- VERIFY SIGNATURE ----- */
let signatureBaseBuffer = Buffer.concat([attestationStruct.authData, clientDataHashBuf]);
let signatureBuffer = attestationStruct.attStmt.sig
let signatureIsValid = false;
let leafCert = base64ToPem(attestationStruct.attStmt.x5c[0].toString('base64'));
signatureIsValid = crypto.createVerify('sha256')
.update(signatureBaseBuffer)
.verify(leafCert, signatureBuffer);
if(!signatureIsValid)
throw new Error('Failed to verify the signature!');
let attestationRootCertificateBuffer = attestationStruct.attStmt.x5c[attestationStruct.attStmt.x5c.length - 1];
if(attestationRootCertificateBuffer.toString('base64') !== androidkeystoreroot)
throw new Error('Attestation root is not invalid!');
let certPath = attestationStruct.attStmt.x5c.map((cert) => {
cert = cert.toString('base64');
let pemcert = '';
for(let i = 0; i < cert.length; i += 64)
pemcert += cert.slice(i, i + 64) + '\n';
return '-----BEGIN CERTIFICATE-----\n' + pemcert + '-----END CERTIFICATE-----';
})
validateCertificatePath(certPath);
/* ----- VERIFY SIGNATURE ENDS ----- */
let certASN1 = asn1.decode(attestationStruct.attStmt.x5c[0]);
/* ----- VERIFY PUBLIC KEY MATCHING ----- */
let certJSON = asn1ObjectToJSON(certASN1);
let certTBS = certJSON.data[0];
let certPubKey = certTBS.data[6];
let certPubKeyBuff = certPubKey.data[1].data;
/* CHECK PUBKEY */
let coseKey = cbor.decodeAllSync(authDataStruct.COSEPublicKey)[0];
/* ANSI ECC KEY is 0x04 with X and Y coefficients. But certs have it padded with 0x00 so for simplicity it easier to do it that way */
let ansiKey = Buffer.concat([Buffer([0x00, 0x04]), coseKey.get(COSEKEYS.x), coseKey.get(COSEKEYS.y)])
if(ansiKey.toString('hex') !== certPubKeyBuff.toString('hex'))
throw new Error('Certificate public key does not match public key in authData')
/* ----- VERIFY PUBLIC KEY MATCHING ENDS ----- */
/* ----- VERIFY CERTIFICATE REQUIREMENTS ----- */
let AttestationExtension = findOID(certASN1, '1.3.6.1.4.1.11129.2.1.17');
let AttestationExtensionJSON = asn1ObjectToJSON(AttestationExtension);
let attestationChallenge = AttestationExtensionJSON.data[1].data[0].data[4].data
if(attestationChallenge.toString('hex') !== clientDataHashBuf.toString('hex'))
throw new Error('Certificate attestation challenge is not set to the clientData hash!');
let softwareEnforcedAuthz = AttestationExtensionJSON.data[1].data[0].data[6].data;
let teeEnforcedAuthz = AttestationExtensionJSON.data[1].data[0].data[7].data;
if(containsASN1Tag(softwareEnforcedAuthz, 600) || containsASN1Tag(teeEnforcedAuthz, 600))
throw new Error('TEE or Software autherisation list contains "allApplication" flag, which means that credential is not bound to the RP!');
/* ----- VERIFY CERTIFICATE REQUIREMENTS ENDS ----- */
return true
}
let androidKeyWebAuthnSample = {
"rawId": "AZD7huwZVx7aW1efRa6Uq3JTQNorj3qA9yrLINXEcgvCQYtWiSQa1eOIVrXfCmip6MzP8KaITOvRLjy3TUHO7_c",
"id": "AZD7huwZVx7aW1efRa6Uq3JTQNorj3qA9yrLINXEcgvCQYtWiSQa1eOIVrXfCmip6MzP8KaITOvRLjy3TUHO7_c",
"response": {
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiVGY2NWJTNkQ1dGVtaDJCd3ZwdHFnQlBiMjVpWkRSeGp3QzVhbnM5MUlJSkRyY3JPcG5XVEs0TFZnRmplVVY0R0RNZTQ0dzhTSTVOc1pzc0lYVFV2RGciLCJvcmlnaW4iOiJodHRwczpcL1wvd2ViYXV0aG4ub3JnIiwiYW5kcm9pZFBhY2thZ2VOYW1lIjoiY29tLmFuZHJvaWQuY2hyb21lIn0",
"attestationObject": "o2NmbXRrYW5kcm9pZC1rZXlnYXR0U3RtdKNjYWxnJmNzaWdYRjBEAiAsp6jPtimcSgc-fgIsVwgqRsZX6eU7KKbkVGWa0CRJlgIgH5yuf_laPyNy4PlS6e8ZHjs57iztxGiTqO7G91sdlWBjeDVjg1kCzjCCAsowggJwoAMCAQICAQEwCgYIKoZIzj0EAwIwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQKDAxHb29nbGUsIEluYy4xEDAOBgNVBAsMB0FuZHJvaWQxOzA5BgNVBAMMMkFuZHJvaWQgS2V5c3RvcmUgU29mdHdhcmUgQXR0ZXN0YXRpb24gSW50ZXJtZWRpYXRlMB4XDTE4MTIwMjA5MTAyNVoXDTI4MTIwMjA5MTAyNVowHzEdMBsGA1UEAwwUQW5kcm9pZCBLZXlzdG9yZSBLZXkwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQ4SaIP3ibDSwCIORpYJ3g9_5OICxZUCIqt-vV6JZVJoXQ8S1JFzyaFz5EFQ2fNT6-5SE5wWTZRAR_A3M52IcaPo4IBMTCCAS0wCwYDVR0PBAQDAgeAMIH8BgorBgEEAdZ5AgERBIHtMIHqAgECCgEAAgEBCgEBBCAqQ4LXu9idi1vfF3LP7MoUOSSHuf1XHy63K9-X3gbUtgQAMIGCv4MQCAIGAWduLuFwv4MRCAIGAbDqja1wv4MSCAIGAbDqja1wv4U9CAIGAWduLt_ov4VFTgRMMEoxJDAiBB1jb20uZ29vZ2xlLmF0dGVzdGF0aW9uZXhhbXBsZQIBATEiBCBa0F7CIcj4OiJhJ97FV1AMPldLxgElqdwhywvkoAZglTAzoQUxAwIBAqIDAgEDowQCAgEApQUxAwIBBKoDAgEBv4N4AwIBF7-DeQMCAR6_hT4DAgEAMB8GA1UdIwQYMBaAFD_8rNYasTqegSC41SUcxWW7HpGpMAoGCCqGSM49BAMCA0gAMEUCIGd3OQiTgFX9Y07kE-qvwh2Kx6lEG9-Xr2ORT5s7AK_-AiEAucDIlFjCUo4rJfqIxNY93HXhvID7lNzGIolS0E-BJBhZAnwwggJ4MIICHqADAgECAgIQATAKBggqhkjOPQQDAjCBmDELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExFjAUBgNVBAcMDU1vdW50YWluIFZpZXcxFTATBgNVBAoMDEdvb2dsZSwgSW5jLjEQMA4GA1UECwwHQW5kcm9pZDEzMDEGA1UEAwwqQW5kcm9pZCBLZXlzdG9yZSBTb2Z0d2FyZSBBdHRlc3RhdGlvbiBSb290MB4XDTE2MDExMTAwNDYwOVoXDTI2MDEwODAwNDYwOVowgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRUwEwYDVQQKDAxHb29nbGUsIEluYy4xEDAOBgNVBAsMB0FuZHJvaWQxOzA5BgNVBAMMMkFuZHJvaWQgS2V5c3RvcmUgU29mdHdhcmUgQXR0ZXN0YXRpb24gSW50ZXJtZWRpYXRlMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE6555-EJjWazLKpFMiYbMcK2QZpOCqXMmE_6sy_ghJ0whdJdKKv6luU1_ZtTgZRBmNbxTt6CjpnFYPts-Ea4QFKNmMGQwHQYDVR0OBBYEFD_8rNYasTqegSC41SUcxWW7HpGpMB8GA1UdIwQYMBaAFMit6XdMRcOjzw0WEOR5QzohWjDPMBIGA1UdEwEB_wQIMAYBAf8CAQAwDgYDVR0PAQH_BAQDAgKEMAoGCCqGSM49BAMCA0gAMEUCIEuKm3vugrzAM4euL8CJmLTdw42rJypFn2kMx8OS1A-OAiEA7toBXbb0MunUhDtiTJQE7zp8zL1e-yK75_65dz9ZP_tZAo8wggKLMIICMqADAgECAgkAogWe0Q5DW1cwCgYIKoZIzj0EAwIwgZgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRYwFAYDVQQHDA1Nb3VudGFpbiBWaWV3MRUwEwYDVQQKDAxHb29nbGUsIEluYy4xEDAOBgNVBAsMB0FuZHJvaWQxMzAxBgNVBAMMKkFuZHJvaWQgS2V5c3RvcmUgU29mdHdhcmUgQXR0ZXN0YXRpb24gUm9vdDAeFw0xNjAxMTEwMDQzNTBaFw0zNjAxMDYwMDQzNTBaMIGYMQswCQYDVQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNTW91bnRhaW4gVmlldzEVMBMGA1UECgwMR29vZ2xlLCBJbmMuMRAwDgYDVQQLDAdBbmRyb2lkMTMwMQYDVQQDDCpBbmRyb2lkIEtleXN0b3JlIFNvZnR3YXJlIEF0dGVzdGF0aW9uIFJvb3QwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATuXV7H4cDbbQOmfua2G-xNal1qaC4P_39JDn13H0Qibb2xr_oWy8etxXfSVpyqt7AtVAFdPkMrKo7XTuxIdUGko2MwYTAdBgNVHQ4EFgQUyK3pd0xFw6PPDRYQ5HlDOiFaMM8wHwYDVR0jBBgwFoAUyK3pd0xFw6PPDRYQ5HlDOiFaMM8wDwYDVR0TAQH_BAUwAwEB_zAOBgNVHQ8BAf8EBAMCAoQwCgYIKoZIzj0EAwIDRwAwRAIgNSGj74s0Rh6c1WDzHViJIGrco2VB9g2ezooZjGZIYHsCIE0L81HZMHx9W9o1NB2oRxtjpYVlPK1PJKfnTa9BffG_aGF1dGhEYXRhWMWVaQiPHs7jIylUA129ENfK45EwWidRtVm7j9fLsim91EUAAAAAKPN9K5K4QcSwKoYM73zANABBAVUvAmX241vMKYd7ZBdmkNWaYcNYhoSZCJjFRGmROb6I4ygQUVmH6k9IMwcbZGeAQ4v4WMNphORudwje5h7ty9ClAQIDJiABIVggOEmiD94mw0sAiDkaWCd4Pf-TiAsWVAiKrfr1eiWVSaEiWCB0PEtSRc8mhc-RBUNnzU-vuUhOcFk2UQEfwNzOdiHGjw"
},
"type": "public-key"
}
verifyAndroidKeyAttestation(androidKeyWebAuthnSample)
@herrjemand
Copy link
Author

Don't forget to install dependencies:

npm i @lapo/asn1js base64url cbor jsrsasign

@kyriakopoulosd
Copy link

@herrjemand I get the following error :

verify-android-key.js:55
'type': asn1object.typeName()
TypeError: Cannot read property 'typeName' of undefined
at asn1ObjectToJSON

Am I missing something?

@maobon
Copy link

maobon commented May 17, 2020

androidkeystoreroot this is only for ECDSA... RSA is not that root cert.

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