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."); }
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); } ret.set("attCert", newX5c.shift()); ret.set("x5c", newX5c);
if (attStmt.ecdaaKeyId) ret.set("ecdaaKeyId", attStmt.ecdaaKeyId);
ret.set("sig", coerceToArrayBuffer(attStmt.sig, "tpm signature"));
ret.set("ver", attStmt.ver);
const alg = { algName: coseAlgToStr(attStmt.alg), hashAlg: coseAlgToHashStr(attStmt.alg), }; ret.set("alg", alg);
const certInfo = parseCertInfo(coerceToArrayBuffer(attStmt.certInfo, "certInfo")); ret.set("certInfo", certInfo);
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);
const magic = dv.getUint32(offset); if (magic !== 0xff544347) { throw new Error("tpm attestation: certInfo had bad magic number: " + magic.toString(16)); } ci.set("magic", magic); offset += 4;
const type = decodeStructureTag(dv.getUint16(offset)); 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;
ret = getTpm2bName(dv, offset); ci.set("qualifiedSignerHashType", ret.hashType); ci.set("qualifiedSigner", ret.nameHash); offset = ret.offset;
ret = getSizedElement(dv, offset); ci.set("extraData", ret.buf); offset = ret.offset;
ci.set("clock", dv.buffer.slice(offset, offset + 8)); offset += 8; ci.set("resetCount", dv.getUint32(offset)); offset += 4; ci.set("restartCount", dv.getUint32(offset)); offset += 4; ci.set("safe", !!dv.getUint8(offset)); offset++;
ci.set("firmwareVersion", dv.buffer.slice(offset, offset + 8)); offset += 8;
ret = getTpm2bName(dv, offset); ci.set("nameHashType", ret.hashType); ci.set("name", ret.nameHash); offset = ret.offset;
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);
const type = algIdToStr(dv.getUint16(offset)); pa.set("type", type); offset += 2;
pa.set("nameAlg", algIdToStr(dv.getUint16(offset))); offset += 2;
pa.set("objectAttributes", decodeObjectAttributes(dv.getUint32(offset))); offset += 4;
ret = getSizedElement(dv, offset); pa.set("authPolicy", ret.buf); offset = ret.offset;
if (type !== "TPM_ALG_RSA") { throw new Error("tpm attestation: only TPM_ALG_RSA supported"); } 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;
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;}
function decodeStructureTag(t) { 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", "TPM_ALG_RSA", null, null, "TPM_ALG_SHA1", "TPM_ALG_HMAC", "TPM_ALG_AES", "TPM_ALG_MGF1", null, "TPM_ALG_KEYEDHASH", "TPM_ALG_XOR", "TPM_ALG_SHA256", "TPM_ALG_SHA384", "TPM_ALG_SHA512", null, null, "TPM_ALG_NULL", null, "TPM_ALG_SM3_256", "TPM_ALG_SM4", "TPM_ALG_RSASSA", "TPM_ALG_RSAES", "TPM_ALG_RSAPSS", "TPM_ALG_OAEP", "TPM_ALG_ECDSA", ];
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");
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"); } const magic = certInfo.get("magic"); if (magic !== 0xff544347) { throw new Error("tpm attestation: certInfo had bad magic number: " + magic.toString(16)); }
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); }
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"); }
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");
const sig = this.authnrData.get("sig"); const rawCertInfo = certInfo.get("rawCertInfo"); const attCertPem = abToPem("CERTIFICATE", parsedAttCert);
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");
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") { this.audit.warning.set("attesation-not-validated", "could not validate attestation because the root attestation certification could not be found"); } else { throw err; } }
if (attCert.getVersion() !== 3) { throw new Error("expected TPM attestation certificate to be x.509v3"); }
const attCertSubject = attCert.getSubject(); if (attCertSubject.size !== 0) { throw new Error("tpm attestation: attestation certificate MUST have empty subject"); }
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"); }
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"); }
if (!directoryName.has("tcg-at-tpm-manufacturer")) { throw new Error("tpm attestation: subject alternative name did not list manufacturer"); }
if (!directoryName.has("tcg-at-tpm-model")) { throw new Error("tpm attestation: subject alternative name did not list model number"); }
if (!directoryName.has("tcg-at-tpm-version")) { throw new Error("tpm attestation: subject alternative name did not list firmware version"); }
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'"); }
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"); }
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");
this.audit.info.set("attestation-type", "AttCA");
this.audit.journal.add("fmt");
return true;
}
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 };