@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
251 lines (250 loc) • 12.8 kB
JavaScript
import { injectable } from "../../plugins/index.mjs";
import __decorate from "../../_virtual/_@oxc-project_runtime@0.151.0/helpers/esm/decorate.mjs";
import "../../agent/index.mjs";
import { CredoWebCrypto } from "../../crypto/webcrypto/CredoWebCrypto.mjs";
import "../../crypto/webcrypto/index.mjs";
import { X509Error, X509ValidationError } from "./X509Error.mjs";
import "./utils/extensions.mjs";
import "./utils/index.mjs";
import { X509Certificate } from "./X509Certificate.mjs";
import "./X509ValidationOptions.mjs";
import { X509ModuleConfig } from "./X509ModuleConfig.mjs";
import { CertificateSigningRequest } from "./CertificateSigningRequest.mjs";
import { validateCriticalExtensionsForChain } from "./utils/criticalExtensions.mjs";
import { X509CertificateRevocationList } from "./X509CertificateRevocationList.mjs";
import { X509RevocationService } from "./X509RevocationService.mjs";
import * as x509 from "@peculiar/x509";
//#region src/modules/x509/X509Service.ts
var _X509Service;
let X509Service = _X509Service = class X509Service {
/**
*
* Validate a chain of X.509 certificates according to RFC 5280
*
* This function requires a list of base64 encoded certificates and, optionally, a certificate that should be found in the chain.
* If no certificate is provided, it will just assume the leaf certificate
*
* The leaf certificate should be the 0th index and the root the last
*
* The Issuer of the certificate is found with the following algorithm:
* - Check if there is an AuthorityKeyIdentifierExtension
* - Go through all the other certificates and see if the SubjectKeyIdentifier is equal to the AuthorityKeyIdentifier
* - If they are equal, the certificate signature is verified and returned as the issuer
*
* Validation checks performed:
* - Chain validation (structure, signatures, validity, trust anchor)
* - Basic constraints are valid for CA certificates
* - Key usage of issuing CA certificates includes `keyCertSign` (when the extension is present)
* - Path length constraints are satisfied
* - All critical extensions are understood and valid
* - Certificates have not been revoked (CRL) - if enabled
*
* Limitations (RFC 5280 features not enforced here):
* - Revocation is checked via CRL only; OCSP is not supported.
* - Name Constraints (§4.2.1.10), Policy Constraints (§4.2.1.11), and Certificate Policies
* (§4.2.1.4) are not enforced. A certificate carrying one of these as a *critical* extension is
* rejected (unrecognized critical extension); a non-critical occurrence is ignored.
* - Extended Key Usage is parsed but not enforced.
* - Key Usage is only enforced when the extension is present (it is optional; absent means
* unrestricted).
* - Subject/Issuer Alternative Names only surface the URI and DNS GeneralName forms.
*/
static async validateCertificateChain(agentContext, { certificateChain, certificate = certificateChain[0], verificationDate = /* @__PURE__ */ new Date(), trustedCertificates, allowNonRootTrustedCertificate = true }) {
const validations = {};
const config = agentContext.dependencyManager.resolve(X509ModuleConfig);
let parsedChain;
try {
if (certificateChain.length === 0) throw new X509Error("Certificate chain is empty");
const webCrypto = new CredoWebCrypto(agentContext);
let parsedLeafCertificate;
let certificatesToBuildChain;
try {
parsedLeafCertificate = new x509.X509Certificate(certificate);
certificatesToBuildChain = [...certificateChain, ...trustedCertificates ?? []].map((c) => new x509.X509Certificate(c));
} catch (error) {
throw new X509Error("Error during parsing of x509 certificate", { cause: error });
}
parsedChain = (await new x509.X509ChainBuilder({ certificates: certificatesToBuildChain }).build(parsedLeafCertificate, webCrypto)).map((c) => X509Certificate.fromRawCertificate(new Uint8Array(c.rawData))).reverse();
if (parsedChain.length < certificateChain.length) throw new X509Error("Could not parse the full chain. Likely due to incorrect ordering");
let previousCertificate;
if (trustedCertificates) {
const parsedTrustedCertificates = trustedCertificates.map((trustedCertificate) => X509Certificate.fromEncodedCertificate(trustedCertificate));
const trustedCertificateIndex = parsedChain.findIndex((cert) => parsedTrustedCertificates.some((tCert) => cert.equal(tCert)));
if (trustedCertificateIndex === -1) throw new X509Error("No trusted certificate was found while validating the X.509 chain");
if (trustedCertificateIndex > 0) {
previousCertificate = parsedChain[trustedCertificateIndex - 1];
parsedChain = parsedChain.slice(trustedCertificateIndex);
}
}
for (let i = 0; i < parsedChain.length; i++) {
const cert = parsedChain[i];
const publicJwk = previousCertificate ? previousCertificate.publicJwk : void 0;
const skipSignatureVerification = i === 0 && trustedCertificates && !publicJwk && allowNonRootTrustedCertificate;
if (i === 0 && trustedCertificates && !publicJwk && !allowNonRootTrustedCertificate) throw new X509Error("Unable to verify the certificate chain. A trusted non-self-signed certificate is the first certificate in the chain, but no parent certificate was found to verify its signature. Either provide the parent certificate in trustedCertificates, or set allowNonRootTrustedCertificate to true to allow trusting intermediate certificates without signature verification.");
await cert.verify({
publicJwk,
verificationDate,
skipSignatureVerification
}, webCrypto);
previousCertificate = cert;
}
validations.chain = { isValid: true };
} catch (error) {
const x509Error = error instanceof X509Error ? error : new X509Error("Chain validation failed", { cause: error });
validations.chain = {
isValid: false,
error: x509Error
};
throw new X509ValidationError(x509Error.message, {
isValid: false,
validations,
error: x509Error
});
}
validations.basicConstraints = _X509Service.validateBasicConstraintsForChain(parsedChain);
if (!validations.basicConstraints.isValid) throw new X509ValidationError(validations.basicConstraints.error?.message ?? "Basic constraints validation failed", {
isValid: false,
validations,
error: validations.basicConstraints.error
});
validations.keyUsage = _X509Service.validateKeyUsageForChain(parsedChain);
if (!validations.keyUsage.isValid) throw new X509ValidationError(validations.keyUsage.error?.message ?? "Key usage validation failed", {
isValid: false,
validations,
error: validations.keyUsage.error
});
validations.pathLength = _X509Service.validatePathLengthConstraints(parsedChain);
if (!validations.pathLength.isValid) throw new X509ValidationError(validations.pathLength.error?.message ?? "Path length constraint validation failed", {
isValid: false,
validations,
error: validations.pathLength.error
});
const peculiarChain = parsedChain.map((cert) => new x509.X509Certificate(cert.rawCertificate));
validations.criticalExtensions = validateCriticalExtensionsForChain(peculiarChain);
if (!validations.criticalExtensions.isValid) throw new X509ValidationError(validations.criticalExtensions.error?.message ?? "Critical extension validation failed", {
isValid: false,
validations,
error: validations.criticalExtensions.error
});
if (config.revocationCheck && config.revocationCheck.mode !== "Disabled") {
validations.revocationStatus = await X509RevocationService.checkCertificateChainRevocation(agentContext, parsedChain, config, verificationDate);
if (!validations.revocationStatus.isValid) throw new X509ValidationError(validations.revocationStatus.error?.message ?? "Revocation check failed", {
isValid: false,
validations,
error: validations.revocationStatus.error
});
} else validations.revocationStatus = {
isValid: true,
details: "Revocation checking disabled"
};
return parsedChain;
}
/**
*
* Parses a base64-encoded X.509 certificate into a {@link X509Certificate}
*
*/
static parseCertificate(_agentContext, { encodedCertificate }) {
return X509Certificate.fromEncodedCertificate(encodedCertificate);
}
static getLeafCertificate(_agentContext, { certificateChain }) {
if (certificateChain.length === 0) throw new X509Error("Certificate chain is empty");
return X509Certificate.fromEncodedCertificate(certificateChain[0]);
}
static async createCertificate(agentContext, options) {
const webCrypto = new CredoWebCrypto(agentContext);
return await X509Certificate.create(options, webCrypto);
}
static async createCertificateSigningRequest(agentContext, options) {
const webCrypto = new CredoWebCrypto(agentContext);
return await CertificateSigningRequest.create(options, webCrypto);
}
/**
* Creates and signs a X.509 Certificate Revocation List (CRL).
*
* Supported CRL extensions: CRL Number (§5.2.3), Delta CRL Indicator (§5.2.4), Authority Key
* Identifier (§5.2.1) and Issuing Distribution Point (§5.2.5), plus the per-entry
* `certificateIssuer` extension (§5.3.3) for indirect CRLs. Other CRL extensions are not produced.
*
* Note: issuer/subject names built from a structured object only support the `CN`, `C`, `OU` and
* `ST` distinguished-name attributes; pass a raw DN string to use any other attribute.
*/
static async createCertificateRevocationList(agentContext, options) {
const webCrypto = new CredoWebCrypto(agentContext);
return await X509CertificateRevocationList.create(options, webCrypto);
}
static parseCertificateSigningRequest({ encodedCertificateSigningRequest }) {
return CertificateSigningRequest.fromEncodedCertificateRequest(encodedCertificateSigningRequest);
}
/**
* Validates path length constraints for a certificate chain
* Per RFC 5280 Section 4.2.1.9
*/
static validatePathLengthConstraints(certificateChain) {
let currentPathLength = 0;
for (let i = certificateChain.length - 1; i >= 0; i--) {
const cert = certificateChain[i];
const basicConstraintsExt = new x509.X509Certificate(cert.rawCertificate).getExtension("2.5.29.19");
if (!basicConstraintsExt) {
if (i === 0) continue;
continue;
}
const basicConstraints = new x509.BasicConstraintsExtension(basicConstraintsExt.rawData);
if (!basicConstraints.ca) continue;
if (basicConstraints.pathLength !== void 0) {
const remainingCAsInChain = currentPathLength;
if (remainingCAsInChain > basicConstraints.pathLength) return {
isValid: false,
error: new X509Error(`Path length constraint violated. Certificate '${cert.subject}' has pathLength=${basicConstraints.pathLength} but ${remainingCAsInChain} intermediate CA(s) follow it in the chain.`),
details: `pathLength=${basicConstraints.pathLength}, actual=${remainingCAsInChain}`
};
}
currentPathLength++;
}
return { isValid: true };
}
/**
* Validates basic constraints for CA certificates in the chain
* Per RFC 5280 Section 4.2.1.9
*/
static validateBasicConstraintsForChain(certificateChain) {
for (let i = 0; i < certificateChain.length - 1; i++) {
const cert = certificateChain[i];
const basicConstraintsExt = new x509.X509Certificate(cert.rawCertificate).getExtension("2.5.29.19");
if (!basicConstraintsExt) return {
isValid: false,
error: new X509Error(`Certificate '${cert.subject}' is used as a CA but does not have a basicConstraints extension`)
};
if (!new x509.BasicConstraintsExtension(basicConstraintsExt.rawData).ca) return {
isValid: false,
error: new X509Error(`Certificate '${cert.subject}' is used as a CA but does not have basicConstraints with ca=true`)
};
}
return { isValid: true };
}
/**
* Validates the Key Usage extension of the issuing CA certificates in the chain.
* Per RFC 5280 §4.2.1.3 / §6.1.4(n): a certificate used to sign other certificates MUST, when it
* asserts a Key Usage extension, include the `keyCertSign` bit.
*
* Key Usage is optional in a certificate; when the extension is absent, all usages are permitted
* and no constraint is enforced. Only the issuing certificates (every certificate except the leaf)
* are checked.
*/
static validateKeyUsageForChain(certificateChain) {
for (let i = 0; i < certificateChain.length - 1; i++) {
const cert = certificateChain[i];
const keyUsage = cert.keyUsage;
if (keyUsage && keyUsage.length > 0 && !keyUsage.includes(32)) return {
isValid: false,
error: new X509Error(`Certificate '${cert.subject}' is used as a CA but its key usage does not include keyCertSign`),
details: "Missing keyCertSign key usage"
};
}
return { isValid: true };
}
};
X509Service = _X509Service = __decorate([injectable()], X509Service);
//#endregion
export { X509Service };
//# sourceMappingURL=X509Service.mjs.map