@simplewebauthn/server
Version:
SimpleWebAuthn for Servers
149 lines (148 loc) • 6.76 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateCertificatePath = validateCertificatePath;
const asn1_schema_1 = require("@peculiar/asn1-schema");
const isCertRevoked_js_1 = require("./isCertRevoked.js");
const verifySignature_js_1 = require("./verifySignature.js");
const mapX509SignatureAlgToCOSEAlg_js_1 = require("./mapX509SignatureAlgToCOSEAlg.js");
const getCertificateInfo_js_1 = require("./getCertificateInfo.js");
const convertPEMToBytes_js_1 = require("./convertPEMToBytes.js");
/**
* Traverse an array of PEM certificates and ensure they form a proper chain
* @param x5cCertsPEM Typically the result of `x5c.map(convertASN1toPEM)`
* @param trustAnchorsPEM PEM-formatted certs that an attestation statement x5c may chain back to
*/
async function validateCertificatePath(x5cCertsPEM, trustAnchorsPEM = []) {
if (trustAnchorsPEM.length === 0) {
// We have no trust anchors to chain back to, so skip path validation
return true;
}
let invalidSubjectAndIssuerError = false;
let certificateNotYetValidOrExpiredErrorMessage = undefined;
for (const anchorPEM of trustAnchorsPEM) {
try {
const certsWithTrustAnchor = x5cCertsPEM.concat([anchorPEM]);
await _validatePath(certsWithTrustAnchor);
// If we successfully validated a path then there's no need to continue. Reset any existing
// errors that were thrown by earlier trust anchors
invalidSubjectAndIssuerError = false;
certificateNotYetValidOrExpiredErrorMessage = undefined;
break;
}
catch (err) {
if (err instanceof InvalidSubjectAndIssuer) {
invalidSubjectAndIssuerError = true;
}
else if (err instanceof CertificateNotYetValidOrExpired) {
certificateNotYetValidOrExpiredErrorMessage = err.message;
}
else {
throw err;
}
}
}
// We tried multiple trust anchors and none of them worked
if (invalidSubjectAndIssuerError) {
throw new InvalidSubjectAndIssuer();
}
else if (certificateNotYetValidOrExpiredErrorMessage) {
throw new CertificateNotYetValidOrExpired(certificateNotYetValidOrExpiredErrorMessage);
}
return true;
}
/**
* @param x5cCerts X.509 `x5c` certs in PEM string format
* @param anchorCert X.509 trust anchor cert in PEM string format
*/
async function _validatePath(x5cCertsWithTrustAnchorPEM) {
if (new Set(x5cCertsWithTrustAnchorPEM).size !== x5cCertsWithTrustAnchorPEM.length) {
throw new Error('Invalid certificate path: found duplicate certificates');
}
// Make sure no certs are revoked, and all are within their time validity window
for (const certificatePEM of x5cCertsWithTrustAnchorPEM) {
const certInfo = (0, getCertificateInfo_js_1.getCertificateInfo)((0, convertPEMToBytes_js_1.convertPEMToBytes)(certificatePEM));
await assertCertNotRevoked(certInfo.parsedCertificate);
assertCertIsWithinValidTimeWindow(certInfo, certificatePEM);
}
// Make sure each x5c cert is issued by the next certificate in the chain
for (let i = 0; i < (x5cCertsWithTrustAnchorPEM.length - 1); i += 1) {
const subjectPem = x5cCertsWithTrustAnchorPEM[i];
const issuerPem = x5cCertsWithTrustAnchorPEM[i + 1];
const subjectInfo = (0, getCertificateInfo_js_1.getCertificateInfo)((0, convertPEMToBytes_js_1.convertPEMToBytes)(subjectPem));
const issuerInfo = (0, getCertificateInfo_js_1.getCertificateInfo)((0, convertPEMToBytes_js_1.convertPEMToBytes)(issuerPem));
// Make sure subject issuer is issuer subject
if (subjectInfo.issuer.combined !== issuerInfo.subject.combined) {
throw new InvalidSubjectAndIssuer();
}
const issuerCertIsRootCert = issuerInfo.issuer.combined === issuerInfo.subject.combined;
await assertSubjectIsSignedByIssuer(subjectInfo.parsedCertificate, issuerPem);
// Perform one final check if the issuer cert is also a root certificate
if (issuerCertIsRootCert) {
await assertSubjectIsSignedByIssuer(issuerInfo.parsedCertificate, issuerPem);
}
}
return true;
}
/**
* Check if the certificate is revoked or not. If it is, raise an error
*/
async function assertCertNotRevoked(certificate) {
// Check for certificate revocation
const subjectCertRevoked = await (0, isCertRevoked_js_1.isCertRevoked)(certificate);
if (subjectCertRevoked) {
throw new Error(`Found revoked certificate in certificate path`);
}
}
/**
* Require the cert to be within its notBefore and notAfter time window
*
* @param certInfo Parsed cert information
* @param certPEM PEM-formatted certificate, for error reporting
*/
function assertCertIsWithinValidTimeWindow(certInfo, certPEM) {
const { notBefore, notAfter } = certInfo;
const now = new Date(Date.now());
if (notBefore > now || notAfter < now) {
throw new CertificateNotYetValidOrExpired(`Certificate is not yet valid or expired: ${certPEM}`);
}
}
/**
* Ensure that the subject cert has been signed by the next cert in the chain
*/
async function assertSubjectIsSignedByIssuer(subjectCert, issuerPEM) {
// Verify the subject certificate's signature with the issuer cert's public key
const data = asn1_schema_1.AsnSerializer.serialize(subjectCert.tbsCertificate);
const signature = subjectCert.signatureValue;
const signatureAlgorithm = (0, mapX509SignatureAlgToCOSEAlg_js_1.mapX509SignatureAlgToCOSEAlg)(subjectCert.signatureAlgorithm.algorithm);
const issuerCertBytes = (0, convertPEMToBytes_js_1.convertPEMToBytes)(issuerPEM);
const verified = await (0, verifySignature_js_1.verifySignature)({
data: new Uint8Array(data),
signature: new Uint8Array(signature),
x509Certificate: issuerCertBytes,
hashAlgorithm: signatureAlgorithm,
});
if (!verified) {
throw new InvalidSubjectSignatureForIssuer();
}
}
// Custom errors to help pass on certain errors
class InvalidSubjectAndIssuer extends Error {
constructor() {
const message = 'Subject issuer did not match issuer subject';
super(message);
this.name = 'InvalidSubjectAndIssuer';
}
}
class InvalidSubjectSignatureForIssuer extends Error {
constructor() {
const message = 'Subject signature was invalid for issuer';
super(message);
this.name = 'InvalidSubjectSignatureForIssuer';
}
}
class CertificateNotYetValidOrExpired extends Error {
constructor(message) {
super(message);
this.name = 'CertificateNotYetValidOrExpired';
}
}