import { arrayBufferEquals, abToHex, b64ToJsObject, coerceToArrayBuffer, coerceToBase64Url, jsObjectToB64, tools } from "./utils.js";
import { CertManager } from "./certUtils.js";
const fidoMdsRootCert = "-----BEGIN CERTIFICATE-----\n" + "MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G\n" + "A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp\n" + "Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4\n" + "MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG\n" + "A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI\n" + "hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8\n" + "RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT\n" + "gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm\n" + "KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd\n" + "QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ\n" + "XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw\n" + "DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o\n" + "LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU\n" + "RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp\n" + "jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK\n" + "6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX\n" + "mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs\n" + "Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH\n" + "WD9f\n" + "-----END CERTIFICATE-----\n";
class MdsEntry { constructor(mdsEntry, tocEntry) { for (const key of Object.keys(tocEntry)) { this[key] = tocEntry[key]; }
for (const key of Object.keys(mdsEntry)) { this[key] = mdsEntry[key]; }
if (this.metadataStatement) { delete this.metadataStatement; }
this.attachmentHint = this.attachmentHint instanceof Array ? this.attachmentHint : attachmentHintToArr(this.attachmentHint); function attachmentHintToArr(hint) { const ret = []; if (hint & 0x0001) ret.push("internal"); if (hint & 0x0002) ret.push("external"); if (hint & 0x0004) ret.push("wired"); if (hint & 0x0008) ret.push("wireless"); if (hint & 0x0010) ret.push("nfc"); if (hint & 0x0020) ret.push("bluetooth"); if (hint & 0x0040) ret.push("network"); if (hint & 0x0080) ret.push("ready"); if (hint & 0xFF00) throw new Error("unknown attachment hint flags: " + hint & 0xFF00); return ret; }
if (!Array.isArray(this.attestationTypes)) throw new Error("expected attestationTypes to be Array, got: " + this.attestationTypes); this.attestationTypes = this.attestationTypes.map((att) => typeof(att) === "string" ? att : attestationTypeToStr(att)); function attestationTypeToStr(att) { switch (att) { case 0x3E07: return "basic-full"; case 0x3E08: return "basic-surrogate"; case 0x3E09: return "ecdaa"; default: throw new Error("uknown attestation type: " + att); } }
if (this.authenticationAlgorithms) { this.authenticationAlgorithm = this.authenticationAlgorithms[0]; }
this.authenticationAlgorithm = typeof(this.authenticationAlgorithm) === "string" ? this.authenticationAlgorithm : algToStr(this.authenticationAlgorithm); function algToStr(alg) { switch (alg) { case 0x0001: return "ALG_SIGN_SECP256R1_ECDSA_SHA256_RAW"; case 0x0002: return "ALG_SIGN_SECP256R1_ECDSA_SHA256_DER"; case 0x0003: return "ALG_SIGN_RSASSA_PSS_SHA256_RAW"; case 0x0004: return "ALG_SIGN_RSASSA_PSS_SHA256_DER"; case 0x0005: return "ALG_SIGN_SECP256K1_ECDSA_SHA256_RAW"; case 0x0006: return "ALG_SIGN_SECP256K1_ECDSA_SHA256_DER"; case 0x0007: return "ALG_SIGN_SM2_SM3_RAW"; case 0x0008: return "ALG_SIGN_RSA_EMSA_PKCS1_SHA256_RAW"; case 0x0009: return "ALG_SIGN_RSA_EMSA_PKCS1_SHA256_DER"; default: throw new Error("unknown authentication algorithm: " + alg); } }
if (this.attestationRootCertificates) { for (const certificate of this.attestationRootCertificates) { CertManager.addCert(certificate); } }
this.keyProtection = this.keyProtection instanceof Array ? this.keyProtection : keyProtToArr(this.keyProtection); function keyProtToArr(kp) { const ret = []; if (kp & 0x0001) ret.push("software"); if (kp & 0x0002) ret.push("hardware"); if (kp & 0x0004) ret.push("tee"); if (kp & 0x0008) ret.push("secure-element"); if (kp & 0x0010) ret.push("remote-handle"); if (kp & 0xFFE0) throw new Error("unknown key protection flags: " + kp & 0xFFE0); return ret; }
this.matcherProtection = this.matcherProtection instanceof Array ? this.matcherProtection : matcherProtToArr(this.matcherProtection); function matcherProtToArr(mp) { const ret = []; if (mp & 0x0001) ret.push("software"); if (mp & 0x0002) ret.push("hardware"); if (mp & 0x0004) ret.push("tee"); if (mp & 0xFFF8) throw new Error("unknown key protection flags: " + mp & 0xFFF8); return ret; }
if (this.publicKeyAlgAndEncodings) this.publicKeyAlgAndEncoding = `ALG_KEY_${this.publicKeyAlgAndEncodings[0].toUpperCase()}`;
this.publicKeyAlgAndEncoding = typeof(this.publicKeyAlgAndEncoding) === "string" ? this.publicKeyAlgAndEncoding : pkAlgAndEncodingToStr(this.publicKeyAlgAndEncoding); function pkAlgAndEncodingToStr(pkalg) { switch (pkalg) { case 0x0100: return "ALG_KEY_ECC_X962_RAW"; case 0x0101: return "ALG_KEY_ECC_X962_DER"; case 0x0102: return "ALG_KEY_RSA_2048_RAW"; case 0x0103: return "ALG_KEY_RSA_2048_DER"; case 0x0104: return "ALG_KEY_COSE"; default: throw new Error("unknown public key algorithm and encoding: " + pkalg); } }
this.tcDisplay = this.tcDisplay instanceof Array ? this.tcDisplay : tcDisplayToArr(this.tcDisplay); function tcDisplayToArr(tcd) { const ret = []; if (tcd & 0x0001) ret.push("any"); if (tcd & 0x0002) ret.push("priviledged-software"); if (tcd & 0x0004) ret.push("tee"); if (tcd & 0x0008) ret.push("hardware"); if (tcd & 0x0010) ret.push("remote"); if (tcd & 0xFFE0) throw new Error("unknown transaction confirmation display flags: " + tcd & 0xFFE0);
return ret; }
this.userVerificationDetails = uvDetailsToSet(this.userVerificationDetails);
function uvDetailsToSet(uvList) { const ret = []; if (!Array.isArray(uvList)) throw new Error("expected userVerificationDetails to be an Array, got: " + uvList); uvList.forEach((uv) => { if (!Array.isArray(uv)) throw new Error("expected userVerification to be Array, got " + uv); const d = uv.map((desc) => { const newDesc = {}; let descKey;
if ("caDesc" in desc) { newDesc.type = "code"; descKey = "caDesc"; }
if ("baDesc" in desc) { newDesc.type = "biometric"; descKey = "baDesc"; }
if ("paDesc" in desc) { newDesc.type = "pattern"; descKey = "paDesc"; }
newDesc.userVerification = uvToArr(desc.userVerification);
if (desc.userVerificationMethod) newDesc.userVerification = (desc.userVerificationMethod.match(/(\w+)_internal/) || [ "none", "none" ])[1];
if (descKey) for (const key of Object.keys(desc[descKey])) { newDesc[key] = desc[descKey][key]; }
return newDesc; }); ret.push(d); }); return ret; }
function uvToArr(uv) { const ret = []; if (uv & 0x00000001) ret.push("presence"); if (uv & 0x00000002) ret.push("fingerprint"); if (uv & 0x00000004) ret.push("passcode"); if (uv & 0x00000008) ret.push("voiceprint"); if (uv & 0x00000010) ret.push("faceprint"); if (uv & 0x00000020) ret.push("location"); if (uv & 0x00000040) ret.push("eyeprint"); if (uv & 0x00000080) ret.push("pattern"); if (uv & 0x00000100) ret.push("handprint"); if (uv & 0x00000200) ret.push("none"); if (uv & 0x00000400) ret.push("all"); return ret; } if (this.protocolFamily === undefined) this.protocolFamily = "uaf";
realBoolean(this, "isSecondFactorOnly"); realBoolean(this, "isKeyRestricted"); realBoolean(this, "isFreshUserVerificationRequired"); }}
class MdsCollection { constructor(collectionName) { if (typeof collectionName !== "string" || collectionName.length < 1) { throw new Error("expected 'collectionName' to be non-empty string, got: " + collectionName); }
this.toc = null; this.unvalidatedEntryList = new Map(); this.entryList = new Map(); this.validated = false; this.name = collectionName; }
async addToc(tocStr, rootCert, crls) { if (typeof tocStr !== "string" || tocStr.length < 1) { throw new Error("expected MDS TOC to be non-empty string"); }
let parsedJws; try { const protectedHeader = await tools.decodeProtectedHeader(tocStr); const publicKey = await tools.getEmbeddedJwk(protectedHeader); parsedJws = await tools.jwtVerify( tocStr, await tools.importJWK(publicKey), );
parsedJws.header = protectedHeader; parsedJws.key = publicKey;
this.toc = parsedJws.payload; } catch (e) { e.message = "could not parse and validate MDS TOC: " + e.message; throw e; }
if (rootCert === undefined) { if (parsedJws.kid === "Metadata TOC Signer 3" || parsedJws.key && parsedJws.key.kid === "Metadata TOC Signer 3") { rootCert = "-----BEGIN CERTIFICATE-----\n" + "MIICQzCCAcigAwIBAgIORqmxkzowRM99NQZJurcwCgYIKoZIzj0EAwMwUzELMAkG\n" + "A1UEBhMCVVMxFjAUBgNVBAoTDUZJRE8gQWxsaWFuY2UxHTAbBgNVBAsTFE1ldGFk\n" + "YXRhIFRPQyBTaWduaW5nMQ0wCwYDVQQDEwRSb290MB4XDTE1MDYxNzAwMDAwMFoX\n" + "DTQ1MDYxNzAwMDAwMFowUzELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUZJRE8gQWxs\n" + "aWFuY2UxHTAbBgNVBAsTFE1ldGFkYXRhIFRPQyBTaWduaW5nMQ0wCwYDVQQDEwRS\n" + "b290MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEFEoo+6jdxg6oUuOloqPjK/nVGyY+\n" + "AXCFz1i5JR4OPeFJs+my143ai0p34EX4R1Xxm9xGi9n8F+RxLjLNPHtlkB3X4ims\n" + "rfIx7QcEImx1cMTgu5zUiwxLX1ookVhIRSoso2MwYTAOBgNVHQ8BAf8EBAMCAQYw\n" + "DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU0qUfC6f2YshA1Ni9udeO0VS7vEYw\n" + "HwYDVR0jBBgwFoAU0qUfC6f2YshA1Ni9udeO0VS7vEYwCgYIKoZIzj0EAwMDaQAw\n" + "ZgIxAKulGbSFkDSZusGjbNkAhAkqTkLWo3GrN5nRBNNk2Q4BlG+AvM5q9wa5WciW\n" + "DcMdeQIxAMOEzOFsxX9Bo0h4LOFE5y5H8bdPFYW+l5gy1tQiJv+5NUyM2IBB55XU\n" + "YjdBz56jSA==\n" + "-----END CERTIFICATE-----\n"; } else { rootCert = fidoMdsRootCert; } }
let rootCerts; if (Array.isArray(rootCert)) rootCerts = rootCert; else rootCerts = [rootCert];
const certHeader = parsedJws.header ? parsedJws.header : parsedJws.protectedHeader;
await CertManager.verifyCertChain(certHeader.x5c, rootCerts, crls);
this.toc.raw = tocStr; if (this.toc.entries.some(entry => !entry.metadataStatement)) console.warn("[DEPRECATION WARNING] FIDO MDS v2 will be removed in October 2022. Please update to MDS v3!");
return this.toc; }
getToc() { return this.toc; }
addEntry(entryStr) { if (typeof entryStr !== "string" || entryStr.length < 1) { throw new Error("expected MDS entry to be non-empty string"); }
let newEntry = b64ToJsObject(entryStr, "MDS entry"); if (newEntry.metadataStatement) { newEntry = newEntry.metadataStatement; entryStr = jsObjectToB64(newEntry); }
newEntry.raw = entryStr; const newEntryId = getMdsEntryId(newEntry);
if (Array.isArray(newEntryId)) { newEntryId.forEach((id) => { this.unvalidatedEntryList.set(id, newEntry); }); } else { this.unvalidatedEntryList.set(newEntryId, newEntry); } }
async validate() { if (typeof this.toc !== "object" || this.toc === null) { throw new Error("add MDS TOC before attempting to validate MDS collection"); }
if (this.unvalidatedEntryList.size < 1) { throw new Error("add MDS entries before attempting to validate MDS collection"); }
let mapEntry; for (mapEntry of this.unvalidatedEntryList) { const entry = mapEntry[1]; const entryId = getMdsEntryId(entry); let tocEntry = this.toc.entries.filter((te) => { const teId = getMdsEntryId(te); const eq = idEquals(teId, entryId); return eq; });
if (tocEntry.length !== 1) { throw new Error(`found the wrong number of TOC entries for '${entryId}': ${tocEntry.length}`); } tocEntry = tocEntry[0];
const entryHash = await tools.hashDigest(entry.raw); let tocEntryHash; if (tocEntry.hash) { tocEntryHash = tocEntry.hash; } else { tocEntryHash = await tools.hashDigest( jsObjectToB64(tocEntry.metadataStatement), ); }
tocEntryHash = coerceToArrayBuffer(tocEntryHash, "MDS TOC entry hash"); if (!(arrayBufferEquals(entryHash, tocEntryHash))) { throw new Error("MDS entry hash did not match corresponding hash in MDS TOC"); }
const newEntry = new MdsEntry(entry, tocEntry); newEntry.collection = this;
if (Array.isArray(entryId)) { entryId.forEach((id) => { this.entryList.set(tocEntry.metadataStatement ? id.replace(/-/g, "") : id, newEntry); }); } else { this.entryList.set(tocEntry.metadataStatement ? entryId.replace(/-/g, "") : entryId, newEntry); } } }
findEntry(id) { if (id instanceof ArrayBuffer) { id = coerceToBase64Url(id, "MDS entry id"); }
if (typeof id !== "string") { throw new Error("expected 'id' to be String, got: " + id); }
return this.entryList.get(id.replace(/-/g, "")) || this.entryList.get( abToHex(tools.base64.toArrayBuffer(id, true)).replace(/-/g, ""), ) || null; }}
function getMdsEntryId(obj) { if (typeof obj !== "object") { throw new Error("getMdsEntryId expected 'obj' to be object, got: " + obj); }
if (typeof obj.aaid === "string") { return obj.aaid; }
if (typeof obj.aaguid === "string") { return obj.aaguid; }
if (Array.isArray(obj.attestationCertificateKeyIdentifiers)) { return obj.attestationCertificateKeyIdentifiers; }
throw new Error("MDS entry didn't have a valid ID");}
function idEquals(id1, id2) { if (id1 instanceof ArrayBuffer) { id1 = coerceToBase64Url(id1); }
if (id2 instanceof ArrayBuffer) { id2 = coerceToBase64Url(id2); }
if (typeof id1 === "string" && typeof id2 === "string") { return id1 === id2; }
if (Array.isArray(id1) && Array.isArray(id2)) { if (id1.length !== id2.length) return false; const allSame = id1.reduce((acc, val) => acc && id2.includes(val), true); if (!allSame) return false; return true; }
return false;}
function realBoolean(obj, prop) { if (obj[prop] === "true") obj[prop] = true; if (obj[prop] === "false") obj[prop] = false;}
export { MdsEntry, MdsCollection};