Skip to main content
Module

x/fido2/lib/attestations/tpm.js

A node.js library for performing FIDO 2.0 / WebAuthn server functionality
Go to Latest
File
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569
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 complexityfunction 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 };