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

305 lines (304 loc) • 13 kB
import { CredoWebCryptoKey } from "../../crypto/webcrypto/CredoWebCryptoKey.mjs"; import { jwaAlgorithmToKeySignParams } from "../../crypto/webcrypto/types.mjs"; import { publicJwkToCryptoKeyAlgorithm } from "../../crypto/webcrypto/utils/keyAlgorithmConversion.mjs"; import "../../crypto/webcrypto/index.mjs"; import { X509Error } from "./X509Error.mjs"; import { convertName } from "./utils/nameConversion.mjs"; import { createAuthorityKeyIdentifierExtension, createCrlEntryCertificateIssuerExtension, createCrlNumberExtension, createDeltaCrlIndicatorExtension, createIssuingDistributionPointExtension } from "./utils/extensions.mjs"; import { x509SignatureAlgorithmToJwa } from "./utils/signatureAlgorithm.mjs"; import "./utils/index.mjs"; import "./X509Certificate.mjs"; import { normalizeSerialNumber } from "./utils/serialNumber.mjs"; import { AsnParser } from "@peculiar/asn1-schema"; import { BaseCRLNumber, CRLNumber, IssuingDistributionPoint } from "@peculiar/asn1-x509"; import * as x509 from "@peculiar/x509"; //#region src/modules/x509/X509CertificateRevocationList.ts /** * Reason a certificate was revoked, as carried in a CRL entry's `reasonCode` extension * (RFC 5280 §5.3.1 `CRLReason`). */ let X509CertificateRevocationListEntryReason = /* @__PURE__ */ function(X509CertificateRevocationListEntryReason) { X509CertificateRevocationListEntryReason[X509CertificateRevocationListEntryReason["Unused"] = 0] = "Unused"; X509CertificateRevocationListEntryReason[X509CertificateRevocationListEntryReason["KeyCompromise"] = 1] = "KeyCompromise"; X509CertificateRevocationListEntryReason[X509CertificateRevocationListEntryReason["CACompromise"] = 2] = "CACompromise"; X509CertificateRevocationListEntryReason[X509CertificateRevocationListEntryReason["AffiliationChanged"] = 3] = "AffiliationChanged"; X509CertificateRevocationListEntryReason[X509CertificateRevocationListEntryReason["Superseded"] = 4] = "Superseded"; X509CertificateRevocationListEntryReason[X509CertificateRevocationListEntryReason["CessationOfOperation"] = 5] = "CessationOfOperation"; X509CertificateRevocationListEntryReason[X509CertificateRevocationListEntryReason["CertificateHold"] = 6] = "CertificateHold"; X509CertificateRevocationListEntryReason[X509CertificateRevocationListEntryReason["RemoveFromCrl"] = 8] = "RemoveFromCrl"; X509CertificateRevocationListEntryReason[X509CertificateRevocationListEntryReason["PrivilegeWithdrawn"] = 9] = "PrivilegeWithdrawn"; X509CertificateRevocationListEntryReason[X509CertificateRevocationListEntryReason["AACompromise"] = 10] = "AACompromise"; return X509CertificateRevocationListEntryReason; }({}); /** * Byte-for-byte equality of two `Uint8Array`s. */ function uint8ArraysEqual(a, b) { if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false; return true; } /** * Wrapper class for X.509 Certificate Revocation List (CRL) * Provides a clean interface over the @peculiar/x509 CRL implementation */ var X509CertificateRevocationList = class X509CertificateRevocationList { constructor(crl) { this.crl = crl; } /** * Parse a CRL from raw bytes */ static fromRaw(rawCrl) { try { const crl = new x509.X509Crl(rawCrl); return new X509CertificateRevocationList(crl); } catch (error) { throw new X509Error("Failed to parse CRL", { cause: error instanceof Error ? error : void 0 }); } } /** * Parse a CRL from PEM or base64 encoded string */ static fromEncoded(encodedCrl) { try { const crl = new x509.X509Crl(encodedCrl); return new X509CertificateRevocationList(crl); } catch (error) { throw new X509Error("Failed to parse encoded CRL", { cause: error instanceof Error ? error : void 0 }); } } /** * Create and sign a new CRL. * * Supports the CRL Number, Authority Key Identifier and Issuing Distribution Point extensions, as * well as indirect CRLs (per-entry `certificateIssuer`). See `options.extensions` / entry options. * * NOTE: indirect CRLs and CRLs whose Issuing Distribution Point scope does not cover the * certificate being checked are rejected during revocation checking (delta CRLs are likewise not * processed), so such CRLs are not treated as authoritative proof that a certificate is unrevoked. */ static async create(options, webCrypto) { const signingKey = new CredoWebCryptoKey(options.authorityKey, publicJwkToCryptoKeyAlgorithm(options.authorityKey), false, "private", ["sign"]); const entries = options.entries?.map((entry) => { const serialNumber = entry.serialNumber ?? entry.certificate?.data.serialNumber; if (!serialNumber) throw new X509Error("A CRL entry must provide either a serialNumber or a certificate"); return { serialNumber, revocationDate: entry.revocationDate, reason: entry.reason === void 0 ? void 0 : entry.reason, extensions: entry.issuer === void 0 ? void 0 : [createCrlEntryCertificateIssuerExtension(entry.issuer)] }; }); const extensions = [ createCrlNumberExtension(options.extensions?.crlNumber), createDeltaCrlIndicatorExtension(options.extensions?.deltaCrlIndicator), createAuthorityKeyIdentifierExtension(options.extensions?.authorityKeyIdentifier, { publicJwk: options.authorityKey }), createIssuingDistributionPointExtension(options.extensions?.issuingDistributionPoint) ]; const crl = await x509.X509CrlGenerator.create({ issuer: convertName(options.issuer), thisUpdate: options.validity?.thisUpdate, nextUpdate: options.validity?.nextUpdate, signingKey, signingAlgorithm: jwaAlgorithmToKeySignParams(options.authorityKey.signatureAlgorithm), entries, extensions: extensions.filter((extension) => extension !== void 0) }, webCrypto); return new X509CertificateRevocationList(crl); } /** * Get the raw CRL data */ get rawCertificateRevocationList() { return new Uint8Array(this.crl.rawData); } /** * Get the issuer of the CRL */ get issuer() { return this.crl.issuer; } /** * Get the date this CRL was issued */ get thisUpdate() { return this.crl.thisUpdate; } /** * Get the date when the next CRL will be published */ get nextUpdate() { return this.crl.nextUpdate; } /** * Check if the CRL has expired (past nextUpdate date) */ isExpired(date = /* @__PURE__ */ new Date()) { return this.nextUpdate ? date > this.nextUpdate : false; } /** * Check if the CRL is not yet valid (before its thisUpdate date). */ isNotYetValid(date = /* @__PURE__ */ new Date()) { return date < this.thisUpdate; } getMatchingExtensions(objectIdentifier) { return this.crl.extensions.filter((e) => e.type === objectIdentifier); } /** * CRL Number (RFC 5280 §5.2.3): the monotonically increasing sequence number of this CRL. */ get crlNumber() { const extensions = this.getMatchingExtensions("2.5.29.20"); if (extensions.length > 1) throw new X509Error("Multiple CRL Number extensions are not allowed"); const extension = extensions[0]; if (!extension) return void 0; return AsnParser.parse(extension.value, CRLNumber).value; } /** * Delta CRL Indicator (RFC 5280 §5.2.4): when present, this CRL is a delta CRL and the value is * the CRL Number of the base (complete) CRL it is relative to. `undefined` for a complete CRL. */ get deltaCrlIndicator() { const extensions = this.getMatchingExtensions("2.5.29.27"); if (extensions.length > 1) throw new X509Error("Multiple Delta CRL Indicator extensions are not allowed"); const extension = extensions[0]; if (!extension) return void 0; return AsnParser.parse(extension.value, BaseCRLNumber).value; } /** * Issuing Distribution Point (RFC 5280 §5.2.5), describing the scope of this CRL. * * NOTE: only the URI form of the distribution point name is surfaced; other GeneralName forms and * `nameRelativeToCRLIssuer` are ignored. */ get issuingDistributionPoint() { const extensions = this.getMatchingExtensions("2.5.29.28"); if (extensions.length > 1) throw new X509Error("Multiple Issuing Distribution Point extensions are not allowed"); const extension = extensions[0]; if (!extension) return void 0; const idp = AsnParser.parse(extension.value, IssuingDistributionPoint); const fullName = []; if (idp.distributionPoint?.fullName) { for (const generalName of idp.distributionPoint.fullName) if (generalName.uniformResourceIdentifier) fullName.push(generalName.uniformResourceIdentifier); } let onlySomeReasons; if (idp.onlySomeReasons) { const reasonBits = idp.onlySomeReasons.toNumber(); onlySomeReasons = []; for (let i = 0; i <= 8; i++) if ((reasonBits & 1 << i) !== 0) onlySomeReasons.push(i); } return { fullName, onlyContainsUserCerts: idp.onlyContainsUserCerts, onlyContainsCACerts: idp.onlyContainsCACerts, onlySomeReasons, indirectCRL: idp.indirectCRL, onlyContainsAttributeCerts: idp.onlyContainsAttributeCerts }; } /** * The object identifiers of all critical extensions on this CRL. */ get criticalExtensionIds() { return this.crl.extensions.filter((extension) => extension.critical).map((extension) => extension.type); } /** * Whether the extension with the given id is marked critical. Throws if the extension is absent. */ isExtensionCritical(id) { const extensions = this.getMatchingExtensions(id); if (extensions.length === 0) throw new X509Error(`extension with id '${id}' is not found`); return !!extensions[0].critical; } /** * Verify this CRL with the issuer's certificate. * * NOTE: the CRL summary cache fast path in `X509RevocationService` reproduces the * issuer-certificate-dependent checks below (public key, subject name, cRLSign key usage). If a * new check depending on the issuer certificate is added here, update that gate in tandem. */ async verify({ issuerCertificate, verificationDate: now = /* @__PURE__ */ new Date() }, webCrypto) { try { const publicJwk = issuerCertificate.publicJwk; const cryptoKeyAlgorithm = publicJwkToCryptoKeyAlgorithm(publicJwk, { alg: publicJwk.kty === "RSA" ? x509SignatureAlgorithmToJwa(this.crl.signatureAlgorithm) : void 0 }); const publicCryptoKey = new CredoWebCryptoKey(publicJwk, cryptoKeyAlgorithm, true, "public", ["verify"]); if (!await this.crl.verify({ publicKey: publicCryptoKey }, webCrypto)) return { isValid: false, error: new X509Error("CRL signature verification failed") }; } catch (error) { return { isValid: false, error: new X509Error("CRL signature verification failed", { cause: error instanceof Error ? error : void 0 }) }; } if (!uint8ArraysEqual(new Uint8Array(this.crl.issuerName.toArrayBuffer()), issuerCertificate.subjectNameBytes)) return { isValid: false, error: new X509Error(`CRL issuer '${this.issuer}' does not match certificate issuer '${issuerCertificate.subject}'`) }; const issuerKeyUsage = issuerCertificate.keyUsage; if (issuerKeyUsage && issuerKeyUsage.length > 0 && !issuerKeyUsage.includes(64)) return { isValid: false, error: new X509Error(`CRL issuer '${issuerCertificate.subject}' is not authorized to sign CRLs (key usage does not include cRLSign)`) }; if (this.isNotYetValid(now)) return { isValid: false, error: new X509Error(`CRL is not yet valid (thisUpdate: ${this.thisUpdate.toISOString()})`) }; if (this.isExpired(now)) return { isValid: false, error: new X509Error(`CRL has expired (nextUpdate: ${this.nextUpdate?.toISOString() ?? "unknown"})`) }; return { isValid: true }; } /** * Check if a certificate is revoked in this CRL. * * We compare serial numbers ourselves (normalizing case and leading zeros) instead of * delegating to `@peculiar/x509`'s `X509Crl.findRevoked`. The latter eagerly resolves the * global crypto provider (even though it isn't needed for a serial number comparison), which * throws when no provider has been registered globally. */ findRevoked(certificate) { const target = normalizeSerialNumber(certificate.data.serialNumber); for (const entry of this.crl.entries) if (normalizeSerialNumber(entry.serialNumber) === target) return { serialNumber: entry.serialNumber, revocationDate: entry.revocationDate, reason: entry.reason }; return null; } /** * Get all revoked certificates in this CRL */ get revokedCertificates() { const revoked = []; for (const entry of this.crl.entries) revoked.push({ serialNumber: entry.serialNumber, revocationDate: entry.revocationDate, reason: entry.reason }); return revoked; } /** * Get the number of revoked certificates in this CRL */ get revokedCount() { return this.crl.entries.length; } /** * @param format the format to export to, defaults to `pem` */ toString(format) { return this.crl.toString(format ?? "pem"); } equal(crl) { const parsedOther = new x509.X509Crl(crl.rawCertificateRevocationList); return this.crl.equal(parsedOther); } }; //#endregion export { X509CertificateRevocationList, X509CertificateRevocationListEntryReason }; //# sourceMappingURL=X509CertificateRevocationList.mjs.map