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