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

301 lines (300 loc) • 14.1 kB
import { TypedArrayEncoder } from "../../utils/TypedArrayEncoder.mjs"; import "../../utils/index.mjs"; import { asymmetricPublicJwkMatches } from "../kms/jwk/equals.mjs"; import "../kms/index.mjs"; import { CredoWebCryptoKey } from "../../crypto/webcrypto/CredoWebCryptoKey.mjs"; import { publicJwkToCryptoKeyAlgorithm, spkiToPublicJwk } from "../../crypto/webcrypto/utils/keyAlgorithmConversion.mjs"; import "../../crypto/webcrypto/utils/index.mjs"; import { CredoWebCrypto } from "../../crypto/webcrypto/CredoWebCrypto.mjs"; import "../../crypto/webcrypto/index.mjs"; import { X509Error } from "./X509Error.mjs"; import { convertName } from "./utils/nameConversion.mjs"; import { createAuthorityKeyIdentifierExtension, createBasicConstraintsExtension, createCrlDistributionPointsExtension, createExtendedKeyUsagesExtension, createIssuerAlternativeNameExtension, createKeyUsagesExtension, createSubjectAlternativeNameExtension, createSubjectKeyIdentifierExtension } from "./utils/extensions.mjs"; import { x509SignatureAlgorithmToJwa } from "./utils/signatureAlgorithm.mjs"; import "./utils/index.mjs"; import { AsnParser } from "@peculiar/asn1-schema"; import { SubjectPublicKeyInfo } from "@peculiar/asn1-x509"; import * as x509 from "@peculiar/x509"; //#region src/modules/x509/X509Certificate.ts let X509KeyUsage = /* @__PURE__ */ function(X509KeyUsage) { X509KeyUsage[X509KeyUsage["DigitalSignature"] = 1] = "DigitalSignature"; X509KeyUsage[X509KeyUsage["NonRepudiation"] = 2] = "NonRepudiation"; X509KeyUsage[X509KeyUsage["KeyEncipherment"] = 4] = "KeyEncipherment"; X509KeyUsage[X509KeyUsage["DataEncipherment"] = 8] = "DataEncipherment"; X509KeyUsage[X509KeyUsage["KeyAgreement"] = 16] = "KeyAgreement"; X509KeyUsage[X509KeyUsage["KeyCertSign"] = 32] = "KeyCertSign"; X509KeyUsage[X509KeyUsage["CrlSign"] = 64] = "CrlSign"; X509KeyUsage[X509KeyUsage["EncipherOnly"] = 128] = "EncipherOnly"; X509KeyUsage[X509KeyUsage["DecipherOnly"] = 256] = "DecipherOnly"; return X509KeyUsage; }({}); let X509ExtendedKeyUsage = /* @__PURE__ */ function(X509ExtendedKeyUsage) { X509ExtendedKeyUsage["ServerAuth"] = "1.3.6.1.5.5.7.3.1"; X509ExtendedKeyUsage["ClientAuth"] = "1.3.6.1.5.5.7.3.2"; X509ExtendedKeyUsage["CodeSigning"] = "1.3.6.1.5.5.7.3.3"; X509ExtendedKeyUsage["EmailProtection"] = "1.3.6.1.5.5.7.3.4"; X509ExtendedKeyUsage["TimeStamping"] = "1.3.6.1.5.5.7.3.8"; X509ExtendedKeyUsage["OcspSigning"] = "1.3.6.1.5.5.7.3.9"; X509ExtendedKeyUsage["MdlDs"] = "1.0.18013.5.1.2"; return X509ExtendedKeyUsage; }({}); var X509Certificate = class X509Certificate { constructor(options) { this.publicJwk = options.publicJwk; this.privateKey = options.privateKey; this.x509Certificate = options.x509Certificate; } set keyId(keyId) { this.publicJwk.keyId = keyId; } get keyId() { return this.publicJwk.keyId; } get hasKeyId() { return this.publicJwk.hasKeyId; } static fromRawCertificate(rawCertificate) { const certificate = new x509.X509Certificate(rawCertificate); return X509Certificate.parseCertificate(certificate); } static fromEncodedCertificate(encodedCertificate) { const certificate = new x509.X509Certificate(encodedCertificate); return X509Certificate.parseCertificate(certificate); } static parseCertificate(certificate) { const spki = AsnParser.parse(certificate.publicKey.rawData, SubjectPublicKeyInfo); const privateKey = certificate.privateKey ? new Uint8Array(certificate.privateKey.rawData) : void 0; const publicJwk = spkiToPublicJwk(spki); return new X509Certificate({ publicJwk, privateKey, x509Certificate: certificate }); } getMatchingExtensions(objectIdentifier) { return this.x509Certificate.extensions.filter((e) => e.type === objectIdentifier); } get rawCertificate() { return new Uint8Array(this.x509Certificate.rawData); } get subjectAlternativeNames() { return this.getMatchingExtensions("2.5.29.17")?.flatMap((s) => s.names.items).map((i) => ({ type: i.type, value: i.value })) ?? []; } get issuerAlternativeNames() { return this.getMatchingExtensions("2.5.29.18")?.flatMap((i) => i.names.items).map((i) => ({ type: i.type, value: i.value })) ?? []; } get sanDnsNames() { return this.subjectAlternativeNames.filter((san) => san.type === "dns").map((san) => san.value); } get sanUriNames() { return this.subjectAlternativeNames.filter((ian) => ian.type === "url").map((ian) => ian.value); } get ianDnsNames() { return this.issuerAlternativeNames.filter((san) => san.type === "dns").map((san) => san.value); } get ianUriNames() { return this.issuerAlternativeNames.filter((ian) => ian.type === "url").map((ian) => ian.value); } get authorityKeyIdentifier() { const keyIds = this.getMatchingExtensions("2.5.29.35")?.map((e) => e.keyId); if (keyIds && keyIds.length > 1) throw new X509Error("Multiple Authority Key Identifiers are not allowed"); return keyIds?.[0]; } get subjectKeyIdentifier() { const keyIds = this.getMatchingExtensions("2.5.29.14")?.map((e) => e.keyId); if (keyIds && keyIds.length > 1) throw new X509Error("Multiple Subject Key Identifiers are not allowed"); return keyIds?.[0]; } get keyUsage() { const keyUsages = this.getMatchingExtensions("2.5.29.15")?.map((e) => e.usages); if (keyUsages && keyUsages.length > 1) throw new X509Error("Multiple Key Usages are not allowed"); if (keyUsages) return Object.values(X509KeyUsage).filter((key) => typeof key === "number").filter((flagValue) => (keyUsages[0] & flagValue) === flagValue).map((flagValue) => flagValue); } get extendedKeyUsage() { const extendedKeyUsages = this.getMatchingExtensions("2.5.29.37")?.map((e) => e.usages); if (extendedKeyUsages && extendedKeyUsages.length > 1) throw new X509Error("Multiple Key Usages are not allowed"); return extendedKeyUsages?.[0] ?? []; } /** * Whether this certificate is a certificate authority (the Basic Constraints `cA` flag is set). * Returns `false` when the Basic Constraints extension is absent (e.g. an end-entity certificate). */ get isCertificateAuthority() { const basicConstraints = this.getMatchingExtensions("2.5.29.19"); if (basicConstraints && basicConstraints.length > 1) throw new X509Error("Multiple Basic Constraints extensions are not allowed"); return basicConstraints?.[0]?.ca ?? false; } /** * Get CRL Distribution Points with full structure including URLs, reasons, and CRL issuer. * Based on RFC 5280 Section 4.2.1.13 * * NOTE: only the URI form is surfaced for both the distribution point name (fullName) and the * cRLIssuer; nameRelativeToCRLIssuer and non-URI general names are ignored. Indirect CRLs * (those with a cRLIssuer) are surfaced here but are not verified during revocation checking. */ get crlDistributionPoints() { const crlDistributionPointsExt = this.getMatchingExtensions("2.5.29.31"); if (crlDistributionPointsExt && crlDistributionPointsExt.length > 1) throw new X509Error("Multiple CRL Distribution Points extensions are not allowed"); const ext = crlDistributionPointsExt?.[0]; if (!ext) return []; const distributionPoints = ext.distributionPoints; if (!distributionPoints || distributionPoints.length === 0) return []; const result = []; for (const dp of distributionPoints) { const urls = []; if (dp.distributionPoint?.fullName) { for (const generalName of dp.distributionPoint.fullName) if (generalName.uniformResourceIdentifier) urls.push(generalName.uniformResourceIdentifier); } if (urls.length === 0) continue; let reasons; if (dp.reasons !== void 0 && dp.reasons !== null) { const reasonBits = dp.reasons.toNumber(); reasons = []; for (let i = 0; i <= 8; i++) if ((reasonBits & 1 << i) !== 0) reasons.push(i); } let crlIssuer; if (dp.cRLIssuer) { for (const generalName of dp.cRLIssuer) if (generalName.uniformResourceIdentifier) { crlIssuer = generalName.uniformResourceIdentifier; break; } } result.push({ urls, reasons, crlIssuer }); } return result; } isExtensionCritical(id) { const extension = this.getMatchingExtensions(id); if (!extension) throw new X509Error(`extension with id '${id}' is not found`); return !!extension[0].critical; } static async create(options, webCrypto) { const subjectPublicKey = options.subjectPublicKey ?? options.authorityKey; const isSelfSignedCertificate = asymmetricPublicJwkMatches(options.authorityKey.toJson(), subjectPublicKey.toJson()); const signingKey = new CredoWebCryptoKey(options.authorityKey, publicJwkToCryptoKeyAlgorithm(options.authorityKey), false, "private", ["sign"]); const publicKey = new CredoWebCryptoKey(subjectPublicKey, publicJwkToCryptoKeyAlgorithm(options.authorityKey), true, "public", ["verify"]); const issuerName = convertName(options.issuer); const extensions = []; extensions.push(createSubjectKeyIdentifierExtension(options.extensions?.subjectKeyIdentifier, { publicJwk: subjectPublicKey })); extensions.push(createKeyUsagesExtension(options.extensions?.keyUsage)); extensions.push(createExtendedKeyUsagesExtension(options.extensions?.extendedKeyUsage)); extensions.push(createAuthorityKeyIdentifierExtension(options.extensions?.authorityKeyIdentifier, { publicJwk: options.authorityKey })); extensions.push(createIssuerAlternativeNameExtension(options.extensions?.issuerAlternativeName)); extensions.push(createSubjectAlternativeNameExtension(options.extensions?.subjectAlternativeName)); extensions.push(createBasicConstraintsExtension(options.extensions?.basicConstraints)); extensions.push(createCrlDistributionPointsExtension(options.extensions?.crlDistributionPoints)); if (isSelfSignedCertificate) { if (options.subject) throw new X509Error("Do not provide a subject name when the certificate is supposed to be self signed"); const certificate = await x509.X509CertificateGenerator.createSelfSigned({ keys: { publicKey, privateKey: signingKey }, name: issuerName, notBefore: options.validity?.notBefore, notAfter: options.validity?.notAfter, extensions: extensions.filter((e) => e !== void 0), serialNumber: options.serialNumber }, webCrypto); const certificateInstance = X509Certificate.parseCertificate(certificate); if (subjectPublicKey.hasKeyId) certificateInstance.publicJwk.keyId = subjectPublicKey.keyId; return certificateInstance; } if (!options.subject) throw new X509Error("Provide a subject name when the certificate is not supposed to be self signed"); const subjectName = convertName(options.subject); const certificate = await x509.X509CertificateGenerator.create({ signingKey, publicKey, issuer: issuerName, subject: subjectName, notBefore: options.validity?.notBefore, notAfter: options.validity?.notAfter, extensions: extensions.filter((e) => e !== void 0), serialNumber: options.serialNumber }, webCrypto); const certificateInstance = X509Certificate.parseCertificate(certificate); if (subjectPublicKey.hasKeyId) certificateInstance.publicJwk.keyId = subjectPublicKey.keyId; return certificateInstance; } get subject() { return this.x509Certificate.subject; } /** * The DER-encoded subject distinguished name. Use this for exact, encoding-stable name comparison * (e.g. binding a CRL to its issuer) rather than the string form returned by {@link subject}. */ get subjectNameBytes() { return new Uint8Array(this.x509Certificate.subjectName.toArrayBuffer()); } get issuer() { return this.x509Certificate.issuer; } async verify({ verificationDate = /* @__PURE__ */ new Date(), publicJwk, skipSignatureVerification = false }, webCrypto) { let publicCryptoKey; if (publicJwk) { const cryptoKeyAlgorithm = publicJwkToCryptoKeyAlgorithm(publicJwk, { alg: publicJwk.kty === "RSA" ? x509SignatureAlgorithmToJwa(this.x509Certificate.signatureAlgorithm) : void 0 }); publicCryptoKey = new CredoWebCryptoKey(publicJwk, cryptoKeyAlgorithm, true, "public", ["verify"]); } const isSignatureValid = skipSignatureVerification ? true : await this.x509Certificate.verify({ signatureOnly: true, publicKey: publicCryptoKey }, webCrypto); const time = verificationDate.getTime(); const isNotBeforeValid = this.x509Certificate.notBefore.getTime() <= time; const isNotAfterValid = time <= this.x509Certificate.notAfter.getTime(); if (!isSignatureValid) throw new X509Error(`Certificate: '${this.x509Certificate.subject}' has an invalid signature`); if (!isNotBeforeValid) throw new X509Error(`Certificate: '${this.x509Certificate.subject}' used before it is allowed`); if (!isNotAfterValid) throw new X509Error(`Certificate: '${this.x509Certificate.subject}' used after it is allowed`); } /** * Get the thumbprint of the X509 certificate in hex format. */ async getThumbprintInHex(agentContext) { const thumbprint = await this.x509Certificate.getThumbprint(new CredoWebCrypto(agentContext)); return TypedArrayEncoder.toHex(new Uint8Array(thumbprint)); } /** * Get the data elements of the x509 certificate */ get data() { return { issuerName: this.x509Certificate.issuerName.toString(), issuer: this.x509Certificate.issuer, subjectName: this.x509Certificate.subjectName.toString(), subject: this.x509Certificate.subject, serialNumber: this.x509Certificate.serialNumber, pem: this.x509Certificate.toString(), notBefore: this.x509Certificate.notBefore, notAfter: this.x509Certificate.notAfter }; } getIssuerNameField(field) { return this.x509Certificate.issuerName.getField(field); } getSubjectNameField(field) { return this.x509Certificate.subjectName.getField(field); } /** * @param format the format to export to, defaults to `pem` */ toString(format) { return this.x509Certificate.toString(format ?? "pem"); } equal(certificate) { const parsedOther = new x509.X509Certificate(certificate.rawCertificate); return this.x509Certificate.equal(parsedOther); } }; //#endregion export { X509Certificate, X509ExtendedKeyUsage, X509KeyUsage }; //# sourceMappingURL=X509Certificate.mjs.map