UNPKG

fido2-lib

Version:

A library for performing FIDO 2.0 / WebAuthn functionality

569 lines (483 loc) 18.9 kB
import { arrayBufferEquals, abToBuf, abToInt, abToPem, appendBuffer, coerceToArrayBuffer, coerceToBase64, tools } from "../utils.js"; import { Certificate } from "../certUtils.js"; import { coseAlgToHashStr, coseAlgToStr } from "../keyUtils.js"; function tpmParseFn(attStmt) { const ret = new Map(); if (attStmt.ecdaaKeyId !== undefined) { throw new Error("TPM ECDAA attesation is not currently supported."); } // x5c const x5c = attStmt.x5c; if (!Array.isArray(x5c)) { throw new TypeError("expected TPM attestation x5c field to be of type Array"); } if (x5c.length < 1) { throw new TypeError("no certificates in TPM x5c field"); } const newX5c = []; for (let cert of x5c) { cert = coerceToArrayBuffer(cert, "TPM x5c cert"); newX5c.push(cert); } // first certificate MUST be the attestation cert ret.set("attCert", newX5c.shift()); // the rest of the certificates (if any) are the certificate chain ret.set("x5c", newX5c); // ecdaa if (attStmt.ecdaaKeyId) ret.set("ecdaaKeyId", attStmt.ecdaaKeyId); // sig ret.set("sig", coerceToArrayBuffer(attStmt.sig, "tpm signature")); // sig ret.set("ver", attStmt.ver); // alg const alg = { algName: coseAlgToStr(attStmt.alg), hashAlg: coseAlgToHashStr(attStmt.alg), }; ret.set("alg", alg); // certInfo const certInfo = parseCertInfo(coerceToArrayBuffer(attStmt.certInfo, "certInfo")); ret.set("certInfo", certInfo); // pubArea const pubArea = parsePubArea(coerceToArrayBuffer(attStmt.pubArea, "pubArea")); ret.set("pubArea", pubArea); return ret; } function parseCertInfo(certInfo) { if (!(certInfo instanceof ArrayBuffer)) { throw new Error("tpm attestation: expected certInfo to be ArrayBuffer"); } const dv = new DataView(certInfo); let offset = 0; let ret; const ci = new Map(); ci.set("rawCertInfo", certInfo); // TPM_GENERATED_VALUE magic number const magic = dv.getUint32(offset); // if this isn't the magic number, the rest of the parsing is going to fail if (magic !== 0xff544347) { // 0xFF + 'TCG' throw new Error("tpm attestation: certInfo had bad magic number: " + magic.toString(16)); } ci.set("magic", magic); offset += 4; // TPMI_ST_ATTEST type const type = decodeStructureTag(dv.getUint16(offset)); // if this isn't the right type, the rest of the parsing is going to fail if (type !== "TPM_ST_ATTEST_CERTIFY") { throw new Error("tpm attestation: got wrong type. expected 'TPM_ST_ATTEST_CERTIFY' got: " + type); } ci.set("type", type); offset += 2; // TPM2B_NAME qualifiedSigner ret = getTpm2bName(dv, offset); ci.set("qualifiedSignerHashType", ret.hashType); ci.set("qualifiedSigner", ret.nameHash); offset = ret.offset; // TPM2B_DATA extraData ret = getSizedElement(dv, offset); ci.set("extraData", ret.buf); offset = ret.offset; // TPMS_CLOCK_INFO clockInfo // UINT64 clock ci.set("clock", dv.buffer.slice(offset, offset + 8)); offset += 8; // UINT32 resetCount ci.set("resetCount", dv.getUint32(offset)); offset += 4; // UINT32 restartCount ci.set("restartCount", dv.getUint32(offset)); offset += 4; // boolean safe ci.set("safe", !!dv.getUint8(offset)); offset++; // UINT64 firmwareVersion ci.set("firmwareVersion", dv.buffer.slice(offset, offset + 8)); offset += 8; // TPMU_ATTEST attested // TPM2B_NAME name ret = getTpm2bName(dv, offset); ci.set("nameHashType", ret.hashType); ci.set("name", ret.nameHash); offset = ret.offset; // TPM2B_NAME qualifiedName ret = getTpm2bName(dv, offset); ci.set("qualifiedNameHashType", ret.hashType); ci.set("qualifiedName", ret.nameHash); offset = ret.offset; if (offset !== certInfo.byteLength) { throw new Error("tpm attestation: left over bytes when parsing cert info"); } return ci; } function parsePubArea(pubArea) { if (!(pubArea instanceof ArrayBuffer)) { throw new Error("tpm attestation: expected pubArea to be ArrayBuffer"); } const dv = new DataView(pubArea); let offset = 0; let ret; const pa = new Map(); pa.set("rawPubArea", pubArea); // TPMI_ALG_PUBLIC type const type = algIdToStr(dv.getUint16(offset)); pa.set("type", type); offset += 2; // TPMI_ALG_HASH nameAlg pa.set("nameAlg", algIdToStr(dv.getUint16(offset))); offset += 2; // TPMA_OBJECT objectAttributes pa.set("objectAttributes", decodeObjectAttributes(dv.getUint32(offset))); offset += 4; // TPM2B_DIGEST authPolicy ret = getSizedElement(dv, offset); pa.set("authPolicy", ret.buf); offset = ret.offset; // TPMU_PUBLIC_PARMS parameters if (type !== "TPM_ALG_RSA") { throw new Error("tpm attestation: only TPM_ALG_RSA supported"); } // TODO: support other types pa.set("symmetric", algIdToStr(dv.getUint16(offset))); offset += 2; pa.set("scheme", algIdToStr(dv.getUint16(offset))); offset += 2; pa.set("keyBits", dv.getUint16(offset)); offset += 2; let exponent = dv.getUint32(offset); if (exponent === 0) exponent = 65537; pa.set("exponent", exponent); offset += 4; // TPMU_PUBLIC_ID unique ret = getSizedElement(dv, offset); pa.set("unique", ret.buf); offset = ret.offset; if (offset !== pubArea.byteLength) { throw new Error("tpm attestation: left over bytes when parsing public area"); } return pa; } // eslint-disable complexity function decodeStructureTag(t) { /* eslint complexity: ["off"] */ switch (t) { case 0x00C4: return "TPM_ST_RSP_COMMAND"; case 0x8000: return "TPM_ST_NULL"; case 0x8001: return "TPM_ST_NO_SESSIONS"; case 0x8002: return "TPM_ST_SESSIONS"; case 0x8003: return "TPM_RESERVED_0x8003"; case 0x8004: return "TPM_RESERVED_0x8004"; case 0x8014: return "TPM_ST_ATTEST_NV"; case 0x8015: return "TPM_ST_ATTEST_COMMAND_AUDIT"; case 0x8016: return "TPM_ST_ATTEST_SESSION_AUDIT"; case 0x8017: return "TPM_ST_ATTEST_CERTIFY"; case 0x8018: return "TPM_ST_ATTEST_QUOTE"; case 0x8019: return "TPM_ST_ATTEST_TIME"; case 0x801A: return "TPM_ST_ATTEST_CREATION"; case 0x801B: return "TPM_RESERVED_0x801B"; case 0x8021: return "TPM_ST_CREATION"; case 0x8022: return "TPM_ST_VERIFIED"; case 0x8023: return "TPM_ST_AUTH_SECRET"; case 0x8024: return "TPM_ST_HASHCHECK"; case 0x8025: return "TPM_ST_AUTH_SIGNED"; case 0x8029: return "TPM_ST_FU_MANIFEST"; default: throw new Error("tpm attestation: unknown structure tag: " + t.toString(16)); } } function decodeObjectAttributes(oa) { const attrList = [ "RESERVED_0", "FIXED_TPM", "ST_CLEAR", "RESERVED_3", "FIXED_PARENT", "SENSITIVE_DATA_ORIGIN", "USER_WITH_AUTH", "ADMIN_WITH_POLICY", "RESERVED_8", "RESERVED_9", "NO_DA", "ENCRYPTED_DUPLICATION", "RESERVED_12", "RESERVED_13", "RESERVED_14", "RESERVED_15", "RESTRICTED", "DECRYPT", "SIGN_ENCRYPT", "RESERVED_19", "RESERVED_20", "RESERVED_21", "RESERVED_22", "RESERVED_23", "RESERVED_24", "RESERVED_25", "RESERVED_26", "RESERVED_27", "RESERVED_28", "RESERVED_29", "RESERVED_30", "RESERVED_31", ]; const ret = new Set(); for (let i = 0; i < 32; i++) { const bit = 1 << i; if (oa & bit) { ret.add(attrList[i]); } } return ret; } function getSizedElement(dv, offset) { const size = dv.getUint16(offset); offset += 2; const buf = dv.buffer.slice(offset, offset + size); dv = new DataView(buf); offset += size; return { size, dv, buf, offset, }; } function getTpm2bName(dvIn, oIn) { const { offset, dv, } = getSizedElement(dvIn, oIn); const hashType = algIdToStr(dv.getUint16(0)); const nameHash = dv.buffer.slice(2); return { hashType, nameHash, offset, }; } function algIdToStr(hashType) { const hashList = [ "TPM_ALG_ERROR", // 0 "TPM_ALG_RSA", // 1 null, null, "TPM_ALG_SHA1", // 4 "TPM_ALG_HMAC", // 5 "TPM_ALG_AES", // 6 "TPM_ALG_MGF1", // 7 null, "TPM_ALG_KEYEDHASH", // 8 "TPM_ALG_XOR", // A "TPM_ALG_SHA256", // B "TPM_ALG_SHA384", // C "TPM_ALG_SHA512", // D null, null, "TPM_ALG_NULL", // 10 null, "TPM_ALG_SM3_256", // 12 "TPM_ALG_SM4", // 13 "TPM_ALG_RSASSA", // 14 "TPM_ALG_RSAES", // 15 "TPM_ALG_RSAPSS", // 16 "TPM_ALG_OAEP", // 17 "TPM_ALG_ECDSA", // 18 ]; return hashList[hashType]; } async function tpmValidateFn() { const parsedAttCert = this.authnrData.get("attCert"); const certInfo = this.authnrData.get("certInfo"); const pubArea = this.authnrData.get("pubArea"); const ver = this.authnrData.get("ver"); if (ver != "2.0") { throw new Error("tpm attestation: expected TPM version 2.0"); } this.audit.journal.add("ver"); // https://www.w3.org/TR/webauthn/#tpm-attestation // Verify that the public key specified by the parameters and unique fields of pubArea is identical to the credentialPublicKey in the attestedCredentialData in authenticatorData. const pubAreaPkN = pubArea.get("unique"); const pubAreaPkExp = pubArea.get("exponent"); const credentialPublicKeyJwk = this.authnrData.get("credentialPublicKeyJwk"); const credentialPublicKeyJwkN = coerceToArrayBuffer(credentialPublicKeyJwk.n,"credentialPublicKeyJwk.n"); const credentialPublicKeyJwkExpBuf = coerceToArrayBuffer(credentialPublicKeyJwk.e,"credentialPublicKeyJwk.e"); const credentialPublicKeyJwkExp = abToInt(credentialPublicKeyJwkExpBuf); if (credentialPublicKeyJwk.kty !== "RSA" || pubArea.get("type") !== "TPM_ALG_RSA") { throw new Error("tpm attestation: only RSA keys are currently supported"); } if (pubAreaPkExp !== credentialPublicKeyJwkExp) { throw new Error("tpm attestation: RSA exponents of WebAuthn credentialPublicKey and TPM publicArea did not match"); } if (!arrayBufferEquals(credentialPublicKeyJwkN, pubAreaPkN)) { throw new Error("tpm attestation: RSA 'n' of WebAuthn credentialPublicKey and TPM publicArea did not match"); } // Validate that certInfo is valid: // Verify that magic is set to TPM_GENERATED_VALUE. const magic = certInfo.get("magic"); if (magic !== 0xff544347) { // 0xFF + 'TCG' throw new Error("tpm attestation: certInfo had bad magic number: " + magic.toString(16)); } // Verify that type is set to TPM_ST_ATTEST_CERTIFY. const type = certInfo.get("type"); if (type !== "TPM_ST_ATTEST_CERTIFY") { throw new Error("tpm attestation: got wrong type. expected 'TPM_ST_ATTEST_CERTIFY' got: " + type); } // Verify that extraData is set to the hash of attToBeSigned using the hash algorithm employed in "alg". const rawAuthnrData = this.authnrData.get("rawAuthnrData"); const rawClientData = this.clientData.get("rawClientDataJson"); const clientDataHashBuf = await tools.hashDigest(abToBuf(rawClientData)); const alg = this.authnrData.get("alg"); if (alg.hashAlg === undefined) { throw new Error("tpm attestation: unknown algorithm: " + alg); } this.audit.journal.add("alg"); const extraDataHashBuf = await tools.hashDigest( appendBuffer(abToBuf(rawAuthnrData), clientDataHashBuf), alg.hashAlg, ); const generatedExtraDataHash = new Uint8Array(extraDataHashBuf).buffer; const extraData = certInfo.get("extraData"); if (!arrayBufferEquals(generatedExtraDataHash, extraData)) { throw new Error("extraData hash did not match authnrData + clientDataHash hashed"); } // Verify that attested contains a TPMS_CERTIFY_INFO structure as specified in [TPMv2-Part2] section 10.12.3, // [see parser] // whose name field contains a valid Name for pubArea, as computed using the algorithm in the nameAlg field of pubArea using the procedure specified in [TPMv2-Part1] section 16. const pubAreaName = certInfo.get("name"); const pubAreaNameHashAlg = tpmHashToNpmHash(certInfo.get("nameHashType")); const pubAreaNameHashBuf = await tools.hashDigest( abToBuf(pubArea.get("rawPubArea")), pubAreaNameHashAlg, ); const generatedPubAreaNameHash = new Uint8Array(pubAreaNameHashBuf).buffer; if (!arrayBufferEquals(generatedPubAreaNameHash, pubAreaName)) { throw new Error("pubAreaName hash did not match hash of publicArea"); } this.audit.journal.add("pubArea"); // Note that the remaining fields in the "Standard Attestation Structure" [TPMv2-Part1] section 31.2, i.e., qualifiedSigner, clockInfo and firmwareVersion are ignored. // These fields MAY be used as an input to risk engines. // If x5c is present, this indicates that the attestation type is not ECDAA. In this case: // Verify the sig is a valid signature over certInfo using the attestation public key in x5c with the algorithm specified in alg. const sig = this.authnrData.get("sig"); const rawCertInfo = certInfo.get("rawCertInfo"); const attCertPem = abToPem("CERTIFICATE", parsedAttCert); // Get public key from cert const cert = new Certificate(attCertPem); const publicKey = await cert.getPublicKey(); const res = await tools.verifySignature( publicKey, sig, abToBuf(rawCertInfo), alg.hashAlg, ); if (!res) { throw new Error("TPM attestation signature verification failed"); } this.audit.journal.add("sig"); this.audit.journal.add("certInfo"); // Verify that x5c meets the requirements in §8.3.1 TPM attestation statement certificate requirements. // https://www.w3.org/TR/webauthn/#tpm-cert-requirements // 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."); this.audit.warning.set("attesation-not-validated", "could not validate attestation because the root attestation certification could not be found"); } else { throw err; } } // Version MUST be set to 3. if (attCert.getVersion() !== 3) { throw new Error("expected TPM attestation certificate to be x.509v3"); } // Subject field MUST be set to empty. const attCertSubject = attCert.getSubject(); if (attCertSubject.size !== 0) { throw new Error("tpm attestation: attestation certificate MUST have empty subject"); } // The Subject Alternative Name extension MUST be set as defined in [TPMv2-EK-Profile] section 3.2.9. // [save certificate warnings, info, and extensions in our audit information] const attCertExt = attCert.getExtensions(); attCertExt.forEach((v, k) => this.audit.info.set(k, v)); attCert.info.forEach((v, k) => this.audit.info.set(k, v)); attCert.warning.forEach((v, k) => this.audit.warning.set(k, v)); const altName = attCertExt.get("subject-alt-name"); if (altName === undefined || !Array.isArray(altName) || altName.length < 1) { throw new Error("tpm attestation: Subject Alternative Name extension MUST be set as defined in [TPMv2-EK-Profile] section 3.2.9"); } // TCG EK Credential Profile For TPM Family 2.0; Level 0 Specification Version 2.0 Revision 14 4 November 2014 // The issuer MUST include TPM manufacturer, TPM part number and TPM firmware version, using the directoryNameform within the GeneralName structure. let directoryName; altName.forEach((name) => { if (name.directoryName !== undefined) { directoryName = name.directoryName; } }); if (directoryName === undefined) { throw new Error("tpm attestation: subject alternative name did not contain directory name"); } // The TPM manufacturer identifies the manufacturer of the TPM. This value MUST be the vendor ID defined in the TCG Vendor ID Registry if (!directoryName.has("tcg-at-tpm-manufacturer")) { throw new Error("tpm attestation: subject alternative name did not list manufacturer"); } // TODO: lookup manufacturer in registry // The TPM part number is encoded as a string and is manufacturer-specific. A manufacturer MUST provide a way to the user to retrieve the part number physically or logically. This information could be e.g. provided as part of the vendor string in the command TPM2_GetCapability(property = TPM_PT_VENDOR_STRING_x; x=1…4). if (!directoryName.has("tcg-at-tpm-model")) { throw new Error("tpm attestation: subject alternative name did not list model number"); } // The TPM firmware version is a manufacturer-specific implementation version of the TPM. This value SHOULD match the version reported by the command TPM2_GetCapability (property = TPM_PT_FIRMWARE_VERSION_1). if (!directoryName.has("tcg-at-tpm-version")) { throw new Error("tpm attestation: subject alternative name did not list firmware version"); } // The Extended Key Usage extension MUST contain the "joint-iso-itu-t(2) internationalorganizations(23) 133 tcg-kp(8) tcg-kp-AIKCertificate(3)" OID. const extKeyUsage = attCertExt.get("ext-key-usage"); if (!Array.isArray(extKeyUsage) || !extKeyUsage.includes("tcg-kp-aik-certificate")) { throw new Error("tpm attestation: the Extended Key Usage extension MUST contain 'tcg-kp-aik-certificate'"); } // The Basic Constraints extension MUST have the CA component set to false. const basicConstraints = attCertExt.get("basic-constraints"); if (typeof basicConstraints !== "object" || basicConstraints.cA !== false) { throw new Error("tpm attestation: the Basic Constraints extension MUST have the CA component set to 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 metadata services. See, for example, the FIDO Metadata Service [FIDOMetadataService]. // [will use MDS] // 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 = attCertExt.get("fido-aaguid"); const aaguid = this.authnrData.get("aaguid"); if (certAaguid !== undefined && !arrayBufferEquals(aaguid, certAaguid)) { throw new Error("tpm attestation: authnrData AAGUID did not match AAGUID in attestation certificate"); } this.audit.journal.add("x5c"); this.audit.journal.add("attCert"); // If successful, return attestation type AttCA and attestation trust path x5c. this.audit.info.set("attestation-type", "AttCA"); this.audit.journal.add("fmt"); return true; // If ecdaaKeyId is present, then the attestation type is ECDAA. // Perform ECDAA-Verify on sig to verify that it is a valid signature over certInfo (see [FIDOEcdaaAlgorithm]). // If successful, return attestation type ECDAA and the identifier of the ECDAA-Issuer public key ecdaaKeyId. // [not currently supported, error would have been thrown in parser] } function tpmHashToNpmHash(tpmHash) { switch (tpmHash) { case "TPM_ALG_SHA1": return "SHA-1"; case "TPM_ALG_SHA256": return "SHA-256"; case "TPM_ALG_SHA384": return "SHA-384"; case "TPM_ALG_SHA512": return "SHA-512"; default: throw new TypeError("Unsupported hash type: " + tpmHash); } } const tpmAttestation = { name: "tpm", parseFn: tpmParseFn, validateFn: tpmValidateFn, }; export { tpmAttestation };