import "reflect-metadata"; import { BasicConstraintsExtension, AuthorityKeyIdentifierExtension, ExtendedKeyUsageExtension, KeyUsageFlags, KeyUsagesExtension, PemConverter, Pkcs10CertificateRequest, PublicKey, SubjectAlternativeNameExtension, SubjectKeyIdentifierExtension, X509Certificate, X509Crl, } from "@peculiar/x509"; import { bytesToBase64Url, bytesToHex } from "@add-ideas/toolbox-helpers"; import { decryptEncryptedPkcs8, inspectEncryptedPkcs8, inspectPkcs8, reservePbkdf2Work, type Pbkdf2WorkBudget, } from "./pbes2"; import { inspectPkcs12, recognizesPkcs12, type Pkcs12BagInventory, } from "./pkcs12"; const MAX_INPUT_BYTES = 8 * 1024 * 1024; const MAX_PEM_BLOCKS = 256; export interface CryptoFinding { severity: "info" | "warning" | "error"; message: string; } export interface CryptoItem { id: string; type: string; title: string; facts: Record; findings: CryptoFinding[]; certificate?: X509Certificate; dnsNames?: string[]; } export interface CryptoInspection { items: CryptoItem[]; findings: CryptoFinding[]; chain: { child: string; issuer: string; signatureValid: boolean }[]; paths: CertificatePathAnalysis[]; } export interface CertificatePathLink { child: string; issuer: string; signatureValid: boolean; issuerIsCa: boolean; keyCertSignAllowed: boolean; authorityKeyIdentifierMatched?: boolean; findings: CryptoFinding[]; } export interface CertificatePathAnalysis { leaf: string; certificates: string[]; status: | "self-signed-anchor-present" | "incomplete" | "ambiguous" | "loop" | "invalid"; links: CertificatePathLink[]; findings: CryptoFinding[]; trusted: false; } export interface PemBlock { label: string; pem: string; bytes: Uint8Array; encrypted: boolean; } const PEM_PATTERN = /-----BEGIN ([A-Z0-9][A-Z0-9 -]{0,80})-----([\s\S]*?)-----END \1-----/gu; function buffer(bytes: Uint8Array): ArrayBuffer { return bytes.buffer.slice( bytes.byteOffset, bytes.byteOffset + bytes.byteLength, ) as ArrayBuffer; } function boundedUtf8Bytes(value: string): number { const size = new TextEncoder().encode(value).byteLength; if (size > MAX_INPUT_BYTES) throw new Error("Input exceeds the 8 MiB inspection limit."); return size; } export function parsePemBlocks(source: string): PemBlock[] { boundedUtf8Bytes(source); const blocks: PemBlock[] = []; for (const match of source.matchAll(PEM_PATTERN)) { if (blocks.length >= MAX_PEM_BLOCKS) throw new Error(`Input contains more than ${MAX_PEM_BLOCKS} PEM blocks.`); const label = match[1]!; const body = match[2]!; const encrypted = /(?:^|\n)(?:Proc-Type:\s*4,ENCRYPTED|DEK-Info:)/iu.test(body) || label === "ENCRYPTED PRIVATE KEY"; let rawData: ArrayBuffer; try { rawData = PemConverter.decodeFirst(match[0]); } catch { throw new Error(`The ${label} PEM block has invalid Base64 or framing.`); } const bytes = new Uint8Array(rawData); if (bytes.byteLength > MAX_INPUT_BYTES) throw new Error(`The ${label} block exceeds the 8 MiB limit.`); blocks.push({ label, pem: match[0], bytes, encrypted }); } return blocks; } async function sha256(bytes: BufferSource): Promise { return ( bytesToHex(new Uint8Array(await crypto.subtle.digest("SHA-256", bytes))) .toUpperCase() .match(/.{2}/gu) ?.join(":") ?? "" ); } function algorithmName(algorithm: Algorithm): string { const details = algorithm as Algorithm & { namedCurve?: string; hash?: { name?: string }; modulusLength?: number; }; return [ details.name, details.namedCurve, details.modulusLength ? `${details.modulusLength} bit` : "", details.hash?.name, ] .filter(Boolean) .join(" · "); } function validityFindings( certificate: X509Certificate, now: Date, ): CryptoFinding[] { if (now < certificate.notBefore) return [ { severity: "warning", message: `Not valid before ${certificate.notBefore.toISOString()}.`, }, ]; if (now > certificate.notAfter) return [ { severity: "error", message: `Expired on ${certificate.notAfter.toISOString()}.`, }, ]; const days = Math.ceil( (certificate.notAfter.getTime() - now.getTime()) / 86_400_000, ); return days <= 30 ? [ { severity: "warning", message: `Expires in ${days} day${days === 1 ? "" : "s"}.`, }, ] : []; } function extensionFacts(certificate: X509Certificate): Record { const facts: Record = {}; const basic = certificate.getExtension(BasicConstraintsExtension); if (basic) facts["Basic constraints"] = basic.ca ? `Certificate authority${basic.pathLength === undefined ? "" : `; path length ${basic.pathLength}`}` : "End entity"; const usages = certificate.getExtension(KeyUsagesExtension); if (usages) facts["Key usage bits"] = `0x${usages.usages.toString(16)}`; const extended = certificate.getExtension(ExtendedKeyUsageExtension); if (extended) facts["Extended key usages"] = extended.usages.join(", "); facts.Extensions = String(certificate.extensions.length); return facts; } async function inspectCertificate( pemOrBytes: string | Uint8Array, index: number, now: Date, ): Promise { const certificate = new X509Certificate( typeof pemOrBytes === "string" ? pemOrBytes : buffer(pemOrBytes), ); const san = certificate.getExtension(SubjectAlternativeNameExtension); const dnsNames = san?.names.items .filter((name) => name.type === "dns") .map((name) => name.value) ?? []; const selfSigned = await certificate.isSelfSigned().catch(() => false); return { id: `certificate-${index}`, type: "X.509 certificate", title: certificate.subject || `Certificate ${index + 1}`, facts: { Subject: certificate.subject || "(empty)", Issuer: certificate.issuer || "(empty)", Serial: certificate.serialNumber, "Valid from": certificate.notBefore.toISOString(), "Valid until": certificate.notAfter.toISOString(), "Public key": algorithmName(certificate.publicKey.algorithm), "Signature algorithm": algorithmName(certificate.signatureAlgorithm), "SHA-256 fingerprint": await sha256(certificate.rawData), "DNS names": dnsNames.join(", ") || "—", "Self-signed": selfSigned ? "Yes (signature verified)" : "No or unverifiable", ...extensionFacts(certificate), }, findings: validityFindings(certificate, now), certificate, dnsNames, }; } async function inspectBlock( block: PemBlock, index: number, now: Date, password: string | undefined, pbkdf2Budget: Pbkdf2WorkBudget, ): Promise { if (block.label === "CERTIFICATE" || block.label === "X509 CERTIFICATE") return inspectCertificate(block.pem, index, now); if ( block.label === "CERTIFICATE REQUEST" || block.label === "NEW CERTIFICATE REQUEST" ) { const request = new Pkcs10CertificateRequest(block.pem); return { id: `csr-${index}`, type: "PKCS #10 certificate request", title: request.subject || `Certificate request ${index + 1}`, facts: { Subject: request.subject || "(empty)", "Public key": algorithmName(request.publicKey.algorithm), "Signature algorithm": algorithmName(request.signatureAlgorithm), Extensions: String(request.extensions.length), "Signature valid": (await request.verify()) ? "Yes" : "No", "SHA-256 fingerprint": await sha256(request.rawData), }, findings: [], }; } if (block.label === "X509 CRL") { const crl = new X509Crl(block.pem); return { id: `crl-${index}`, type: "X.509 certificate revocation list", title: crl.issuer || `CRL ${index + 1}`, facts: { Issuer: crl.issuer, "This update": crl.thisUpdate.toISOString(), "Next update": crl.nextUpdate?.toISOString() ?? "—", "Revoked entries": String(crl.entries.length), "SHA-256 fingerprint": await sha256(crl.rawData), }, findings: crl.nextUpdate && crl.nextUpdate < now ? [ { severity: "warning", message: "The CRL next-update time has passed.", }, ] : [], }; } if (block.label === "PUBLIC KEY") { const key = new PublicKey(block.pem); return { id: `public-${index}`, type: "SubjectPublicKeyInfo", title: `Public key ${index + 1}`, facts: { Algorithm: algorithmName(key.algorithm), "SHA-256 SPKI fingerprint": await sha256(key.rawData), }, findings: [], }; } if (block.label.includes("PRIVATE KEY")) { if (block.label === "ENCRYPTED PRIVATE KEY") { const encryption = inspectEncryptedPkcs8(block.bytes); if (password !== undefined) reservePbkdf2Work( pbkdf2Budget, encryption.iterations, `Encrypted private key ${index + 1}`, ); const decrypted = password === undefined ? undefined : await decryptEncryptedPkcs8(block.bytes, password); try { return { id: `private-${index}`, type: block.label, title: `Encrypted private key ${index + 1}`, facts: { Encrypted: "Yes", Container: "PKCS #8 EncryptedPrivateKeyInfo", Encryption: `${encryption.scheme} · ${encryption.kdf} ${encryption.prf} · ${encryption.iterations.toLocaleString()} iterations · ${encryption.cipher}-${encryption.keyLength}`, Salt: `${encryption.saltBytes} bytes`, "IV / nonce": `${encryption.ivOrNonceBytes} bytes`, "Encrypted size": `${encryption.encryptedBytes.toLocaleString()} bytes`, "Private-key algorithm": decrypted ? `${decrypted.key.algorithm}${decrypted.key.curve ? ` · ${decrypted.key.curve}` : ""}` : "Password required to inspect", "Decryption status": decrypted ? "Decrypted and structurally validated in memory" : "Not attempted", "SHA-256 fingerprint": await sha256(buffer(block.bytes)), }, findings: [ ...(encryption.cipher === "AES-CBC" ? [ { severity: "warning" as const, message: "AES-CBC PBES2 does not authenticate the ciphertext; a successful padding/structure check is not an integrity guarantee.", }, ] : []), { severity: decrypted ? "warning" : "info", message: decrypted ? "The PBES2 key was decrypted only in page memory. Private-key material is sensitive and was not added to the report." : "PBES2 parameters were inspected without decryption. Enter a password explicitly to validate/import the key in memory.", }, ], }; } finally { decrypted?.bytes.fill(0); } } const key = block.label === "PRIVATE KEY" ? inspectPkcs8(block.bytes) : undefined; return { id: `private-${index}`, type: block.label, title: `Private key ${index + 1}`, facts: { Encrypted: block.encrypted ? "Yes" : "No", Container: block.label === "PRIVATE KEY" ? "PKCS #8 PrivateKeyInfo" : "Legacy or unsupported private-key container", ...(key ? { "Private-key algorithm": `${key.algorithm}${key.curve ? ` · ${key.curve}` : ""}`, } : {}), Size: `${block.bytes.byteLength.toLocaleString()} bytes`, "SHA-256 fingerprint": await sha256(buffer(block.bytes)), }, findings: [ { severity: block.encrypted ? "info" : "warning", message: block.encrypted ? "Encrypted private-key material was identified but not decrypted." : block.label === "PRIVATE KEY" ? "Unencrypted PKCS #8 private-key material is sensitive. It is structurally inspected but not persisted." : "This legacy private-key container is fingerprinted but is not imported; convert it to PKCS #8 explicitly outside this tool.", }, ], }; } return { id: `pem-${index}`, type: block.label, title: `${block.label} ${index + 1}`, facts: { Size: `${block.bytes.byteLength.toLocaleString()} bytes`, "SHA-256 fingerprint": await sha256(buffer(block.bytes)), }, findings: [ { severity: "warning", message: "This PEM object is identified but its internal structure is not supported in v0.1.", }, ], }; } type ExtendedJsonWebKey = JsonWebKey & { kid?: string; use?: string; key_ops?: string[]; }; type JsonWebKeySet = { keys: ExtendedJsonWebKey[] }; function isJwks(value: unknown): value is JsonWebKeySet { return ( !!value && typeof value === "object" && Array.isArray((value as { keys?: unknown }).keys) ); } function isJwk(value: unknown): value is ExtendedJsonWebKey { return ( !!value && typeof value === "object" && !Array.isArray(value) && typeof (value as { kty?: unknown }).kty === "string" ); } function thumbprintMembers(key: ExtendedJsonWebKey): Record { if (key.kty === "RSA" && key.e && key.n) return { e: key.e, kty: key.kty, n: key.n }; if (key.kty === "EC" && key.crv && key.x && key.y) return { crv: key.crv, kty: key.kty, x: key.x, y: key.y }; if (key.kty === "OKP" && key.crv && key.x) return { crv: key.crv, kty: key.kty, x: key.x }; if (key.kty === "oct" && key.k) return { k: key.k, kty: key.kty }; throw new Error( `JWK ${key.kid ?? "without a kid"} lacks the RFC 7638 members for ${key.kty ?? "an unknown key type"}.`, ); } async function inspectJwk( key: ExtendedJsonWebKey, index: number, ): Promise { const canonical = JSON.stringify(thumbprintMembers(key)); const digest = new Uint8Array( await crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonical)), ); const privateMembers = ["d", "p", "q", "dp", "dq", "qi", "oth", "k"].filter( (name) => name in key, ); return { id: `jwk-${index}`, type: "JSON Web Key", title: key.kid || `${key.kty ?? "Unknown"} key ${index + 1}`, facts: { Type: key.kty ?? "—", Curve: key.crv ?? "—", Algorithm: key.alg ?? "—", Use: key.use ?? "—", Operations: key.key_ops?.join(", ") ?? "—", "RFC 7638 SHA-256 thumbprint": bytesToBase64Url(digest, false), "Contains private material": privateMembers.length ? `Yes (${privateMembers.join(", ")})` : "No", }, findings: privateMembers.length ? [ { severity: "warning", message: "This JWK contains private or symmetric key material. It remains in memory only.", }, ] : [], }; } async function inspectJson(source: string): Promise { let parsed: unknown; try { parsed = JSON.parse(source); } catch { throw new Error("JSON input is not valid JSON."); } const candidates: unknown[] = isJwks(parsed) ? parsed.keys : [parsed]; if (!candidates.every(isJwk)) throw new Error( "JSON input must be a JWK or a JWKS containing only JWK objects.", ); const keys = candidates; if (keys.length > 1_000) throw new Error("JWKS contains more than 1,000 keys."); return Promise.all(keys.map((key, index) => inspectJwk(key, index))); } function extensionIdentifiers(certificate: X509Certificate): { authority?: string; subject?: string; } { const authority = certificate.getExtension(AuthorityKeyIdentifierExtension); const subject = certificate.getExtension(SubjectKeyIdentifierExtension); return { ...(authority?.keyId ? { authority: authority.keyId.toUpperCase() } : {}), ...(subject?.keyId ? { subject: subject.keyId.toUpperCase() } : {}), }; } export async function analyzeCertificatePaths( items: CryptoItem[], now = new Date(), ): Promise { const certificates = items.filter( (item): item is CryptoItem & { certificate: X509Certificate } => !!item.certificate, ); if (certificates.length === 0) return []; const leaves = certificates.filter( (candidate) => !certificates.some( (child) => child !== candidate && child.certificate.issuer === candidate.certificate.subject, ), ); const starts = leaves.length > 0 ? leaves : certificates; const paths: CertificatePathAnalysis[] = []; for (const start of starts) { const certificatePath = [start]; const links: CertificatePathLink[] = []; const findings: CryptoFinding[] = []; const visited = new Set([start.id]); let current = start; let status: CertificatePathAnalysis["status"] = "incomplete"; let pathInvalid = false; for (let depth = 0; depth < 32; depth += 1) { const certificate = current.certificate; if (now < certificate.notBefore || now > certificate.notAfter) { pathInvalid = true; findings.push({ severity: "error", message: `${current.title} is outside its certificate validity interval at the selected inspection time.`, }); } if (certificate.subject === certificate.issuer) { const selfSignature = await certificate .isSelfSigned() .catch(() => false); if (!selfSignature) { pathInvalid = true; findings.push({ severity: "error", message: `${current.title} names itself as issuer but its self-signature did not verify.`, }); } status = pathInvalid ? "invalid" : "self-signed-anchor-present"; findings.push({ severity: "info", message: "A self-signed certificate terminates this explicit-input path, but it is not treated as trusted.", }); break; } const identifiers = extensionIdentifiers(certificate); const nameCandidates = certificates.filter( (candidate) => candidate !== current && candidate.certificate.subject === certificate.issuer, ); let candidates = nameCandidates; let authorityMatched: boolean | undefined; if (identifiers.authority && nameCandidates.length > 0) { const keyMatches = nameCandidates.filter( (candidate) => extensionIdentifiers(candidate.certificate).subject === identifiers.authority, ); if (keyMatches.length > 0) { candidates = keyMatches; authorityMatched = true; } else if ( nameCandidates.some( (candidate) => extensionIdentifiers(candidate.certificate).subject !== undefined, ) ) { findings.push({ severity: "error", message: `${current.title} authority key identifier does not match any same-name issuer candidate.`, }); status = "invalid"; break; } } if (candidates.length === 0) { findings.push({ severity: "warning", message: `No supplied certificate has subject “${certificate.issuer}” for ${current.title}.`, }); status = pathInvalid ? "invalid" : "incomplete"; break; } if (candidates.length > 1) { findings.push({ severity: "warning", message: `${current.title} has ${candidates.length} indistinguishable issuer candidates; the path is not guessed.`, }); status = "ambiguous"; break; } const issuer = candidates[0]!; if (visited.has(issuer.id)) { findings.push({ severity: "error", message: `Certificate loop detected when linking ${current.title} to ${issuer.title}.`, }); status = "loop"; break; } const signatureValid = await certificate .verify({ publicKey: issuer.certificate.publicKey, signatureOnly: true, }) .catch(() => false); const basic = issuer.certificate.getExtension(BasicConstraintsExtension); const usages = issuer.certificate.getExtension(KeyUsagesExtension); const issuerIsCa = basic?.ca === true; const keyCertSignAllowed = !usages || (usages.usages & KeyUsageFlags.keyCertSign) !== 0; const linkFindings: CryptoFinding[] = []; if (!signatureValid) linkFindings.push({ severity: "error", message: "Certificate signature did not verify with this issuer candidate.", }); if (!issuerIsCa) linkFindings.push({ severity: "error", message: "Issuer certificate does not assert CA=true in Basic Constraints.", }); if (!keyCertSignAllowed) linkFindings.push({ severity: "error", message: "Issuer Key Usage does not permit certificate signing.", }); const caCertificatesBelow = certificatePath .slice(1) .filter( (item) => item.certificate.getExtension(BasicConstraintsExtension)?.ca === true, ).length; if ( basic?.pathLength !== undefined && caCertificatesBelow > basic.pathLength ) linkFindings.push({ severity: "error", message: `Issuer pathLength ${basic.pathLength} is exceeded by ${caCertificatesBelow} subordinate CA certificate(s).`, }); if (linkFindings.some((finding) => finding.severity === "error")) pathInvalid = true; links.push({ child: current.title, issuer: issuer.title, signatureValid, issuerIsCa, keyCertSignAllowed, ...(authorityMatched === undefined ? {} : { authorityKeyIdentifierMatched: authorityMatched }), findings: linkFindings, }); findings.push(...linkFindings); certificatePath.push(issuer); visited.add(issuer.id); current = issuer; if (depth === 31) { findings.push({ severity: "error", message: "Certificate path exceeds the 32-certificate bound.", }); status = "invalid"; } } paths.push({ leaf: start.title, certificates: certificatePath.map((item) => item.title), status, links, findings, trusted: false, }); } return paths; } function chainFromPaths( paths: readonly CertificatePathAnalysis[], ): CryptoInspection["chain"] { const result: CryptoInspection["chain"] = []; const seen = new Set(); for (const path of paths) { for (const link of path.links) { const key = `${link.child}\0${link.issuer}`; if (seen.has(key)) continue; seen.add(key); result.push({ child: link.child, issuer: link.issuer, signatureValid: link.signatureValid, }); } } return result; } function pfxEncryptionSummary( encryption: NonNullable, ): string { return `${encryption.scheme} · ${encryption.kdf} ${encryption.prf} · ${encryption.iterations.toLocaleString()} iterations · ${encryption.cipher}-${encryption.keyLength}`; } async function inspectPkcs12Items( bytes: Uint8Array, now: Date, password: string | undefined, ): Promise { const pfx = await inspectPkcs12(bytes, { ...(password === undefined ? {} : { password }), }); const macStatus = pfx.mac.status === "verified" ? "Verified with the explicitly supplied password" : pfx.mac.status === "password-required" ? "Present; explicit password required to verify" : pfx.mac.status === "unsupported" ? `Present; unsupported digest (${pfx.mac.algorithm ?? "unknown"})` : "Absent"; const contentSummary = pfx.contents .map((content) => { const encryption = content.encryption ? `; ${content.encryption.cipher}-${content.encryption.keyLength}, ${content.encryption.iterations.toLocaleString()} PBKDF2 iterations` : ""; return `#${content.index + 1} ${content.type}: ${content.state}, ${content.bagCount} bag(s)${encryption}`; }) .join(" | "); const envelope: CryptoItem = { id: "pfx-envelope", type: "PKCS #12/PFX", title: "PKCS #12/PFX authenticated safe", facts: { Version: String(pfx.version), "Authenticated-safe content type": pfx.authSafeContentType, MacData: macStatus, ...(pfx.mac.algorithm ? { "MAC algorithm": pfx.mac.algorithm } : {}), ...(pfx.mac.iterations === undefined ? {} : { "MAC iterations": pfx.mac.iterations.toLocaleString() }), Contents: contentSummary || "Empty AuthenticatedSafe", "SafeBag inventory": `${pfx.bags.length.toLocaleString()} bag(s)`, Size: `${pfx.bytes.toLocaleString()} bytes`, "SHA-256 fingerprint": await sha256(buffer(bytes)), }, findings: [ ...pfx.warnings.map((message) => ({ severity: "warning" as const, message, })), ...(pfx.mac.status === "password-required" ? [ { severity: "info" as const, message: "Enable password use explicitly to verify MacData and inspect supported encrypted SafeContents/key bags.", }, ] : []), { severity: "warning", message: "Private-key bag values are only structurally inspected in page memory. Secret bytes and the password are never included in the report or exported implicitly.", }, ], }; const bagItems: CryptoItem[] = []; for (const [index, bag] of pfx.bags.entries()) { const bagFacts: Record = { "Bag path": bag.path, "Bag OID": bag.bagOid, "Friendly name": bag.friendlyName ?? "—", localKeyId: bag.localKeyId ?? "—", Encrypted: bag.encrypted ? "Yes" : "No", State: bag.state === "inspected" ? "Structurally inspected" : bag.state === "password-required" ? "Explicit password required" : "Value deliberately not decoded", ...(bag.encryption ? { Encryption: pfxEncryptionSummary(bag.encryption) } : {}), ...(bag.key ? { "Private-key algorithm": `${bag.key.algorithm}${bag.key.curve ? ` · ${bag.key.curve}` : ""}`, "Private-key container size": `${bag.key.bytes.toLocaleString()} bytes`, } : {}), }; if (bag.certificateBytes) { let certificate: CryptoItem; try { certificate = await inspectCertificate( bag.certificateBytes, index, now, ); } catch (reason) { throw new Error( `${bag.path} contains an invalid X.509 certificate: ${reason instanceof Error ? reason.message : "certificate parsing failed"}`, { cause: reason }, ); } bagItems.push({ ...certificate, id: `pfx-bag-${index}`, title: bag.friendlyName || certificate.title, facts: { ...bagFacts, ...certificate.facts }, }); continue; } bagItems.push({ id: `pfx-bag-${index}`, type: bag.bagType === "private-key" || bag.bagType === "shrouded-private-key" ? "PKCS #12 private-key bag" : `PKCS #12 ${bag.bagType} bag`, title: bag.friendlyName ?? `${bag.bagType.replaceAll("-", " ")} ${index + 1}`, facts: bagFacts, findings: [ ...(bag.state === "password-required" ? [ { severity: "info" as const, message: "The encrypted bag was inventoried without attempting decryption.", }, ] : []), ...(bag.key ? [ { severity: "warning" as const, message: "Only private-key container metadata is shown; secret material is not retained in the inspection result.", }, ] : []), ...(bag.state === "unsupported" ? [ { severity: "warning" as const, message: "This bag type is identified, but its value is not decoded or exported.", }, ] : []), ], }); } return [envelope, ...bagItems]; } export async function inspectCryptoInput( input: string | Uint8Array, now = new Date(), options: { password?: string } = {}, ): Promise { if (typeof input !== "string" && input.byteLength > MAX_INPUT_BYTES) throw new Error("Input exceeds the 8 MiB inspection limit."); const findings: CryptoFinding[] = []; const pbkdf2Budget: Pbkdf2WorkBudget = { iterations: 0 }; let items: CryptoItem[]; if (typeof input === "string" && input.trimStart().startsWith("{")) { boundedUtf8Bytes(input); items = await inspectJson(input); } else if (typeof input === "string") { const blocks = parsePemBlocks(input); if (!blocks.length) throw new Error("No supported PEM or JWK/JWKS object was found."); items = []; for (const [index, block] of blocks.entries()) items.push( await inspectBlock(block, index, now, options.password, pbkdf2Budget), ); } else { if (recognizesPkcs12(input)) { items = await inspectPkcs12Items(input, now, options.password); const paths = await analyzeCertificatePaths(items, now); const chain = chainFromPaths(paths); if ( items.some((item) => item.certificate) && !paths.some((path) => path.status === "self-signed-anchor-present") ) findings.push({ severity: "info", message: "No supplied path terminates at a valid self-signed certificate. No browser or operating-system trust store is consulted.", }); return { items, findings, chain, paths }; } const candidates: ((bytes: Uint8Array) => Promise)[] = [ (bytes) => inspectCertificate(bytes, 0, now), async (bytes) => inspectBlock( { label: "CERTIFICATE REQUEST", pem: PemConverter.encode(buffer(bytes), "CERTIFICATE REQUEST"), bytes, encrypted: false, }, 0, now, undefined, pbkdf2Budget, ), async (bytes) => inspectBlock( { label: "X509 CRL", pem: PemConverter.encode(buffer(bytes), "X509 CRL"), bytes, encrypted: false, }, 0, now, undefined, pbkdf2Budget, ), async (bytes) => { const encryption = inspectEncryptedPkcs8(bytes); if (options.password !== undefined) reservePbkdf2Work( pbkdf2Budget, encryption.iterations, "Encrypted private key 1", ); const decrypted = options.password === undefined ? undefined : await decryptEncryptedPkcs8(bytes, options.password); try { return { id: "private-0", type: "ENCRYPTED PRIVATE KEY", title: "Encrypted private key 1", facts: { Container: "PKCS #8 EncryptedPrivateKeyInfo", Encryption: `${encryption.scheme} · ${encryption.kdf} ${encryption.prf} · ${encryption.iterations.toLocaleString()} iterations · ${encryption.cipher}-${encryption.keyLength}`, Salt: `${encryption.saltBytes} bytes`, "IV / nonce": `${encryption.ivOrNonceBytes} bytes`, "Encrypted size": `${encryption.encryptedBytes.toLocaleString()} bytes`, "Private-key algorithm": decrypted ? `${decrypted.key.algorithm}${decrypted.key.curve ? ` · ${decrypted.key.curve}` : ""}` : "Password required to inspect", "Decryption status": decrypted ? "Decrypted and structurally validated in memory" : "Not attempted", "SHA-256 fingerprint": await sha256(buffer(bytes)), }, findings: [ ...(encryption.cipher === "AES-CBC" ? [ { severity: "warning" as const, message: "AES-CBC PBES2 does not authenticate the ciphertext; a successful padding/structure check is not an integrity guarantee.", }, ] : []), { severity: decrypted ? "warning" : "info", message: decrypted ? "The PBES2 key was decrypted only in page memory. Private-key material is sensitive and was not added to the report." : "PBES2 parameters were inspected without decryption. Enter a password explicitly to validate/import the key in memory.", }, ], }; } finally { decrypted?.bytes.fill(0); } }, ]; let matched: CryptoItem | undefined; for (const [candidateIndex, candidate] of candidates.entries()) { try { matched = await candidate(input); break; } catch (reason) { if (candidateIndex === 3 && options.password) { let recognized = false; try { inspectEncryptedPkcs8(input); recognized = true; } catch { /* this was not PBES2; continue probing */ } if (recognized) throw reason; } /* try the next DER model */ } } if (!matched) throw new Error( "The binary input is not a supported DER certificate, CSR, CRL, PBES2 encrypted PKCS #8 key, or PKCS #12/PFX file.", ); items = [matched]; } const paths = await analyzeCertificatePaths(items, now); const chain = chainFromPaths(paths); if ( items.some((item) => item.certificate) && !paths.some((path) => path.status === "self-signed-anchor-present") ) findings.push({ severity: "info", message: "No supplied path terminates at a valid self-signed certificate. No browser or operating-system trust store is consulted.", }); return { items, findings, chain, paths }; } function dnsMatch(pattern: string, hostname: string): boolean { const left = pattern.toLowerCase().replace(/\.$/u, ""); const right = hostname.toLowerCase().replace(/\.$/u, ""); if (!left.includes("*")) return left === right; if (!left.startsWith("*.") || left.slice(2).includes("*")) return false; const suffix = left.slice(1); return ( right.endsWith(suffix) && right.split(".").length === left.split(".").length ); } export function checkCertificateHostname( item: CryptoItem, hostname: string, ): { valid: boolean; message: string } { const candidate = hostname.trim().replace(/\.$/u, ""); const validHostname = candidate.length > 0 && candidate.length <= 253 && candidate .split(".") .every( (label) => label.length > 0 && label.length <= 63 && /^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/u.test(label), ); if (!validHostname) return { valid: false, message: "Enter a valid ASCII DNS hostname for this local check.", }; if (!item.certificate) return { valid: false, message: "Select a certificate." }; if (!item.dnsNames?.length) return { valid: false, message: "The certificate has no DNS subject-alternative names; legacy Common Name fallback is not used.", }; const match = item.dnsNames.find((name) => dnsMatch(name, candidate)); return match ? { valid: true, message: `${candidate} matches ${match}.` } : { valid: false, message: `${candidate} does not match any DNS subject-alternative name.`, }; }