UNPKG

fido2-lib

Version:

A library for performing FIDO 2.0 / WebAuthn functionality

298 lines (248 loc) 9.57 kB
import { arrayBufferEquals, abToPem, appendBuffer, coerceToArrayBuffer, coerceToBase64, tools } from "../utils.js"; import { Certificate, CertManager } from "../certUtils.js"; import { u2fRootCerts as rootCertList } from "./u2fRootCerts.js"; const algMap = new Map([ [-7, { algName: "ECDSA_w_SHA256", hashAlg: "SHA-256", }], // [-8, { // name: "EdDSA", // hash: undefined // }], [-35, { algName: "ECDSA_w_SHA384", hashAlg: "SHA-384", }], [-36, { algName: "ECDSA_w_SHA512", hashAlg: "SHA-512", }], [-257, { algName: "RSASSA-PKCS1-v1_5_w_SHA256", hashAlg: "SHA-256", }], ]); function packedParseFn(attStmt) { const ret = new Map(); // alg const algEntry = algMap.get(attStmt.alg); if (algEntry === undefined) { throw new Error("packed attestation: unknown algorithm: " + attStmt.alg); } ret.set("alg", algEntry); // x5c const x5c = attStmt.x5c; const newX5c = []; if (Array.isArray(x5c)) { for (let cert of x5c) { cert = coerceToArrayBuffer(cert, "packed x5c cert"); newX5c.push(cert); } ret.set("attCert", newX5c.shift()); ret.set("x5c", newX5c); } else { ret.set("x5c", x5c); } // ecdaaKeyId let ecdaaKeyId = attStmt.ecdaaKeyId; if (ecdaaKeyId !== undefined) { ecdaaKeyId = coerceToArrayBuffer(ecdaaKeyId, "ecdaaKeyId"); ret.set("ecdaaKeyId", ecdaaKeyId); } // sig let sig = attStmt.sig; sig = coerceToArrayBuffer(sig, "packed signature"); ret.set("sig", sig); return ret; } async function packedValidateFn() { const x5c = this.authnrData.get("x5c"); const ecdaaKeyId = this.authnrData.get("ecdaaKeyId"); if (x5c !== undefined && ecdaaKeyId !== undefined) { throw new Error("packed attestation: should be 'basic' or 'ecdaa', got both"); } if (x5c) return await packedValidateBasic.call(this); if (ecdaaKeyId) return await packedValidateEcdaa.call(this); return await packedValidateSurrogate.call(this); } async function packedValidateBasic() { // see what algorithm we're working with const { algName, hashAlg, } = this.authnrData.get("alg"); if (algName === undefined) { throw new Error("packed attestation: unknown algorithm " + algName); } // from: https://w3c.github.io/webauthn/#packed-attestation // Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash using the attestation public key in x5c with the algorithm specified in alg. const res = await validateSignature( this.clientData.get("rawClientDataJson"), this.authnrData.get("rawAuthnrData"), this.authnrData.get("sig"), hashAlg, this.authnrData.get("attCert"), ); if (!res) { throw new Error("packed attestation signature verification failed"); } this.audit.journal.add("sig"); this.audit.journal.add("alg"); // Verify that x5c meets the requirements in §8.2.1 Packed attestation statement certificate requirements. await validateCerts( this.authnrData.get("attCert"), this.authnrData.get("aaguid"), this.authnrData.get("x5c"), this.audit ); // If successful, return attestation type Basic and attestation trust path x5c. this.audit.info.set("attestation-type", "basic"); this.audit.journal.add("fmt"); return true; } async function validateSignature( rawClientData, authenticatorData, sig, hashAlg, parsedAttCert, ) { // create clientDataHash const hash = await tools.hashDigest(rawClientData); const clientDataHash = new Uint8Array(hash).buffer; // convert cert to PEM const attCertPem = abToPem("CERTIFICATE", parsedAttCert); // Get public key from cert const cert = new Certificate(attCertPem); const publicKey = await cert.getPublicKey(); // verify signature const verify = await tools.verifySignature( publicKey, sig, appendBuffer(authenticatorData, clientDataHash), hashAlg, ); return verify; } async function validateCerts(parsedAttCert, aaguid, _x5c, audit) { // ToDo: Do something with x5c! Prefixed with _ to avoid linting errors for now // make sure our root certs are loaded if (CertManager.getCerts().size === 0) { rootCertList.forEach((cert) => CertManager.addCert(cert)); } // decode attestation cert const attCert = new Certificate(coerceToBase64(parsedAttCert, "parsedAttCert")); try { await attCert.verify(); } catch (e) { const err = e; if (err.message === "Please provide issuer certificate as a parameter") { // err = new Error("Root attestation certificate for this token could not be found. Please contact your security key vendor."); audit.warning.set("attesation-not-validated", "could not validate attestation because the root attestation certification could not be found"); } else { throw err; } } // TODO: validate chain? audit.journal.add("x5c"); // cert MUST be x.509v3 if (attCert.getVersion() !== 3) { throw new Error("expected packed attestation certificate to be x.509v3"); } // save certificate warnings, info, and extensions in our audit information const exts = attCert.getExtensions(); exts.forEach((v, k) => audit.info.set(k, v)); attCert.info.forEach((v, k) => audit.info.set(k, v)); attCert.warning.forEach((v, k) => audit.warning.set(k, v)); audit.journal.add("attCert"); // console.log("_cert", attCert._cert); // console.log("_cert.subject", attCert._cert.subject); // from: https://w3c.github.io/webauthn/#packed-attestation // Version MUST be set to 3 (which is indicated by an ASN.1 INTEGER with value 2). if (attCert.getVersion() !== 3) { throw new Error("expected packed attestation certificate to be x.509v3"); } // Subject field MUST be set to: // Subject-C ISO 3166 code specifying the country where the Authenticator vendor is incorporated (PrintableString) // Subject-O Legal name of the Authenticator vendor (UTF8String) // Subject-OU Literal string “Authenticator Attestation” (UTF8String) // Subject-CN A UTF8String of the vendor’s choosing const subject = attCert.getSubject(); if (typeof subject.get("country-name") !== "string") { throw new Error("packed attestation: attestation certificate missing 'country name'"); } if (typeof subject.get("organization-name") !== "string") { throw new Error("packed attestation: attestation certificate missing 'organization name'"); } if (subject.get("organizational-unit-name") !== "Authenticator Attestation") { throw new Error("packed attestation: attestation certificate 'organizational unit name' must be 'Authenticator Attestation'"); } if (typeof subject.get("common-name") !== "string") { throw new Error("packed attestation: attestation certificate missing 'common name'"); } // If the related attestation root certificate is used for multiple authenticator models, the Extension OID 1.3.6.1.4.1.45724.1.1.4 (id-fido-gen-ce-aaguid) MUST be present, containing the AAGUID as a 16-byte OCTET STRING. The extension MUST NOT be marked as critical. // XXX: no way to tell if AAGUID is required on the server side... // The Basic Constraints extension MUST have the CA component set to false. const basicConstraints = exts.get("basic-constraints"); if (basicConstraints.cA !== false) { throw new Error("packed attestation: basic constraints 'cA' must be 'false'"); } // An Authority Information Access (AIA) extension with entry id-ad-ocsp and a CRL Distribution Point extension [RFC5280] are both OPTIONAL as the status of many attestation certificates is available through authenticator metadata services // TODO: no example of this is available to test against // If x5c contains an extension with OID 1.3.6.1.4.1.45724.1.1.4 (id-fido-gen-ce-aaguid) verify that the value of this extension matches the aaguid in authenticatorData. const certAaguid = exts.get("fido-aaguid"); if (certAaguid !== undefined && !arrayBufferEquals(aaguid, certAaguid)) { throw new Error("packed attestation: authnrData AAGUID did not match AAGUID in attestation certificate"); } } async function validateSelfSignature(rawClientData, authenticatorData, sig, hashAlg, publicKeyPem) { // create clientDataHash const clientDataHash = await tools.hashDigest(rawClientData, hashAlg); // verify signature const verify = await tools.verifySignature( publicKeyPem, sig, appendBuffer(authenticatorData, clientDataHash), hashAlg, ); return verify; } async function packedValidateSurrogate() { // see what algorithm we're working with const { algName, hashAlg, } = this.authnrData.get("alg"); if (algName === undefined) { throw new Error("packed attestation: unknown algorithm " + algName); } // from: https://w3c.github.io/webauthn/#packed-attestation // Verify that sig is a valid signature over the concatenation of authenticatorData and clientDataHash using the credential public key with alg. const res = await validateSelfSignature( this.clientData.get("rawClientDataJson"), this.authnrData.get("rawAuthnrData"), this.authnrData.get("sig"), hashAlg, this.authnrData.get("credentialPublicKeyPem"), ); if (!res || typeof res !== "boolean") { throw new Error("packed attestation signature verification failed"); } this.audit.journal.add("sig"); this.audit.journal.add("alg"); this.audit.journal.add("x5c"); // If successful, return attestation type Self and an empty trust path this.audit.info.set("attestation-type", "self"); this.audit.journal.add("fmt"); return true; } function packedValidateEcdaa() { throw new Error("packed attestation: ECDAA not implemented, please open a GitHub issue."); } const packedAttestation = { name: "packed", parseFn: packedParseFn, validateFn: packedValidateFn, }; export { packedAttestation };