@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
JavaScript
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