UNPKG

@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 • 43.1 kB
{"version":3,"file":"X509RevocationService.mjs","names":[],"sources":["../../../src/modules/x509/X509RevocationService.ts"],"sourcesContent":["import type { AgentContext } from '../../agent'\nimport { CredoWebCrypto } from '../../crypto/webcrypto'\nimport { injectable } from '../../plugins'\nimport { validateCriticalCrlExtensionIds } from './utils/criticalExtensions'\nimport { fetchCrl, getCachedCrlSummary, setCachedCrlSummary } from './utils/crlFetcher'\nimport {\n buildCrlSummary,\n CrlSummaryVerifiedCrl,\n computeCrlSha256,\n computeIssuerNameSha256,\n computeIssuerPublicJwkThumbprint,\n type VerifiedCrl,\n type X509CrlSummary,\n} from './utils/crlSummary'\nimport { X509Certificate, X509KeyUsage } from './X509Certificate'\nimport { X509CertificateRevocationList, type X509CertificateRevocationListEntry } from './X509CertificateRevocationList'\nimport { ALL_REVOCATION_REASONS_MASK, X509RevocationReason } from './X509CrlDistributionPoint'\nimport { X509CrlUnavailableError, X509Error } from './X509Error'\nimport { X509ModuleConfig } from './X509ModuleConfig'\nimport type {\n X509CheckCertificateRevocationOptions,\n X509FetchCertificateRevocationListOptions,\n} from './X509ServiceOptions'\nimport { X509RevocationCheckMode, type X509RevocationCheckOptions } from './X509ValidationOptions'\nimport type { X509CertificateSingleValidationResult } from './X509ValidationResult'\n\nexport interface X509RevocationCheckResult {\n isValid: boolean\n isRevoked?: boolean\n error?: Error\n details?: string\n method?: 'crl'\n}\n\n/**\n * Default freshness period for a cached verified CRL summary (1 hour): how long it is served\n * without being revalidated against freshly fetched CRL bytes.\n */\nconst DEFAULT_CRL_CACHE_EXPIRY_SECONDS = 3600\n\ninterface FetchCrlOptions {\n url: string\n issuerCertificate: X509Certificate\n verificationDate: Date\n options: { timeoutMs?: number; maxCrlSizeBytes?: number; crlCacheExpirySeconds?: number }\n useCache: boolean\n}\n\n@injectable()\n// biome-ignore lint/complexity/noStaticOnlyClass: Service class pattern used in Credo\nexport class X509RevocationService {\n /**\n * Check the revocation status of a single certificate using CRL.\n */\n public static async checkCertificateRevocation(\n agentContext: AgentContext,\n { certificate, issuerCertificate, revocationCheckOptions }: X509CheckCertificateRevocationOptions\n ): Promise<X509RevocationCheckResult> {\n const parsedCertificate =\n certificate instanceof X509Certificate ? certificate : X509Certificate.fromEncodedCertificate(certificate)\n const parsedIssuerCertificate =\n issuerCertificate instanceof X509Certificate\n ? issuerCertificate\n : X509Certificate.fromEncodedCertificate(issuerCertificate)\n\n const options = revocationCheckOptions ??\n agentContext.dependencyManager.resolve(X509ModuleConfig).revocationCheck ?? {\n mode: X509RevocationCheckMode.SoftFail,\n }\n\n const mode = options.mode ?? X509RevocationCheckMode.SoftFail\n if (mode === X509RevocationCheckMode.Disabled) {\n return { isValid: true, details: 'Revocation checking disabled' }\n }\n\n try {\n return await X509RevocationService.checkCrl(agentContext, parsedCertificate, parsedIssuerCertificate, options)\n } catch (error) {\n return X509RevocationService.handleRevocationError(error, mode)\n }\n }\n\n /**\n * Check the revocation status of every certificate in a chain (the leaf and, unless\n * `checkFullChain` is disabled, the intermediate CAs). The root is skipped as it is self-issued and\n * not covered by a CRL we can fetch and verify.\n *\n * The chain must be ordered from root (index 0) to leaf (last index); each certificate's issuer is\n * the certificate that precedes it in the chain.\n *\n * The same CRL handling limitations as {@link checkCertificateRevocation} apply (CRL only; delta\n * and indirect CRLs rejected; no IDP distribution-point-name matching; no `onlySomeReasons`\n * cross-check; Freshest CRL / AIA and `invalidityDate` not processed).\n */\n public static async checkCertificateChainRevocation(\n agentContext: AgentContext,\n certificateChain: X509Certificate[],\n config: X509ModuleConfig,\n verificationDate: Date\n ): Promise<X509CertificateSingleValidationResult> {\n const configuredRevocationCheck = config.revocationCheck\n if (!configuredRevocationCheck || configuredRevocationCheck.mode === X509RevocationCheckMode.Disabled) {\n return { isValid: true, details: 'Revocation checking disabled' }\n }\n\n // Use the chain's verificationDate for CRL validity (thisUpdate/nextUpdate) checks\n // unless the revocation config explicitly overrides it.\n const revocationConfig: X509RevocationCheckOptions = {\n ...configuredRevocationCheck,\n verificationDate: configuredRevocationCheck.verificationDate ?? verificationDate,\n }\n\n // The chain is ordered from root (index 0) to leaf (last index). A certificate's issuer is the\n // certificate that precedes it in the chain. The root certificate (index 0) is skipped.\n const checkFullChain = revocationConfig.checkFullChain ?? true\n const startIndex = checkFullChain ? 1 : certificateChain.length - 1\n\n for (let i = startIndex; i < certificateChain.length; i++) {\n const certificate = certificateChain[i]\n // The issuer is the preceding certificate in the chain.\n const issuerCertificate = certificateChain[i - 1]\n\n if (!issuerCertificate) {\n // No issuer available (e.g. a single self-signed certificate); nothing to check.\n continue\n }\n\n const result = await X509RevocationService.checkCertificateRevocation(agentContext, {\n certificate,\n issuerCertificate,\n revocationCheckOptions: revocationConfig,\n })\n\n if (!result.isValid) {\n return {\n isValid: false,\n error: result.error,\n details: result.details,\n }\n }\n }\n\n return { isValid: true, details: 'No certificates are revoked' }\n }\n\n /**\n * Checks the revocation status of a certificate using CRL with proper reason partitioning\n * Based on RFC 5280 Section 5.2.5\n */\n private static async checkCrl(\n agentContext: AgentContext,\n certificate: X509Certificate,\n issuerCertificate: X509Certificate,\n options: X509RevocationCheckOptions\n ): Promise<X509RevocationCheckResult> {\n // Get CRL distribution points from certificate\n const distributionPoints = certificate.crlDistributionPoints\n if (distributionPoints.length === 0) {\n return {\n isValid: true,\n details: 'No CRL distribution points in certificate',\n }\n }\n\n const verificationDate = options.verificationDate ?? new Date()\n\n // Step 1: Check if there's a \"full\" distribution point (no reasons specified). Indirect CRLs\n // (those with a crlIssuer) are excluded here - we can't verify them, so they fall through to the\n // partitioned path below where they are tracked as an unsupported feature instead of being\n // fetched and failing verification against the wrong issuer.\n const fullDistributionPoint = distributionPoints.find(\n (dp) => dp.reasons === undefined && dp.crlIssuer === undefined\n )\n\n if (fullDistributionPoint) {\n // Full DP covers all reasons - just fetch and check this one CRL\n return await X509RevocationService.checkDistributionPoint(\n agentContext,\n certificate,\n issuerCertificate,\n fullDistributionPoint,\n verificationDate,\n options\n )\n }\n\n // Step 2: No full DP - need to ensure required reasons are covered by fetching multiple CRLs\n // Calculate which reasons we need to cover\n // Default: require all reasons that are actually present in the certificate's DPs\n // (no point requiring reasons the issuer never published DPs for)\n const requiredReasonsMask = options.requiredReasons\n ? X509RevocationService.calculateReasonMask(options.requiredReasons)\n : X509RevocationService.calculateAllReasonsInDPs(distributionPoints)\n\n let coveredReasons = 0 // Bitmask of covered reason codes\n const coveredCrls: VerifiedCrl[] = []\n const fetchedDPs: string[] = []\n const fetchErrors: string[] = [] // Track errors for DPs we couldn't fetch\n const unsupportedDPs: Array<{ reasons: number; feature: string }> = [] // Track DPs we can't process\n\n // Whether any DP downloaded a CRL that then failed verification/validity\n let integrityFailure = false\n\n for (const dp of distributionPoints) {\n // Calculate which reasons this DP covers\n const dpReasonMask = X509RevocationService.calculateReasonMask(dp.reasons)\n\n // Check if this DP adds any new coverage for reasons we care about\n const newCoverage = dpReasonMask & requiredReasonsMask & ~coveredReasons\n if (newCoverage === 0) {\n // This DP doesn't add any new reason coverage for required reasons, skip it\n continue\n }\n\n // Check for unsupported features\n if (dp.crlIssuer !== undefined) {\n // This DP uses an indirect CRL (different issuer) which we don't support\n // Track it but don't fail immediately - we might cover these reasons with other DPs\n unsupportedDPs.push({ reasons: dpReasonMask, feature: `crlIssuer (${dp.crlIssuer})` })\n continue\n }\n\n // Try to fetch CRL from this distribution point\n const result = await X509RevocationService.fetchAndVerifyCrl(\n agentContext,\n certificate,\n dp.urls,\n issuerCertificate,\n verificationDate,\n options\n )\n\n if (!result.success) {\n // CRL fetch failed - track the error but don't fail immediately\n // Another DP might cover the same reasons\n fetchErrors.push(`URLs ${dp.urls.join(', ')}: ${result.error}`)\n\n // A downloaded-but-rejected CRL is an integrity failure\n if (result.reachable) integrityFailure = true\n\n continue\n }\n\n // Successfully fetched CRL - mark these reasons as covered\n coveredReasons |= dpReasonMask\n fetchedDPs.push(result.usedUrl)\n\n // Keep the verified CRL so we can look up the certificate using the same\n // (format-normalizing) matching logic as the single distribution point path.\n coveredCrls.push(result.crl)\n\n // Check if we've covered all required reasons\n if ((coveredReasons & requiredReasonsMask) === requiredReasonsMask) {\n break\n }\n }\n\n // Step 3: Check if the certificate is revoked in any CRL we DID collect. Presence on a scoped\n // CRL is a definitive revocation regardless of whether every required reason partition was\n // obtained, so this must be checked BEFORE concluding anything about coverage completeness -\n // otherwise an unreachable partition could mask a revocation we already have proof of.\n // Use the CRL's own findRevoked so serial number formats are normalized consistently with the\n // single distribution point path.\n let revokedEntry: X509CertificateRevocationListEntry | null = null\n for (const crl of coveredCrls) {\n revokedEntry = crl.findRevoked(certificate)\n if (revokedEntry) break\n }\n\n if (revokedEntry) {\n return {\n isValid: false,\n isRevoked: true,\n error: new X509Error(\n `Certificate '${certificate.subject}' has been revoked. ` +\n `Revocation date: ${revokedEntry.revocationDate.toISOString()}` +\n (revokedEntry.reason !== undefined ? `, reason: ${revokedEntry.reason}` : '')\n ),\n details: `Revoked on ${revokedEntry.revocationDate.toISOString()}`,\n method: 'crl',\n }\n }\n\n // Step 4: The certificate was not found revoked. We can only conclude it is NOT revoked if we\n // actually covered all required revocation reasons; otherwise the status is indeterminate.\n if ((coveredReasons & requiredReasonsMask) !== requiredReasonsMask) {\n const missingReasons = requiredReasonsMask & ~coveredReasons\n const coveredNames = X509RevocationService.reasonMaskToNames(coveredReasons)\n const requiredNames = X509RevocationService.reasonMaskToNames(requiredReasonsMask)\n const missingNames = X509RevocationService.reasonMaskToNames(missingReasons)\n\n // Build detailed error message\n let errorMsg =\n `Unable to cover all required revocation reasons. ` +\n `Covered: [${coveredNames.join(', ')}], ` +\n `Required: [${requiredNames.join(', ')}], ` +\n `Missing: [${missingNames.join(', ')}]. ` +\n `Certificate revocation status cannot be fully determined.`\n\n // Include unsupported DP info if any missing reasons could have been covered by them\n let unsupportedCoversMissing = false\n for (const unsupported of unsupportedDPs) {\n if (unsupported.reasons & missingReasons) {\n unsupportedCoversMissing = true\n const unsupportedReasons = X509RevocationService.reasonMaskToNames(unsupported.reasons & missingReasons)\n errorMsg += ` Unsupported CRL feature ${unsupported.feature} covers missing reasons: [${unsupportedReasons.join(', ')}].`\n }\n }\n\n if (fetchErrors.length > 0) {\n errorMsg += ` Fetch errors: ${fetchErrors.join('; ')}`\n }\n\n // Check if the missing reasons are covered by any DP (even if we couldn't fetch its CRL):\n // if not, it's a structural gap that can't be soft-failed.\n const reasonsPublishedByAnyDp = X509RevocationService.calculateAllReasonsInDPs(distributionPoints)\n const structuralGap = (missingReasons & ~reasonsPublishedByAnyDp) !== 0\n\n // The coverage gap is soft-failable only when it is purely caused by CRLs we could not reach;\n // an integrity failure, an unsupported feature, or a structural gap is never soft-failed.\n const availabilityOnly = !integrityFailure && !unsupportedCoversMissing && !structuralGap\n\n throw availabilityOnly ? new X509CrlUnavailableError(errorMsg) : new X509Error(errorMsg)\n }\n\n // Certificate not found in any CRL and all required reasons covered - it's valid\n return {\n isValid: true,\n isRevoked: false,\n details: `Certificate not found in CRLs from: ${fetchedDPs.join(', ')}`,\n method: 'crl',\n }\n }\n\n /**\n * Calculate bitmask of revocation reasons covered by a distribution point\n */\n private static calculateReasonMask(reasons: number[] | undefined): number {\n if (!reasons || reasons.length === 0) {\n // No reasons specified means this DP covers ALL reasons\n return ALL_REVOCATION_REASONS_MASK\n }\n\n // Convert array of reason codes to bitmask\n let mask = 0\n for (const reason of reasons) {\n if (reason >= 0 && reason <= 8) {\n mask |= 1 << reason\n }\n }\n return mask\n }\n\n /**\n * Calculate the union of all reasons present in distribution points\n * Used to determine the default required reasons (no point requiring reasons the issuer never published)\n */\n private static calculateAllReasonsInDPs(\n distributionPoints: Array<{ urls: string[]; reasons?: number[]; crlIssuer?: string }>\n ): number {\n let allReasons = 0\n for (const dp of distributionPoints) {\n allReasons |= X509RevocationService.calculateReasonMask(dp.reasons)\n }\n return allReasons\n }\n\n /**\n * Convert a bitmask of revocation reasons to human-readable names\n */\n private static reasonMaskToNames(mask: number): string[] {\n const names: string[] = []\n const reasonNames: Record<number, string> = {\n [X509RevocationReason.Unused]: 'unused',\n [X509RevocationReason.KeyCompromise]: 'keyCompromise',\n [X509RevocationReason.CACompromise]: 'cACompromise',\n [X509RevocationReason.AffiliationChanged]: 'affiliationChanged',\n [X509RevocationReason.Superseded]: 'superseded',\n [X509RevocationReason.CessationOfOperation]: 'cessationOfOperation',\n [X509RevocationReason.CertificateHold]: 'certificateHold',\n [X509RevocationReason.PrivilegeWithdrawn]: 'privilegeWithdrawn',\n [X509RevocationReason.AACompromise]: 'aACompromise',\n }\n\n for (let i = 0; i <= 8; i++) {\n if (mask & (1 << i)) {\n names.push(reasonNames[i] ?? `unknown(${i})`)\n }\n }\n\n return names\n }\n\n /**\n * Check revocation status using a single distribution point (typically a \"full\" DP)\n */\n private static async checkDistributionPoint(\n agentContext: AgentContext,\n certificate: X509Certificate,\n issuerCertificate: X509Certificate,\n distributionPoint: { urls: string[]; reasons?: number[] },\n verificationDate: Date,\n options: X509RevocationCheckOptions\n ): Promise<X509RevocationCheckResult> {\n const result = await X509RevocationService.fetchAndVerifyCrl(\n agentContext,\n certificate,\n distributionPoint.urls,\n issuerCertificate,\n verificationDate,\n options\n )\n\n if (!result.success) {\n const message =\n `Failed to fetch CRL from distribution point (URLs: ${distributionPoint.urls.join(', ')}). ` +\n `Error: ${result.error}`\n\n throw result.reachable ? new X509Error(message) : new X509CrlUnavailableError(message)\n }\n\n // Check if certificate is in the CRL\n const revokedEntry = result.crl.findRevoked(certificate)\n\n if (revokedEntry) {\n return {\n isValid: false,\n isRevoked: true,\n error: new X509Error(\n `Certificate '${certificate.subject}' has been revoked. ` +\n `Revocation date: ${revokedEntry.revocationDate.toISOString()}` +\n (revokedEntry.reason !== undefined ? `, reason: ${revokedEntry.reason}` : '')\n ),\n details: `Revoked on ${revokedEntry.revocationDate.toISOString()}`,\n method: 'crl',\n }\n }\n\n // Certificate not found in CRL - it's valid\n return {\n isValid: true,\n isRevoked: false,\n details: `Certificate not found in CRL from ${result.usedUrl}`,\n method: 'crl',\n }\n }\n\n /**\n * Determine whether a verified CRL can be used to authoritatively determine the revocation status\n * of `certificate`. Uses the CRL's own extensions (RFC 5280 §5.2.4 Delta CRL Indicator and §5.2.5\n * Issuing Distribution Point) to reject CRLs we cannot correctly interpret as a complete, direct\n * CRL covering this certificate. Returns a human-readable reason when the CRL must be rejected.\n */\n private static checkCrlApplicability(\n crl: VerifiedCrl,\n certificate: X509Certificate\n ): { usable: true } | { usable: false; reason: string } {\n // RFC 5280 §6.3.3: a CRL bearing a critical extension we do not recognize MUST NOT be used, as\n // that extension may change the CRL's meaning (e.g. its scope) in ways we cannot account for.\n const criticalExtensions = validateCriticalCrlExtensionIds(crl.criticalExtensionIds)\n if (!criticalExtensions.isValid) {\n return {\n usable: false,\n reason: criticalExtensions.error?.message ?? 'CRL contains unrecognized critical extensions',\n }\n }\n\n // A delta CRL only lists changes relative to a base CRL. We do not process delta CRLs, so a\n // certificate's absence from one is not proof it is unrevoked: treating it as a complete CRL\n // could let a certificate revoked on the base CRL appear valid.\n if (crl.deltaCrlIndicator !== undefined) {\n return { usable: false, reason: 'CRL is a delta CRL, which is not supported for revocation checking' }\n }\n\n const issuingDistributionPoint = crl.issuingDistributionPoint\n if (issuingDistributionPoint) {\n // An indirect CRL may revoke certificates from issuers other than the one that signed it (via\n // the per-entry certificateIssuer extension), so its entries cannot be reliably attributed to\n // this certificate's issuer. We do not process indirect CRLs.\n if (issuingDistributionPoint.indirectCRL) {\n return { usable: false, reason: 'CRL is an indirect CRL, which is not supported for revocation checking' }\n }\n\n // This CRL only covers attribute certificates, never the public-key certificate being checked.\n if (issuingDistributionPoint.onlyContainsAttributeCerts) {\n return { usable: false, reason: 'CRL only covers attribute certificates' }\n }\n\n // The CRL is scoped to a certificate category that excludes the certificate being checked, so\n // its (non-)presence says nothing about this certificate's revocation status.\n const isCertificateAuthority = certificate.isCertificateAuthority\n if (issuingDistributionPoint.onlyContainsUserCerts && isCertificateAuthority) {\n return {\n usable: false,\n reason: 'CRL only covers end-entity certificates but the certificate is a CA certificate',\n }\n }\n if (issuingDistributionPoint.onlyContainsCACerts && !isCertificateAuthority) {\n return {\n usable: false,\n reason: 'CRL only covers CA certificates but the certificate is an end-entity certificate',\n }\n }\n }\n\n return { usable: true }\n }\n\n /**\n * Fetch and verify a CRL from a list of mirror URLs\n * Tries each URL in order until one succeeds\n */\n private static async fetchAndVerifyCrl(\n agentContext: AgentContext,\n certificate: X509Certificate,\n urls: string[],\n issuerCertificate: X509Certificate,\n verificationDate: Date,\n options: X509RevocationCheckOptions\n ): Promise<\n { success: true; crl: VerifiedCrl; usedUrl: string } | { success: false; error: string; reachable: boolean }\n > {\n let lastError: string | undefined\n\n // Whether at least one URL produced a downloaded CRL\n let reachable = false\n\n const webCrypto = new CredoWebCrypto(agentContext)\n\n // Try each URL (these are mirrors) until one succeeds\n for (const url of urls) {\n const result = await X509RevocationService.fetchVerifyAndCacheCrl(\n agentContext,\n { url, issuerCertificate, verificationDate, options, useCache: true },\n webCrypto\n )\n\n if (result.success) {\n // A verified CRL is only authoritative for this certificate if it is a complete, direct CRL\n // whose scope covers the certificate. Otherwise its (non-)presence proves nothing about the\n // certificate's revocation status, so it must not be used.\n const applicability = X509RevocationService.checkCrlApplicability(result.crl, certificate)\n if (applicability.usable) return { success: true, crl: result.crl, usedUrl: url }\n\n // The CRL was downloaded and verified, but cannot be used for this certificate. Treat it as\n // reachable so the coverage gap is classified as an integrity (hard) failure rather than a\n // soft-failable availability failure - we did obtain a CRL, it is just not one we can use.\n lastError = `CRL from ${url} cannot be used: ${applicability.reason}`\n reachable = true\n continue\n }\n\n lastError = result.error\n if (result.reachable) reachable = true\n }\n\n // All URLs failed\n return {\n success: false,\n error: lastError ?? `All CRL URLs failed: ${urls.join(', ')}`,\n reachable,\n }\n }\n\n /**\n * Obtain a verified CRL for a URL, preferring the cached verified summary.\n *\n * A summary is only reused when the provided issuer certificate carries the same subject name and\n * public key the summary was verified against, its key usage (when present) permits cRLSign, and\n * the summary's validity window covers `verificationDate` — together these reproduce exactly the\n * issuer-dependent checks of {@link X509CertificateRevocationList.verify}, so a renewed or\n * cross-certified issuer certificate (same name and key) reuses the summary directly. A reusable\n * summary that is still fresh (`staleAt` in the future) is then trusted from the\n * (application-controlled) cache without any network I/O; a stale one is revalidated on the full\n * path, which reuses it without re-parsing when the freshly fetched bytes are unchanged.\n *\n * Anything else — a different key or subject name, a key usage without cRLSign, an out-of-window,\n * missing or malformed summary — falls through to the full fetch-parse-verify path, which\n * produces all failure results (an unreachable host is then an availability failure like any\n * other fetch, also when revalidating a stale summary). On success the full path overwrites the\n * summary with the new issuer binding.\n */\n private static async fetchVerifyAndCacheCrl(\n agentContext: AgentContext,\n fetchOptions: FetchCrlOptions,\n webCrypto: CredoWebCrypto\n ): Promise<{ success: true; crl: VerifiedCrl } | { success: false; error: string; reachable: boolean }> {\n const { url, issuerCertificate, verificationDate, useCache } = fetchOptions\n\n if (useCache) {\n const summary = await getCachedCrlSummary(agentContext, url)\n const verificationTime = verificationDate.getTime()\n const issuerKeyUsage = issuerCertificate.keyUsage\n if (\n summary &&\n summary.issuerNameSha256 === computeIssuerNameSha256(issuerCertificate) &&\n summary.issuerPublicJwkThumbprint === computeIssuerPublicJwkThumbprint(issuerCertificate) &&\n // Mirror verify()'s cRLSign check: key usage, when present and non-empty, must include cRLSign\n !(issuerKeyUsage && issuerKeyUsage.length > 0 && !issuerKeyUsage.includes(X509KeyUsage.CrlSign)) &&\n // Mirror verify()'s validity window checks (isNotYetValid/isExpired use strict comparisons)\n verificationTime >= summary.thisUpdate &&\n (summary.nextUpdate === undefined || verificationTime <= summary.nextUpdate)\n ) {\n if (summary.staleAt > verificationTime) {\n return { success: true, crl: new CrlSummaryVerifiedCrl(summary) }\n }\n\n // Stale but otherwise reusable: revalidate on the full path. Only a summary that passed\n // every reuse gate may be revalidated by bytes hash — hash-based reuse must not resurrect\n // a summary bound to a different issuer or an uncovered validity window.\n return X509RevocationService.fetchParseVerifyAndCacheCrl(\n agentContext,\n { ...fetchOptions, staleSummary: summary },\n webCrypto\n )\n }\n }\n\n return X509RevocationService.fetchParseVerifyAndCacheCrl(agentContext, fetchOptions, webCrypto)\n }\n\n /**\n * Download a single CRL, parse it, and verify it against the issuer. When `useCache` is enabled,\n * a successful verification also caches a derived summary so subsequent checks can skip the\n * download and the (potentially very expensive) parse via {@link fetchVerifyAndCacheCrl}.\n *\n * `staleSummary` is a cached summary that passed all reuse gates but whose `staleAt` has passed:\n * when the fetched bytes hash to its `crlSha256`, the parse and verification are skipped and the\n * summary is re-cached with a refreshed `staleAt`.\n */\n private static async fetchParseVerifyAndCacheCrl(\n agentContext: AgentContext,\n {\n url,\n issuerCertificate,\n verificationDate,\n options,\n useCache,\n staleSummary,\n }: FetchCrlOptions & { staleSummary?: X509CrlSummary },\n webCrypto: CredoWebCrypto\n ): Promise<{ success: true; crl: VerifiedCrl } | { success: false; error: string; reachable: boolean }> {\n // Whether the CRL bytes were downloaded successfully.\n let reachable = false\n\n try {\n const crlData = await fetchCrl({\n url,\n timeoutMs: options.timeoutMs,\n maxSizeBytes: options.maxCrlSizeBytes,\n agentContext,\n })\n\n reachable = true\n\n // The globally configured expiry applies even when per-call revocation options omit it; the\n // hardcoded default only kicks in when neither is set.\n const crlCacheExpirySeconds =\n options.crlCacheExpirySeconds ??\n agentContext.dependencyManager.resolve(X509ModuleConfig).revocationCheck?.crlCacheExpirySeconds ??\n DEFAULT_CRL_CACHE_EXPIRY_SECONDS\n const staleAt = verificationDate.getTime() + crlCacheExpirySeconds * 1000\n\n if (staleSummary && computeCrlSha256(crlData) === staleSummary.crlSha256) {\n // Unchanged bytes: the stale summary was verified against exactly these bytes, so only its\n // freshness deadline needs refreshing.\n const refreshed: X509CrlSummary = { ...staleSummary, staleAt }\n await setCachedCrlSummary(\n agentContext,\n url,\n refreshed,\n X509RevocationService.computeCacheTtlSeconds(staleSummary.nextUpdate, verificationDate, crlCacheExpirySeconds)\n )\n\n return { success: true, crl: new CrlSummaryVerifiedCrl(refreshed) }\n }\n\n const crl = X509CertificateRevocationList.fromRaw(crlData)\n\n // Verify the CRL signature, issuer-name binding, and validity window against the issuer.\n const verifyResult = await crl.verify({ issuerCertificate, verificationDate }, webCrypto)\n if (!verifyResult.isValid) {\n return { success: false, error: verifyResult.error?.message ?? 'CRL verification failed', reachable }\n }\n\n if (useCache) {\n // The entry lives until the CRL's own `nextUpdate` so an expired CRL is never served from\n // the cache; freshness within that window is governed by the summary's `staleAt`.\n const ttlSeconds = X509RevocationService.computeCacheTtlSeconds(\n crl.nextUpdate?.getTime(),\n verificationDate,\n crlCacheExpirySeconds\n )\n\n // Cache the derived summary of every successfully verified CRL (fresh fetch or\n // re-verification against a different issuer certificate). Best-effort: a malformed CRL\n // (e.g. duplicate extensions) throws while deriving the summary; skip caching it so such\n // CRLs keep surfacing that error at applicability time.\n try {\n await setCachedCrlSummary(agentContext, url, buildCrlSummary(crl, issuerCertificate, staleAt), ttlSeconds)\n } catch {\n // the summary cache is an optimization only\n }\n }\n\n return { success: true, crl }\n } catch (error) {\n return { success: false, error: error instanceof Error ? error.message : String(error), reachable }\n }\n }\n\n /**\n * Compute the cache TTL (in seconds) for a verified CRL summary: the full time until its\n * `nextUpdate` (freshness within that window is governed by the summary's `staleAt`). Returns 0\n * when the CRL is already at/after `nextUpdate` so it is not cached. A CRL without `nextUpdate`\n * gets the freshness period itself as TTL, so its entry expires instead of being revalidated.\n */\n private static computeCacheTtlSeconds(\n nextUpdate: number | undefined,\n verificationDate: Date,\n crlCacheExpirySeconds: number\n ): number {\n if (nextUpdate === undefined) return crlCacheExpirySeconds\n\n return Math.max(0, Math.floor((nextUpdate - verificationDate.getTime()) / 1000))\n }\n\n /**\n * Handles errors during revocation checking based on the configured mode\n */\n private static handleRevocationError(error: unknown, mode: X509RevocationCheckMode): X509RevocationCheckResult {\n const errorMessage = error instanceof Error ? error.message : String(error)\n\n if (mode === X509RevocationCheckMode.Disabled) {\n return {\n isValid: true,\n details: 'Revocation checking disabled',\n }\n }\n\n // SoftFail only tolerates an availability failure (the CRL could not be obtained)\n if (mode === X509RevocationCheckMode.SoftFail && error instanceof X509CrlUnavailableError) {\n return {\n isValid: true,\n details: `CRL could not be obtained but SoftFail mode allowed it: ${errorMessage}`,\n }\n }\n\n // Remaining cases result in validation failure\n return {\n isValid: false,\n error: new X509Error(`Required revocation check failed: ${errorMessage}`, {\n cause: error instanceof Error ? error : undefined,\n }),\n }\n }\n\n /**\n * Fetch a CRL from a URL and parse it into an {@link X509CertificateRevocationList}.\n *\n * When `issuerCertificate` is provided, the CRL's signature, issuer name, issuer `cRLSign` key\n * usage, and validity window are verified, and an error is thrown on failure. When omitted, the CRL\n * is returned unverified.\n *\n * Note: this is a raw fetch+parse helper. Unlike {@link checkCertificateRevocation}, it does not\n * apply revocation-scope/applicability checks (delta/indirect/IDP scope), so it will return a delta\n * or scoped CRL as-is for inspection.\n */\n public static async fetchCertificateRevocationList(\n agentContext: AgentContext,\n {\n url,\n issuerCertificate,\n timeoutMs,\n maxCrlSizeBytes,\n verificationDate = new Date(),\n }: X509FetchCertificateRevocationListOptions\n ): Promise<X509CertificateRevocationList> {\n // Without an issuer certificate we cannot verify the CRL; just fetch and parse it.\n if (!issuerCertificate) {\n const crlData = await fetchCrl({ url, timeoutMs, maxSizeBytes: maxCrlSizeBytes, agentContext })\n return X509CertificateRevocationList.fromRaw(crlData)\n }\n\n const parsedIssuerCertificate =\n issuerCertificate instanceof X509Certificate\n ? issuerCertificate\n : X509Certificate.fromEncodedCertificate(issuerCertificate)\n\n const result = await X509RevocationService.fetchParseVerifyAndCacheCrl(\n agentContext,\n {\n url,\n issuerCertificate: parsedIssuerCertificate,\n verificationDate,\n options: { timeoutMs, maxCrlSizeBytes },\n // This is an explicit, one-off fetch; don't read from or populate the revocation cache.\n useCache: false,\n },\n new CredoWebCrypto(agentContext)\n )\n\n if (!result.success) {\n // Distinguish an availability failure (could not download) from an integrity failure\n // (downloaded but rejected), mirroring the revocation engine's classification.\n throw result.reachable\n ? new X509Error(`CRL from ${url} could not be verified: ${result.error}`)\n : new X509CrlUnavailableError(result.error)\n }\n\n // Without `staleSummary` (and with `useCache: false`) the full path always returns the freshly\n // parsed CRL, never a cached-summary view.\n return result.crl as X509CertificateRevocationList\n }\n}\n"],"mappings":"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"}