@credo-ts/core
Version:
<p align="center"> <br /> <img alt="Credo Logo" src="https://github.com/openwallet-foundation/credo-ts/blob/c7886cb8377ceb8ee4efe8d264211e561a75072d/images/credo-logo.png" height="250px" /> </p> <h1 align="center"><b>Credo - Core</b></h1
1 lines • 12.1 kB
Source Map (JSON)
{"version":3,"file":"crlSummary.mjs","names":["z"],"sources":["../../../../src/modules/x509/utils/crlSummary.ts"],"sourcesContent":["import { z } from 'zod'\nimport { Hasher } from '../../../crypto/hashes/Hasher'\nimport { TypedArrayEncoder } from '../../../utils'\nimport type { X509Certificate } from '../X509Certificate'\nimport type {\n X509CertificateRevocationList,\n X509CertificateRevocationListEntry,\n X509CertificateRevocationListEntryReason,\n X509IssuingDistributionPoint,\n} from '../X509CertificateRevocationList'\nimport { normalizeSerialNumber } from './serialNumber'\n\n/**\n * The subset of a verified CRL that the revocation engine consumes.\n *\n * Satisfied both by a freshly parsed and verified {@link X509CertificateRevocationList} and by a\n * cached {@link X509CrlSummary} (via {@link CrlSummaryVerifiedCrl}), so a cache hit does not need\n * to re-parse the CRL.\n */\nexport interface VerifiedCrl {\n readonly deltaCrlIndicator: number | undefined\n readonly issuingDistributionPoint: X509IssuingDistributionPoint | undefined\n readonly criticalExtensionIds: string[]\n findRevoked(certificate: X509Certificate): X509CertificateRevocationListEntry | null\n}\n\n/**\n * Plain-JSON summary of a verified CRL, cached so that subsequent revocation checks do not need to\n * re-parse the (potentially multi-MB) CRL. Must remain JSON-serializable so it survives\n * storage-backed cache implementations.\n */\nconst zX509CrlSummary = z\n .object({\n // The issuer binding captures what CRL verification actually depends on from the issuer\n // certificate: its subject name (issuer-name binding) and its public key (signature). The\n // remaining issuer-dependent check, cRLSign key usage, is re-evaluated on the certificate\n // provided at hit time, so a renewed or cross-certified issuer certificate with the same name\n // and key can reuse the summary.\n /** SHA-256 (hex) of the DER-encoded subject name of the issuer certificate */\n issuerNameSha256: z.string(),\n /** RFC 7638 JWK thumbprint (hex) of the issuer certificate's public key */\n issuerPublicJwkThumbprint: z.string(),\n /** `thisUpdate` as epoch milliseconds */\n thisUpdate: z.number(),\n /** `nextUpdate` as epoch milliseconds, absent when the CRL has none */\n nextUpdate: z.optional(z.number()),\n /** SHA-256 (hex) of the DER-encoded CRL bytes, used to revalidate a stale summary without re-parsing */\n crlSha256: z.string(),\n /** Epoch milliseconds after which the summary must be revalidated against freshly fetched CRL bytes */\n staleAt: z.number(),\n deltaCrlIndicator: z.optional(z.number()),\n issuingDistributionPoint: z.optional(\n z.object({\n fullName: z.array(z.string()),\n onlyContainsUserCerts: z.boolean(),\n onlyContainsCACerts: z.boolean(),\n onlySomeReasons: z.optional(z.array(z.number())),\n indirectCRL: z.boolean(),\n onlyContainsAttributeCerts: z.boolean(),\n })\n ),\n criticalExtensionIds: z.array(z.string()),\n // Parallel arrays with one slot per revoked entry, preserving CRL order so lookups keep the\n // first-match semantics of X509CertificateRevocationList.findRevoked.\n /** Original serial number strings as parsed from the CRL (not normalized) */\n serialNumbers: z.array(z.string()),\n /** Revocation date per entry as epoch milliseconds */\n revocationDates: z.array(z.number()),\n /** Revocation reason per entry, `null` when absent (`undefined` is not JSON-safe in arrays) */\n reasons: z.array(z.nullable(z.number())),\n })\n .refine(\n (summary) =>\n summary.serialNumbers.length === summary.revocationDates.length &&\n summary.serialNumbers.length === summary.reasons.length\n )\n\nexport type X509CrlSummary = z.output<typeof zX509CrlSummary>\n\n/**\n * Guard for cached summaries. Anything that does not match the expected shape (older formats,\n * corrupted values) is treated as a cache miss by callers, never as an error, so this must not\n * throw. Callers must keep using the original object rather than zod's parsed copy: cloning would\n * copy the potentially huge entry arrays on every cache read and break the revoked-index\n * memoization, which is keyed on object identity.\n */\nexport function isX509CrlSummary(value: unknown): value is X509CrlSummary {\n return zX509CrlSummary.safeParse(value).success\n}\n\nexport function computeIssuerNameSha256(issuerCertificate: X509Certificate): string {\n return TypedArrayEncoder.toHex(Hasher.hash(issuerCertificate.subjectNameBytes, 'SHA-256'))\n}\n\nexport function computeIssuerPublicJwkThumbprint(issuerCertificate: X509Certificate): string {\n return TypedArrayEncoder.toHex(issuerCertificate.publicJwk.getJwkThumbprint())\n}\n\nexport function computeCrlSha256(crlBytes: Uint8Array): string {\n return TypedArrayEncoder.toHex(Hasher.hash(crlBytes, 'SHA-256'))\n}\n\n/**\n * Derive the cacheable summary of a verified CRL.\n *\n * `staleAt` is caching policy (freshness deadline derived from the verification date and the\n * configured expiry), so it is provided by the caller rather than derived from the CRL.\n *\n * May throw for malformed CRLs (e.g. duplicate extensions, via the delta/issuing-distribution-point\n * getters); callers must then skip caching the summary so such CRLs keep their existing behavior.\n */\nexport function buildCrlSummary(\n crl: X509CertificateRevocationList,\n issuerCertificate: X509Certificate,\n staleAt: number\n): X509CrlSummary {\n const serialNumbers: string[] = []\n const revocationDates: number[] = []\n const reasons: (number | null)[] = []\n for (const entry of crl.revokedCertificates) {\n serialNumbers.push(entry.serialNumber)\n revocationDates.push(entry.revocationDate.getTime())\n reasons.push(entry.reason ?? null)\n }\n\n const issuingDistributionPoint = crl.issuingDistributionPoint\n\n return {\n issuerNameSha256: computeIssuerNameSha256(issuerCertificate),\n issuerPublicJwkThumbprint: computeIssuerPublicJwkThumbprint(issuerCertificate),\n thisUpdate: crl.thisUpdate.getTime(),\n nextUpdate: crl.nextUpdate?.getTime(),\n crlSha256: computeCrlSha256(crl.rawCertificateRevocationList),\n staleAt,\n deltaCrlIndicator: crl.deltaCrlIndicator,\n issuingDistributionPoint: issuingDistributionPoint\n ? {\n fullName: issuingDistributionPoint.fullName,\n onlyContainsUserCerts: issuingDistributionPoint.onlyContainsUserCerts,\n onlyContainsCACerts: issuingDistributionPoint.onlyContainsCACerts,\n onlySomeReasons: issuingDistributionPoint.onlySomeReasons,\n indirectCRL: issuingDistributionPoint.indirectCRL,\n onlyContainsAttributeCerts: issuingDistributionPoint.onlyContainsAttributeCerts,\n }\n : undefined,\n criticalExtensionIds: crl.criticalExtensionIds,\n serialNumbers,\n revocationDates,\n reasons,\n }\n}\n\n// Lookup index per summary object. Keyed weakly so in-memory caches (which return the same summary\n// object on every hit) only pay the index build once, while the summaries themselves can be evicted.\nconst revokedIndexBySummary = new WeakMap<X509CrlSummary, Map<string, number>>()\n\n/**\n * {@link VerifiedCrl} view over a cached {@link X509CrlSummary}. Read-only: the summary may be a\n * shared reference owned by an in-memory cache and must never be mutated.\n */\nexport class CrlSummaryVerifiedCrl implements VerifiedCrl {\n public constructor(private readonly summary: X509CrlSummary) {}\n\n public get deltaCrlIndicator(): number | undefined {\n return this.summary.deltaCrlIndicator\n }\n\n public get issuingDistributionPoint(): X509IssuingDistributionPoint | undefined {\n const idp = this.summary.issuingDistributionPoint\n if (!idp) return undefined\n return {\n fullName: [...idp.fullName],\n onlyContainsUserCerts: idp.onlyContainsUserCerts,\n onlyContainsCACerts: idp.onlyContainsCACerts,\n onlySomeReasons: idp.onlySomeReasons ? [...idp.onlySomeReasons] : undefined,\n indirectCRL: idp.indirectCRL,\n onlyContainsAttributeCerts: idp.onlyContainsAttributeCerts,\n }\n }\n\n public get criticalExtensionIds(): string[] {\n return [...this.summary.criticalExtensionIds]\n }\n\n public findRevoked(certificate: X509Certificate): X509CertificateRevocationListEntry | null {\n const index = this.revokedIndex.get(normalizeSerialNumber(certificate.data.serialNumber))\n if (index === undefined) return null\n\n return {\n serialNumber: this.summary.serialNumbers[index],\n revocationDate: new Date(this.summary.revocationDates[index]),\n reason: (this.summary.reasons[index] ?? undefined) as X509CertificateRevocationListEntryReason | undefined,\n }\n }\n\n private get revokedIndex(): Map<string, number> {\n let index = revokedIndexBySummary.get(this.summary)\n if (!index) {\n index = new Map()\n for (let i = 0; i < this.summary.serialNumbers.length; i++) {\n const normalized = normalizeSerialNumber(this.summary.serialNumbers[i])\n // Keep the first occurrence of a duplicate serial, matching the linear scan of\n // X509CertificateRevocationList.findRevoked\n if (!index.has(normalized)) index.set(normalized, i)\n }\n revokedIndexBySummary.set(this.summary, index)\n }\n return index\n }\n}\n"],"mappings":"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"}