{"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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"}