From 4592239057d7b3b0bf9a9002d759d47255760847 Mon Sep 17 00:00:00 2001 From: El-Virus Date: Tue, 30 Jul 2024 10:55:56 +0200 Subject: [PATCH 1/7] Add CRL & methods to read CRLs and verify them --- lib/oids.js | 10 +- lib/x509.js | 721 ++++++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 675 insertions(+), 56 deletions(-) diff --git a/lib/oids.js b/lib/oids.js index d1504eb16..b613c1c42 100644 --- a/lib/oids.js +++ b/lib/oids.js @@ -148,16 +148,16 @@ _I_('2.5.29.16', 'privateKeyUsagePeriod'); _IN('2.5.29.17', 'subjectAltName'); _IN('2.5.29.18', 'issuerAltName'); _IN('2.5.29.19', 'basicConstraints'); -_I_('2.5.29.20', 'cRLNumber'); -_I_('2.5.29.21', 'cRLReason'); +_IN('2.5.29.20', 'cRLNumber'); +_IN('2.5.29.21', 'cRLReason'); _I_('2.5.29.22', 'expirationDate'); _I_('2.5.29.23', 'instructionCode'); -_I_('2.5.29.24', 'invalidityDate'); +_IN('2.5.29.24', 'invalidityDate'); _I_('2.5.29.25', 'cRLDistributionPoints'); // deprecated use .31 _I_('2.5.29.26', 'issuingDistributionPoint'); // deprecated use .28 -_I_('2.5.29.27', 'deltaCRLIndicator'); +_IN('2.5.29.27', 'deltaCRLIndicator'); _I_('2.5.29.28', 'issuingDistributionPoint'); -_I_('2.5.29.29', 'certificateIssuer'); +_IN('2.5.29.29', 'certificateIssuer'); _I_('2.5.29.30', 'nameConstraints'); _IN('2.5.29.31', 'cRLDistributionPoints'); _IN('2.5.29.32', 'certificatePolicies'); diff --git a/lib/x509.js b/lib/x509.js index 2877810c1..57bc75c20 100644 --- a/lib/x509.js +++ b/lib/x509.js @@ -1,10 +1,13 @@ /** * Javascript implementation of X.509 and related components (such as - * Certification Signing Requests) of a Public Key Infrastructure. + * Certification Signing Requests & Certificate Revocation Lists) of + * a Public Key Infrastructure. * * @author Dave Longley + * @author Eloi Montañés * * Copyright (c) 2010-2014 Digital Bazaar, Inc. + * Copyright (c) 2024 Eloi Montañés . * * The ASN.1 representation of an X.509v3 certificate is as follows * (see RFC 2459): @@ -106,6 +109,33 @@ * signatureAlgorithm AlgorithmIdentifier{{ SignatureAlgorithms }}, * signature BIT STRING * } + * + * + * The ASN.1 representation of a CRL is as follows + * (see RFC 5280): + * + * CertificateList ::= SEQUENCE { + * tbsCertList TBSCertList, + * signatureAlgorithm AlgorithmIdentifier, + * signatureValue BIT STRING } + * + * TBSCertList ::= SEQUENCE { + * version Version OPTIONAL, + * -- if present, MUST be v2 + * signature AlgorithmIdentifier, + * issuer Name, + * thisUpdate Time, + * nextUpdate Time OPTIONAL, + * revokedCertificates SEQUENCE OF SEQUENCE { + * userCertificate CertificateSerialNumber, + * revocationDate Time, + * crlEntryExtensions Extensions OPTIONAL + * -- if present, version MUST be v2 + * } OPTIONAL, + * crlExtensions [0] EXPLICIT Extensions OPTIONAL + * -- if present, version MUST be v2 + * } + * The other types are described above. */ var forge = require('./forge'); require('./aes'); @@ -493,6 +523,122 @@ var certificationRequestValidator = { ] }; +var certificateListValidator = { + name: 'CertificateList', + tagClass: asn1.Class.UNIVERSAL, + type: asn1.Type.SEQUENCE, + constucted: true, + value: [{ + name: 'CertificateList.TBSCertList', + tagClass: asn1.Class.UNIVERSAL, + type: asn1.Type.SEQUENCE, + constructed: true, + captureAsn1: 'tbsCertList', + value: [{ + name: 'CertificateList.TBSCertList.verison', + tagClass: asn1.Class.UNIVERSAL, + type: asn1.Type.INTEGER, + constructed: false, + optional: true, + capture: 'tbsVersion' + }, { + name: 'CertificateList.TBSCertList.signature', + tagClass: asn1.Class.UNIVERSAL, + type: asn1.Type.SEQUENCE, + constructed: true, + value: [{ + name: 'CertificateList.TBSCertList.signature.algorithm', + tagClass: asn1.Class.UNIVERSAL, + type: asn1.Type.OID, + constructed: false, + capture: 'tbsSigAlgorithm' + }, { + name: 'CertificateList.TBSCertList.signature.parameters', + tagClass: asn1.Class.UNIVERSAL, + optional: true, + capture: 'tbsSigParam' + }] + }, { + name: 'CertificateList.TBSCertList.issuer', + tagClass: asn1.Class.UNIVERSAL, + type: asn1.Type.SEQUENCE, + constructed: true, + captureAsn1: 'tbsIssuer' + }, { + name: 'CertificateList.TBSCertList.thisUpdate', + tagClass: asn1.Class.UNIVERSAL, + constructed: false, + captureAsn1: 'tbsCUpdate' + }, { + name: 'CertificateList.TBSCertList.nextUpdate', + tagClass: asn1.Class.UNIVERSAL, + constructed: false, + optional: true, + captureAsn1: 'tbsNUpdate' + }, { + name: 'CertificateList.TBSCertList.revokedCertificates', + tagClass: asn1.Class.UNIVERSAL, + type: asn1.Type.SEQUENCE, + constructed: true, + optional: true, + capture: 'revokedCertificates', + value: [{ + name: 'CertificateList.TBSCertList.revokedCertificates2', + tagClass: asn1.Class.UNIVERSAL, + type: asn1.Type.SEQUENCE, + constructed: true, + value: [{ + name: 'CertificateList.TBSCertList.revokedCertificates.userCertificate', + tagClass: asn1.Class.UNIVERSAL, + type: asn1.Type.INTEGER, + constructed: false + }, { + name: 'CertificateList.TBSCertList.revokedCertificates.revocationDate', + tagClass: asn1.Class.UNIVERSAL, + type: asn1.Type.UTCTIME, + constructed: false + }, { + name: 'CertificateList.TBSCertList.revokedCertificates.crlEntryExtensions', + tagClass: asn1.Class.UNIVERSAL, + type: asn1.Class.SEQUENCE, + constructed: true, + optional: true + }] + }] + }, { + name: 'CertificateList.TBSCertList.crlExtensions', + tagClass: asn1.Class.CONTEXT_SPECIFIC, + type: 0, + constructed: true, + captureAsn1: 'tbsExtensions', + optional: true + }] + }, { + name: 'CertificateList.signatureAlgorithm', + tagClass: asn1.Class.UNIVERSAL, + type: asn1.Type.SEQUENCE, + constructed: true, + value: [{ + name: 'CertificateList.TBSCertList.signature.algorithm', + tagClass: asn1.Class.UNIVERSAL, + type: asn1.Type.OID, + constructed: false, + capture: 'crlSigAlgorithm' + }, { + name: 'CertificateList.TBSCertList.signature.parameters', + tagClass: asn1.Class.UNIVERSAL, + optional: true, + capture: 'crlSigParams' + }] + }, { + name: 'CertificateList.signatureValue', + tagClass: asn1.Class.UNIVERSAL, + type: asn1.Type.BITSTRING, + constructed: false, + captureBitStringValue: 'crlSignature' + }] +}; + /** * Converts an RDNSequence of ASN.1 DER-encoded RelativeDistinguishedName * sets into an array with objects that have type and value properties. @@ -726,7 +872,7 @@ var _createSignatureDigest = function(options) { * Verify signature on certificate or CSR. * * @param options: - * certificate the certificate or CSR to verify. + * certificate the certificate or CSR to verify against. * md the signature digest. * signature the signature * @return a created md instance. throws if unknown oid. @@ -778,10 +924,43 @@ var _verifySignature = function(options) { break; } - // verify signature on cert using public key - return cert.publicKey.verify( - options.md.digest().getBytes(), options.signature, scheme - ); + // try to verify signature on cert using public key + try { + return cert.publicKey.verify( + options.md.digest().getBytes(), options.signature, scheme + ); + } catch { + // if it fails, we're using the wrong certificate + return false; + } +}; + +/** + * Given an array of extensions, gets an extension by its name or id. + * + * @param exts the array of extensions to search + * @param options the name to use or an object with: + * name the name to use. + * id the id to use. + * + * @return the extension or null if not found. + */ +_getExtension = function(exts, options) { + if(typeof options === 'string') { + options = {name: options}; + } + + var rval = null; + var ext; + for(var i = 0; rval === null && i < exts.length; ++i) { + ext = exts[i]; + if(options.id && ext.id === options.id) { + rval = ext; + } else if(options.name && ext.name === options.name) { + rval = ext; + } + } + return rval; }; /** @@ -1102,22 +1281,8 @@ pki.createCertificate = function() { * @return the extension or null if not found. */ cert.getExtension = function(options) { - if(typeof options === 'string') { - options = {name: options}; - } - - var rval = null; - var ext; - for(var i = 0; rval === null && i < cert.extensions.length; ++i) { - ext = cert.extensions[i]; - if(options.id && ext.id === options.id) { - rval = ext; - } else if(options.name && ext.name === options.name) { - rval = ext; - } - } - return rval; - }; + return _getExtension(cert.extensions, options); + } /** * Signs this certificate using the given private key. @@ -1418,6 +1583,62 @@ pki.certificateFromAsn1 = function(obj, computeHash) { return cert; }; +/** + * Iterates through an ASN.1 extensions object and calls the + * callback function for each extension. + * Returns an array with the return values of the callback + * function. + * + * @param exts the extensions ASN.1 with extension sequences to parse. + * @param callback the callback function. + * @returns the array with the return values of the callback. + */ +function _iterExtensionsAndCallBack(exts, callback) { + var rval = []; + for(var i = 0; i < exts.value.length; ++i) { + // get extension sequence + var extseq = exts.value[i]; + for(var ei = 0; ei < extseq.value.length; ++ei) { + rval.push(callback(extseq.value[ei])); + } + } + + return rval; +} + +/** + * Attempts to parse a single extension from ASN.1. + * If the oid is known, return the callback function's + * return value; if not, return the extension without + * any further processing. + * + * @param ext the extension in ASN.1 format. + * + * @return the parsed extension as an object. + */ +function _genericExceptionParser(ext, callback) { + // an extension has: + // [0] extnID OBJECT IDENTIFIER + // [1] critical BOOLEAN DEFAULT FALSE + // [2] extnValue OCTET STRING + var e = {}; + e.id = asn1.derToOid(ext.value[0].value); + e.critical = false; + if(ext.value[1].type === asn1.Type.BOOLEAN) { + e.critical = (ext.value[1].value.charCodeAt(0) !== 0x00); + e.value = ext.value[2].value; + } else { + e.value = ext.value[1].value; + } + // if the oid is known, get its name + if(e.id in oids) { + e.name = oids[e.id]; + + return callback(e); + } + return e; +} + /** * Converts an ASN.1 extensions object (with extension sequences as its * values) into an array of extension objects with types and values. @@ -1474,16 +1695,7 @@ pki.certificateFromAsn1 = function(obj, computeHash) { * @return the array. */ pki.certificateExtensionsFromAsn1 = function(exts) { - var rval = []; - for(var i = 0; i < exts.value.length; ++i) { - // get extension sequence - var extseq = exts.value[i]; - for(var ei = 0; ei < extseq.value.length; ++ei) { - rval.push(pki.certificateExtensionFromAsn1(extseq.value[ei])); - } - } - - return rval; + return _iterExtensionsAndCallBack(exts, pki.certificateExtensionFromAsn1); }; /** @@ -1494,23 +1706,7 @@ pki.certificateExtensionsFromAsn1 = function(exts) { * @return the parsed extension as an object. */ pki.certificateExtensionFromAsn1 = function(ext) { - // an extension has: - // [0] extnID OBJECT IDENTIFIER - // [1] critical BOOLEAN DEFAULT FALSE - // [2] extnValue OCTET STRING - var e = {}; - e.id = asn1.derToOid(ext.value[0].value); - e.critical = false; - if(ext.value[1].type === asn1.Type.BOOLEAN) { - e.critical = (ext.value[1].value.charCodeAt(0) !== 0x00); - e.value = ext.value[2].value; - } else { - e.value = ext.value[1].value; - } - // if the oid is known, get its name - if(e.id in oids) { - e.name = oids[e.id]; - + return _genericExceptionParser(ext, function(e) { // handle key usage if(e.name === 'keyUsage') { // get value as BIT STRING @@ -1633,8 +1829,9 @@ pki.certificateExtensionFromAsn1 = function(ext) { var ev = asn1.fromDer(e.value); e.subjectKeyIdentifier = forge.util.bytesToHex(ev.value); } - } - return e; + + return e; + }) }; /** @@ -3240,3 +3437,425 @@ pki.verifyCertificateChain = function(caStore, chain, options) { return true; }; + +/** + * Creates an empty Certificate Revocation List + * + * @return the empty Certificate Revocation List + */ +pki.createCertificateRevocationList = function() { + var crl = {}; + + // TBS + crl.version = 0x01; + crl.siginfo = {}; + crl.siginfo.algorithmOid = null; + + crl.issuer = {}; + crl.issuer.getField = function(sn) { + return _getAttribute(crl.issuer, sn); + }; + crl.issuer.addField = function(attr) { + _fillMissingFields([attr]); + crl.issuer.attributes.push(attr); + }; + crl.issuer.attributes = []; + crl.issuer.hash = null; + + crl.thisUpdate = new Date(); + crl.nextUpdate = new Date(); + + crl.revocations = []; + + crl.extensions = []; + + // CRL + crl.signatureOid = null; + crl.signature = null; + + crl.md = null; + + // Methods + + /** + * Gets an extension by its name or id. + * + * @param options the name to use or an object with: + * name the name to use. + * id the id to use. + * + * @return the extension or null if not found. + */ + crl.getExtension = function(options) { + return _getExtension(crl.extensions, options); + } + + /** + * Checks if the CRL was signed by a specific certificate. + * + * @param cert certificate to verify against. + * + * @returns true if certificate signed the CRL, false if not. + */ + crl.verify = function(cert) { + var rval = false; + + var md = crl.md; + if (md === null) { + md = _createSignatureDigest({ + signatureOid: crl.signatureOid, + type: 'certificate revocation list' + }); + + // produce DER formatted CertificationRequestInfo and digest it + var tcl = crl.tbsCertList; //TODO: Handle tcl generation + var bytes = asn1.toDer(tcl); + md.update(bytes.getBytes()); + } + + if (md !== null) { + rval = _verifySignature({ + certificate: cert, md: md, signature: crl.signature + }); + } + + return rval; + } + + /** + * Checks if the CRL is a Delta CRL. + * + * @returns true if it's a Delta CRL, false if not. + */ + crl.isDelta = function() { + if (crl.version < 1) + return false; + return !!crl.getExtension('deltaCRLIndicator'); + } + + /** + * Gets a revocation, if present in the CRL, if not, returns null. + * + * @param serial the cerificate's cerial + * @returns the revocation if revoked, null if not. + */ + crl.getRevocation = function(serial) { + var rval = null; + for (var i = 0; rval === null && i < crl.revocations.length; ++i) { + if (crl.revocations[i].serial === serial) { + rval = crl.revocations[i]; + // handle case where Certificate is actually removed from CRL + if (crl.isDelta() && rval.getExtension('cRLReason') === 8) + return null; + } + } + return rval; + } + + /** + * Checks if a certificate is revoked. + * + * @param cert the certificate to check. + * + * @returns true if revoked, false if not + */ + crl.isCertRevoked = function(cert) { + return (crl.getRevocation(cert.serialNumber) !== null); + } + + return crl; +} + +pki.certificateRevocationListFromAsn1 = function(obj, computeHash) { + // validate CRL and capture data + var capture = {}; + var errors = []; + if (!asn1.validate(obj, certificateListValidator, capture, errors)) { + var error = new Error('Cannot read certificate revocation list. ' + + 'ASN.1 object is not a CertificateList.'); + error.errors = errors; + throw error; + } + // TODO: Ensure that if the specified fields are set, version must be 2. + + // create crl object + var crl = pki.createCertificateRevocationList(); + crl.version = capture.tbsVersion ? capture.tbsVersion.charCodeAt(0) : 0; + + // convert signature information + crl.signatureOid = forge.asn1.derToOid(capture.crlSigAlgorithm); + crl.signatureParameters = _readSignatureParameters(crl.signatureOid, capture.crlSigParams, true); + crl.siginfo.algorithmOid = forge.asn1.derToOid(capture.tbsSigAlgorithm); + crl.siginfo.signatureParameters = _readSignatureParameters(crl.siginfo.algorithmOid, capture.tbsSigParam, true); + crl.signature = capture.crlSignature; + + // store tbsCertList for verification + crl.tbsCertList = capture.tbsCertList; + + if (computeHash) { + // create digest for OID signature type + crl.md = _createSignatureDigest({ + signatureOid: crl.signatureOid, + type: 'certificate revocation list' + }); + + // produce DER formatted TBSCertList and digest it + var bytes = asn1.toDer(crl.tbsCertList); + crl.md.update(bytes.getBytes()); + } + + // convert issuer & hash + var imd = forge.md.sha1.create(); + crl.issuer.attributes = pki.RDNAttributesAsArray(capture.tbsIssuer, imd); + crl.issuer.hash = imd.digest().toHex(); + + // convert update times, and check their type validity + // (Validator didn't, as it can be either UTC or Generalized) + if (capture.tbsCUpdate.type === asn1.Type.UTCTIME) { + crl.thisUpdate = asn1.utcTimeToDate(capture.tbsCUpdate.value); + } else if (capture.tbsCUpdate === asn1.Type.GENERALIZEDTIME) { + crl.thisUpdate = asn1.generalizedTimeToDate(capture.tbsCUpdate.value); + } else { + throw new Error('CRL\'s thisUpdate Time is not of time type'); + } + if (capture.tbsNUpdate) { + if (capture.tbsNUpdate.type === asn1.Type.UTCTIME) { + crl.nextUpdate = asn1.utcTimeToDate(capture.tbsNUpdate.value); + } else if (capture.tbsNUpdate === asn1.Type.GENERALIZEDTIME) { + crl.nextUpdate = asn1.generalizedTimeToDate(capture.tbsNUpdate.value); + } else { + throw new Error('CRL\'s nextUpdate Time is present but is not of time type'); + } + } + + // convert revocations + for (const revoked of capture.revokedCertificates) { + // certificateListValidator has already verified the entries + var rci = {}; + + var serial = forge.util.createBuffer(revoked.value[0].value); + rci.serial = serial.toHex(); + + rci.date = asn1.utcTimeToDate(revoked.value[1].value); + + if (revoked.value[2]) { + rci.extensions = pki.certificateRevocationListEntryExtensionsFromAsn1(revoked.value[2]); + } else { + rci.extensions = []; + } + + rci.getExtension = function(options) { + return _getExtension(rci.extensions, options); + } + + crl.revocations.push(rci); + } + + if (capture.tbsExtensions) { + crl.extensions = pki.certificateRevocationListExtensionsFromAsn1(capture.tbsExtensions); + } else { + crl.extensions = []; + } + + return crl; +} + +/** + * Converts an ASN.1 extensions object (with extension sequences as its + * values) into an array of extension objects with types and values. + * + * Supported extensions: + * + * id-ce-cRLReasons OBJECT IDENTIFIER ::= { id-ce 21 } + * CRLReason ::= ENUMERATED { + * unspecified (0), + * keyCompromise (1), + * cACompromise (2), + * affiliationChanged (3), + * superseded (4), + * cessationOfOperation (5), + * certificateHold (6), + * -- value 7 is not used + * removeFromCRL (8), + * privilegeWithdrawn (9), + * aACompromise (10) + * } + * + * id-ce-invalidityDate OBJECT IDENTIFIER ::= { id-ce 24 } + * InvalidityDate ::= GeneralizedTime + * + * TODO: + * id-ce-certificateIssuer OBJECT IDENTIFIER ::= { id-ce 29 } + * CertificateIssuer ::= GeneralNames + * + * @param exts the extensions ASN.1 with extension sequences to parse. + * + * @return the array. + */ +pki.certificateRevocationListEntryExtensionsFromAsn1 = function(exts) { + var rval = []; + for (var i = 0; i < exts.value.length; ++i) { + rval.push(pki.certificateRevocationListEntryExtensionFromAsn1(exts.value[i])); + } + return rval; +}; + +/** + * Parses a single CRL Entry extension from ASN.1. + * + * @param ext the extension in ASN.1 format. + * + * @return the parsed extension as an object. + */ +pki.certificateRevocationListEntryExtensionFromAsn1 = function(ext) { + return _genericExceptionParser(ext, function (e) { + // we're in uncharted territory, the validator has not parsed the extensions. + var ev = asn1.fromDer(e.value); + if (e.name === 'cRLReason') { + if (ev.tagClass !== asn1.Class.UNIVERSAL || ev.type !== asn1.Type.ENUMERATED) + throw new Error('CRL Entry\'s CRLReason is not ENUMERATED'); + e.reason = ev.value.charCodeAt(0); + } else if (e.name === 'invalidityDate') { + if (ev.tagClass !== asn1.Class.UNIVERSAL || ev.type !== asn1.Type.GENERALIZEDTIME) + throw new Error('CRL Entry\'s Invalidity Date is not GENERALIZEDTIME'); + e.invalidSince = asn1.generalizedTimeToDate(ev.value); + } else if (e.name === 'certificateIssuer') { + // TODO: Implement this (RFC 5280 § 5.3.3) + } + + return e; + }); +} + +/** + * Converts an ASN.1 extensions object (with extension sequences as its + * values) into an array of extension objects with types and values. + * + * Supported extensions: + * + * id-ce-authorityKeyIdentifier OBJECT IDENTIFIER ::= { id-ce 35 } + * AuthorityKeyIdentifier ::= SEQUENCE { + * keyIdentifier [0] KeyIdentifier OPTIONAL, + * !!!TODO: authorityCertIssuer [1] GeneralNames OPTIONAL, + * authorityCertSerialNumber [2] CertificateSerialNumber OPTIONAL + * } + * KeyIdentifier ::= OCTET STRING + * + * TODO: + * id-ce-issuerAltName OBJECT IDENTIFIER ::= { id-ce 18 } + * IssuerAltName ::= GeneralNames + * + * id-ce-cRLNumber OBJECT IDENTIFIER ::= { id-ce 20 } + * CRLNumber ::= INTEGER (0..MAX) + * + * id-ce-deltaCRLIndicator OBJECT IDENTIFIER ::= { id-ce 27 } + * BaseCRLNumber ::= CRLNumber + * + * TODO: + * id-ce-issuingDistributionPoint OBJECT IDENTIFIER ::= { id-ce 28 } + * IssuingDistributionPoint ::= SEQUENCE { + * distributionPoint [0] DistributionPointName OPTIONAL, + * onlyContainsUserCerts [1] BOOLEAN DEFAULT FALSE, + * onlyContainsCACerts [2] BOOLEAN DEFAULT FALSE, + * onlySomeReasons [3] ReasonFlags OPTIONAL, + * indirectCRL [4] BOOLEAN DEFAULT FALSE, + * onlyContainsAttributeCerts [5] BOOLEAN DEFAULT FALSE + * } + * -- at most one of onlyContainsUserCerts, onlyContainsCACerts, + * -- and onlyContainsAttributeCerts may be set to TRUE. + * + * TODO: + * id-ce-freshestCRL OBJECT IDENTIFIER ::= { id-ce 46 } + * FreshestCRL ::= CRLDistributionPoints + * + * TODO: + * id-pe-authorityInfoAccess OBJECT IDENTIFIER ::= { id-pe 1 } + * AuthorityInfoAccessSyntax ::= SEQUENCE SIZE (1..MAX) OF AccessDescription + * AccessDescription ::= SEQUENCE { + * accessMethod OBJECT IDENTIFIER, + * accessLocation GeneralName + * } + * id-ad OBJECT IDENTIFIER ::= { id-pkix 48 } + * id-ad-caIssuers OBJECT IDENTIFIER ::= { id-ad 2 } + * id-ad-ocsp OBJECT IDENTIFIER ::= { id-ad 1 } + * + * @param exts the extensions ASN.1 with extension sequences to parse. + * + * @return the array. + */ +pki.certificateRevocationListExtensionsFromAsn1 = function(exts) { + return _iterExtensionsAndCallBack(exts, pki.certificateRevocationListExtensionFromAsn1); +} + +/** + * Parses a single CRL extension from ASN.1. + * + * @param ext the extension in ASN.1 format. + * + * @return the parsed extension as an object. + */ +pki.certificateRevocationListExtensionFromAsn1 = function(ext) { + return _genericExceptionParser(ext, function (e) { + // we're in uncharted territory, the validator has not parsed the extensions. + var ev = asn1.fromDer(e.value); + if (e.name === 'authorityKeyIdentifier') { + if (ev.tagClass !== asn1.Class.UNIVERSAL || ev.type !== asn1.Type.SEQUENCE + || ev.value[0].tagClass !== asn1.Class.CONTEXT_SPECIFIC) + throw new Error("CRL\'s Authority Key Identifier is not tagged correctly"); + if (ev.value[0].type === 0) { + e.authorityKeyIdentifier = forge.util.bytesToHex(ev.value[0].value) + } else if (ev.value[0].type === 1) { + //TODO: Implement this + } else if (ev.value[0].type === 2) { + var serial = forge.util.createBuffer(ev.value[0].value); + e.authoritySerial = serial.toHex(); + } else { + throw new Error("CRL\'s Authority Key Identifier is not typed correctly"); + } + } else if (e.name === 'issuerAltName') { + //TODO: Implement this (RFC 5280 § 5.2.2) + } else if (e.name === 'cRLNumber') { + if (ev.tagClass !== asn1.Class.UNIVERSAL || ev.type !== asn1.Type.INTEGER) + throw new Error('CRL\'s CRLNumber is not INTEGER'); + e.number = asn1.derToInteger(ev.value); + } else if (e.name === 'deltaCRLIndicator') { + if (ev.tagClass !== asn1.Class.UNIVERSAL || ev.type !== asn1.Type.INTEGER) + throw new Error('CRL\'s deltaCRLI is not INTEGER'); + e.deltai = asn1.derToInteger(ev.value); + } else if (e.name === 'issuingDistributionPoint') { + //TODO: Implement this (RFC 5280 § 5.2.5), and activate OID Lookup + } else if (e.name === 'freshestCRL') { + //TODO: Implement this (RFC 5280 § 5.2.6), and activate OID Lookup + } else if (e.name === 'authorityInfoAccess') { + //TODO: Implement this (RFC 5280 § 5.2.7) + } + + return e; + }); +} + +/** + * Converts a Certificate Revocation List from PEM format. + * + * @param pem the PEM-formatted CRL. + * @param computeHash true to compute the hash for verification. + * @param strict true to be strict when checking ASN.1 value lengths, false to + * allow truncated values (default: true). + * @returns the Certificate Revocation List + */ +pki.certificateRevocationListFromPem = function(pem, computeHash, strict) { + var msg = forge.pem.decode(pem)[0]; + if (msg.type !== 'X509 CRL') { + var error = new Error( + 'Could not convert CSR from PEM; PEM header type is not X509 CRL.'); + error.headerType = msg.type; + throw error; + } + if (msg.procType && msg.procType.type === 'ENCRYPTED') + throw new Error('Could not convert certificate from PEM; PEM is encrypted.'); + + // convert DER to ASN.1 object + var obj = asn1.fromDer(msg.body, strict); + + return pki.certificateRevocationListFromAsn1(obj, computeHash); +} \ No newline at end of file From ea004062f08c76f529f8ffdd116e016d681c90d0 Mon Sep 17 00:00:00 2001 From: El-Virus Date: Tue, 30 Jul 2024 11:39:19 +0200 Subject: [PATCH 2/7] Add unit tests for CRLs --- tests/unit/crl.js | 241 ++++++++++++++++++++++++++++++++++++++++++++ tests/unit/index.js | 1 + 2 files changed, 242 insertions(+) create mode 100644 tests/unit/crl.js diff --git a/tests/unit/crl.js b/tests/unit/crl.js new file mode 100644 index 000000000..300a42a96 --- /dev/null +++ b/tests/unit/crl.js @@ -0,0 +1,241 @@ +var ASSERT = require('assert'); +var PKI = require('../../lib/pki'); + +(function() { + var _pem = { + /*caKey: -----BEGIN RSA PRIVATE KEY----- + MIIEogIBAAKCAQEApgaKzl0MIXjFGykNKOPXuYn0G9lwLJ2eRTRcpqUXiWqwSEAD + fyK/5qUbed7XgIIPX8j8Cghm4jTqDyVE66sX7fWTqoY4vhfEmVGdhq2Q4niI5xLv + 32Na66KztMGVaB2FYbquwXGN2RopHCcyLQL2krphik4Ftn+CJ6qK38czE2SX4JqM + SK2kjfvrBAkKUoAeJgqzV/6ICG0T2j7MVeYN8ygee5YQvaBaHgq+rd/nj7naqUNp + KoWCD49vaIQn0CUx39mCMdCQ/ZjT0ddr10bjIN2Ta7Jf5o6XnIGY38ImpBtuzikp + zdMEkX+2Jjjo/MS+M9008pZjkbBkDCxvxMPgdwIDAQABAoIBAEetHGDyN8n9jy7m + HqnQD7Ko1avuSCji1VDwRa2mKY6ocjmG9Vt+X5XOIxoOtD/lJokGRpV4Qh6XlJL8 + VpBd2aNgeaNNdhLPRQ+h9h2OMjYrroMAIHHzPW3sXKQFTSDZWduy0j5ubTxUuHnQ + jC91j4kSEQk6HOpIiyLf1Du/DpRoj2ktnGO36K5B3AKsbK8vH5xuTZkv0teoghLL + kMTZGrGEJQzYbDxl8I4gKNfpktpzXzMJU92M0+sRBRJ8SCy+V6QUF8XdKxAZ1aZ7 + 7ajumUFzhwWAqLt4D/iDtWo5m+IZXUcAJ+Fb6gSsbh2rTgXZU37dG6v9JerDH1sC + 1wFQiOkCgYEA3DziQYO9N5iUVnFBZ0vbmk2R5QKJ8xL8hL6ZsT6evV1oC2/Jv/Bu + /iaM6nKQwX+6KzLIcbYJsX5i2M47nIeElT40ZFBJ6FE2NGt1hZNTp+v/iPJG6A4H + uP7GEq+Ew2SYEDfBXDcrUvrZrGYYZD14+fJ2qD7Pu047U98qQnFwlb8CgYEAwPwY + nwMrMFlW7fn7aS0uC/RSjPwPzOAqJTBudttPOhYDCuzy+0ktGRnxxUP++xowd2by + /UrGa5+12r5DnbEQXpOKonB1QXSp+kCwC75alcWseeQqqNy9lDawBS/dNND6+e8h + g8CFptM3ahjt8oxMVorD9gQHbH9mZ4K0e0p0E0kCgYBTHLbVun2BqZbxODRSYxIw + nO1d2yNsE9Iv1i3x8Yu+Mq29AybDxFxelPXA1BNEsorzGmsCXowx61wqLUnZvFqQ + Z7Ul1hbOETe/eH4VNo/vYuRALg4MLJ9FdQAStSIJCsFH/YJ+5mL3Iatbn/u8eGZb + DOEyhOGn8dH5yNIN2Pl/yQKBgCt8o0+xtxm+CAi4PB8HP0kSVUfPxP+1w8l9kGbY + JJJCQ41Ct75ITxFI92IsYFjVHfbKDBdnsi6uXpxcI4B1Ver59FOGY+XMFEGAMitz + SZZWZPdSowpKM64iZKfGkWJFdUi8yiCWUYe2MNaHp5bwZoNZ4a6eWc3pJ3pLyb++ + l0mBAoGADz3D1T/p29qIjqSuwfGoc3Nuto7CGLILt/IPoIJmgbtYql1EeusVpuNK + uD4cunMTUUKYYE1lpnYWzz3BYHIPerf1PZvwCDY60bUobH5X9Lnpk6JB0DbwRA3J + LwLwujA9pWowUPTiU/lnssuuUYOuF12vGVsc8Qa1B50ufribQsA= + -----END RSA PRIVATE KEY-----*/ + caCert: '-----BEGIN CERTIFICATE-----\r\n' + + 'MIIDEjCCAfqgAwIBAgIIJpO/C+XBou8wDQYJKoZIhvcNAQELBQAwHzELMAkGA1UE\r\n' + + 'BhMCVVMxEDAOBgNVBAMTB1Rlc3QgQ0EwHhcNMjQwNzMwMDAwMDAwWhcNMzQwNzI5\r\n' + + 'MjM1OTU5WjAfMQswCQYDVQQGEwJVUzEQMA4GA1UEAxMHVGVzdCBDQTCCASIwDQYJ\r\n' + + 'KoZIhvcNAQEBBQADggEPADCCAQoCggEBAKYGis5dDCF4xRspDSjj17mJ9BvZcCyd\r\n' + + 'nkU0XKalF4lqsEhAA38iv+alG3ne14CCD1/I/AoIZuI06g8lROurF+31k6qGOL4X\r\n' + + 'xJlRnYatkOJ4iOcS799jWuuis7TBlWgdhWG6rsFxjdkaKRwnMi0C9pK6YYpOBbZ/\r\n' + + 'gieqit/HMxNkl+CajEitpI376wQJClKAHiYKs1f+iAhtE9o+zFXmDfMoHnuWEL2g\r\n' + + 'Wh4Kvq3f54+52qlDaSqFgg+Pb2iEJ9AlMd/ZgjHQkP2Y09HXa9dG4yDdk2uyX+aO\r\n' + + 'l5yBmN/CJqQbbs4pKc3TBJF/tiY46PzEvjPdNPKWY5GwZAwsb8TD4HcCAwEAAaNS\r\n' + + 'MFAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUhCDAEgZnoevZblFVw+RdSGsC\r\n' + + 'YK4wCwYDVR0PBAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG9w0BAQsF\r\n' + + 'AAOCAQEAfFS8ab8eYTMun6mH5AMqiJ+FLkx5IbZhteVFdXtGm+vC4kf1r9zsaKmv\r\n' + + 'CGfd7hoNBB4RPq9U7WyFAt2sfqJPkdd9OoJqkZmz1TlAhu7A7fZP/WtCy6FemN/r\r\n' + + 'ZbFFjlfnwBv86l/V1kvkHsZdR8AMwbU+CNObfvzMueCC4h+j7ybf/lEPf0hodqR4\r\n' + + '2OAr/sy7EyzBMlD718q9W93e/35G8rNaYNP/LfO03oOcmYEoBSqGzuHhg+ZlEqM+\r\n' + + 'kfBn+nWGb7UniQJDFBVSGb8yad+NWGwaayZ0VpdPe5kBl8hRKBGEEwmMee1kti9m\r\n' + + '7hhYWEVbfOf8rtL2W0rCDm7A6xROXQ==\r\n' + + '-----END CERTIFICATE-----', + crl: '-----BEGIN X509 CRL-----\r\n' + + 'MIIB3jCBxwIBATANBgkqhkiG9w0BAQsFADAfMQswCQYDVQQGEwJVUzEQMA4GA1UE\r\n' + + 'AxMHVGVzdCBDQRcNMjQwNzMwMDAwMDAwWhcNMjQwODI4MjM1OTU5WjBDMEECCDa4\r\n' + + '2ilYjPkhFw0yNDA3MzAwODMwNTZaMCYwGAYDVR0YBBEYDzIwMjQwNzMwMDgzMDAw\r\n' + + 'WjAKBgNVHRUEAwoBBaAvMC0wHwYDVR0jBBgwFoAUhCDAEgZnoevZblFVw+RdSGsC\r\n' + + 'YK4wCgYDVR0UBAMCAQEwDQYJKoZIhvcNAQELBQADggEBAF63xKW9rKRaYRgiH0qA\r\n' + + 'ZmzlHm75JSxpi7Cym0xxJLjwezOX9bn3kv18uWRGsjZ1mSGYfqnVPTxbLU0pmvwo\r\n' + + 'dWCUiZD1/19MCUoMh6qA882jTU2KIU6ib3ooYphH68UcLI/OWwqGVYjBWZo+kgHL\r\n' + + 't8X7oRlhjJuTlOTTvITqUhFYUF4QpPUVf35qs7/lfpCR9XEfzRgJQuupuwwDh8mU\r\n' + + 'm0hc1EE+w7OnnkIjTHkAiIF97+ZTw9Q5ZwRz3i+N3FuPkLhzb4ZTIZuGLd+P/JfW\r\n' + + 'egZCmYxqAh8EqP97dfL8ONx3y8A8+oX1/YlQfkNFdRl0ycOWWMBI5weuXNpxnU+J\r\n' + + 'gwY=\r\n' + + '-----END X509 CRL-----', + nonRevokedCert: '-----BEGIN CERTIFICATE-----\r\n' + + 'MIIC/zCCAeegAwIBAgIIClyuSS0X1skwDQYJKoZIhvcNAQELBQAwHzELMAkGA1UE\r\n' + + 'BhMCVVMxEDAOBgNVBAMTB1Rlc3QgQ0EwHhcNMjQwNzMwMDgzMDAwWhcNMjUwNzMw\r\n' + + 'MDgzMDAwWjAvMQswCQYDVQQGEwJVUzEgMB4GA1UEAxMXVGVzdCBOb24gUmV2b2tl\r\n' + + 'ZCBDbGllbnQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDQWBOO2fYu\r\n' + + 'gmcb/BkKVnVTgJg7+8QNj/KouIzDJ047ID6OiTrmM9kP9C+9oDr0ubmJNdGWQ90z\r\n' + + 'ymd7+obCHtW5m7ikQrT+huXM9hYnKbduBch0k+Sfh7qN8GfT1YJjuGRGcd/tXLeI\r\n' + + 'wDyhqC+/y6csEyJxyLPWX+iElCrAbQ//bWzT2M5oNHsraN6RiGDWSzs4l8Uj53pV\r\n' + + 'FURuamT3m5NqIlQPPl4Z3b9FT0S262aPM0yp+mDq9/nrdbvVOKDcj7fGTBLRNJgh\r\n' + + 'JydRJtRWQvSGQ76+FlpGIzULBM1bIYcZPJCFBpIcHi+qk0MGKyNuIc4ZNncfY90O\r\n' + + 'NN/rWG7Hu4P9AgMBAAGjLzAtMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFC/ruN0B\r\n' + + '14D/YN/tEdVdnolMFgitMA0GCSqGSIb3DQEBCwUAA4IBAQCZ04mpam1fJuEv7JPX\r\n' + + 'w5TlHeYouZIFfQ+DeBNROi81QquPpdBmxXQftdRi7353+DGE2WaA09etZ8VOpzee\r\n' + + '1aaCORAGj3R0pg7sgltC1YgPrv1m1MqWymhmruMVV6itIkj+vJTQwSVPC+3A8PDO\r\n' + + '1JO7fwbb19MoCAWaKXTLioIRAHzzB+XvJTkfY/Bu62MNL2i07WFKAbg7b4/5RXmY\r\n' + + 'Uyr/g+rsIo8Qsp2y/WAD355KQ81kG+7D6PZXUlj3akXLp6s7G3Q6xRDTRtmD2GPJ\r\n' + + 'VcNNhZNAKojYk31CWXxZaEdT6EIvr46bXC/lzDRUCD1RNTFeB5WIUlWUfOhyQQDW\r\n' + + '9CtR\r\n' + + '-----END CERTIFICATE-----', + /*nonRevokedCertKey: -----BEGIN RSA PRIVATE KEY----- + MIIEowIBAAKCAQEA0FgTjtn2LoJnG/wZClZ1U4CYO/vEDY/yqLiMwydOOyA+jok6 + 5jPZD/QvvaA69Lm5iTXRlkPdM8pne/qGwh7VuZu4pEK0/oblzPYWJym3bgXIdJPk + n4e6jfBn09WCY7hkRnHf7Vy3iMA8oagvv8unLBMicciz1l/ohJQqwG0P/21s09jO + aDR7K2jekYhg1ks7OJfFI+d6VRVEbmpk95uTaiJUDz5eGd2/RU9EtutmjzNMqfpg + 6vf563W71Tig3I+3xkwS0TSYIScnUSbUVkL0hkO+vhZaRiM1CwTNWyGHGTyQhQaS + HB4vqpNDBisjbiHOGTZ3H2PdDjTf61hux7uD/QIDAQABAoIBABBlVgiwa3jGh2HC + 6Z+QJUSQgqp5yjh9Aw43E9DJ15S8mV+zOgDixKrGPzmPkgQvV4QOSbOnHJHWVGWD + 1jYRoiUstY+rtj2vlQcXuK+VT1untdpCx0OstUg1Sp53l37MhIusq4AtAz6OTlc0 + eql/1+SWjufgcRKmUpCYbnLdQlyJ9iI7g/75r0hYRvs9LV6xQYGftr+Eenm1n9XS + vIni9m48BRL56QltJNgiXLNVG5Y8XrG0q8ZWCnsFdors+3ygpqm5HZikRAwfXwmJ + 6FRiipJA1s4XmHog6UQjcIIrAvwEIxMNv1vIfmdS7Xmnn+ezRU7tZBEerxw6JV7f + gU232RECgYEA4d9CVZsgRfO3HFe+OwMsENO1rbVyvV0/LRURToBPnl5O9+oGP5KY + MYlQcziEQa8vXjcqD6FkDcTY9CL51agMN6jUbWJWXIhbs9oLVDRn9+q3scXRes8T + hD68Kffzr9rudxljM2jDl3mXUeWBfLd6guwiMoVBEhOg82oWHH/5rpkCgYEA7CJL + qI1DJrKKp18iGR/Nxtqo15zBbUR4U2MMYai8C575sFQ5yAuE4FcmEGxR51VoX9B8 + As/UKUANzJDwH9Rve4es8J6Zs0agKP2AOCtxEJJYDbe/H8IK7kUwHEvmCLrDL+vO + 7iX/IUvYiUi24FmgRQlBr4S66szZlXEPFntr0wUCgYB/620qBlzUwR4nExpNWZKP + RRdTdbuxuymYYqIWj1yIGGkoxoUbY+6Fv3qshomAmbJ97UgI6iI8Ggu02Eod0rp4 + m0kTWeoHJcKprQdVfQiUw32dVKc6oiQvdUgjjKWaJqd/FAW2i9KZ6ubkHtKiy1a6 + 5vjHG+iqUCuLL72uDlxdoQKBgByC05HJZKdCfX1R/kL8VRNCiYpnEe/IiaK/3dnY + zsO0cT96G/PseCHCRAVNnuIIrO6MtLx+LYbBhikCAwxE0SUgL6Bp9fLwfxwT56xg + imlO0jTtz7Tc8Abu8a0o+OBq9HBPz49vpQt3JfEFh5c1GyXaxUSVCSCalVb27LRx + OIalAoGBANWJsdC3ByPEmo9qx/SGThbe9uBIASstL+fmk6tJeyxtcD5UpxaQJHvd + KEwnRNk+THcbbRjBg0O3M9c98Kdgy4PlVsQ68244H3XG20ZhdNmQ+JKDOclj8B6U + OXxzGJChZlhs6p71o9WeN94OvbGwHjT8swY9Nk4OC+YEng4EqCJC + -----END RSA PRIVATE KEY-----*/ + revokedCert: '-----BEGIN CERTIFICATE-----\r\n' + + 'MIIC+zCCAeOgAwIBAgIINrjaKViM+SEwDQYJKoZIhvcNAQELBQAwHzELMAkGA1UE\r\n' + + 'BhMCVVMxEDAOBgNVBAMTB1Rlc3QgQ0EwHhcNMjQwNzMwMDgyOTAwWhcNMjUwNzMw\r\n' + + 'MDgyOTAwWjArMQswCQYDVQQGEwJVUzEcMBoGA1UEAxMTVGVzdCBSZXZva2VkIENs\r\n' + + 'aWVudDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANrhP9CWtKP7y82f\r\n' + + 'zbfGREVNcZ1qFcfLWXsZCAXItp2OKHHXFdgG3A0juAHuFGZXQ7cMHURhFxhU+bli\r\n' + + 'pZjBBxYxJInk775CRNKxV0oZ0HjgWMaI4Uneehi72qaOirXyQLKwKf8Go/4HLaF5\r\n' + + '/wZdiG2RSnC4jyYbpkzJXKJEpvHvQVmOqvQBCFmMVlRXq7Ltcg2FLAi6JLe2oSUd\r\n' + + 'q+gG4GRojAdyhraDz1MvTrh7LnRq0TIGnQkF7rBhfDOKi9XiwsRlKmTuF+zbU8cJ\r\n' + + '+pVPv3qnDNRUEyfir7V2t879B0fMW69inBEWTMgVIEJPWpf//NG+A8BxSIPkHOXS\r\n' + + 'y5f6lvMCAwEAAaMvMC0wDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU/T+uLKWxnQYV\r\n' + + '4FDRXZylBVLwXS4wDQYJKoZIhvcNAQELBQADggEBAI1dXV/7ptmVL+DyHBloUQdZ\r\n' + + 'qbenBCPX21cN4x/1FwoysV+yhY97mLAhLja/TGhLuz+W8y/NvVQQRWjdPEgyz9P5\r\n' + + '1PJDkrcySSbjdlhYcIRQMjpPirXcBPouBQiuZ6hlqBARdLQ6pdW7COVec3L2jSDq\r\n' + + '/7PGMAMiwTMwQ0i5mdASw6Z4RJwjvBWi2Bugw4Fy5EeBfp3bPfnsosXo+nKvCbMf\r\n' + + '9lXmfE6wqWg8p/ha0auCD1nnCOifXX/ZaQ6sQcupZKxve0or6VB2P2uIfwWoz2eu\r\n' + + 'LbqS6Lh1K0VEwf6aq2mOpCfsrNHvBFbwwYescIwrRgFJpsxNqQhHq+f7oZ1iS70=\r\n' + + '-----END CERTIFICATE-----' + /*revokedCertKey: -----BEGIN RSA PRIVATE KEY----- + MIIEpAIBAAKCAQEA2uE/0Ja0o/vLzZ/Nt8ZERU1xnWoVx8tZexkIBci2nY4ocdcV + 2AbcDSO4Ae4UZldDtwwdRGEXGFT5uWKlmMEHFjEkieTvvkJE0rFXShnQeOBYxojh + Sd56GLvapo6KtfJAsrAp/waj/gctoXn/Bl2IbZFKcLiPJhumTMlcokSm8e9BWY6q + 9AEIWYxWVFersu1yDYUsCLokt7ahJR2r6AbgZGiMB3KGtoPPUy9OuHsudGrRMgad + CQXusGF8M4qL1eLCxGUqZO4X7NtTxwn6lU+/eqcM1FQTJ+KvtXa3zv0HR8xbr2Kc + ERZMyBUgQk9al//80b4DwHFIg+Qc5dLLl/qW8wIDAQABAoIBABmpJWrLoFXxTo1x + hL6yNzggwjHVnsQTqmPs0AH4QWCWAAEzWX5AH6BIEGnkLZlp9ahfeow/uGNujZsX + CO8FrP0EZJI+DYAIE5AtS7HboOiVn2gh4re0UNWBgJAyTz98LUBFuEa2fUIU5Aag + X4Oxh4MWRjHnkUYYdmteLXFgtxR55BdeBnq7zV4ypqapb4RZgW96cuC6VctYeTCL + pPwXwn56LtOfKjIAn8+bY2w8aw795TABdHHZ+1bMQirq/I71r/OY4W+DPq/99qX+ + fxIuJl3YiewB8ooA/d2jsiLBpL2214JoCYAO+5Bk53CT3nbX8LcNebQ4YcXDlGrK + oGpEBSECgYEA9Vi26BN+hujzK3IoFzZcI3llohOElzLfIKhIKttmkMe2TGcVPLh9 + aGm7KFJJTo5U6zt/7z9paFBcSYyw2ZeUtmeIA9qrAAJwBTRQ52lz4ofAFILVzFqa + k8Oh0GhsN8HKDmsjhknyi3iBp2u/lrL9Z15nPfS90tyMRA/jlxC9PesCgYEA5GJU + d8A/a5Uo2skgjvDLT6gNPv7gLCZLz7VK9SnJ6vWAeJuzNqDh72aJh1NAtsIuMWDF + CWpT0/AJsnIrCLJQIMzJnQ2mJ6U6PX46HeCg6jW7X9rtRt7Bo3b9DK41ZoBnMez9 + AXjvb1KgNJLs0sw1FnNJxnjjv/6MktqKw5084RkCgYAPuDJn5i/aJvzFkNfevN3k + a/fGDagWI+1F42JUVKBasGEOviAPNubaFMQoDjWiMd5g//vvcUmopFV1ZO1D08F0 + emetj4obQwy4WKTCXvBM2FPHPKbEJB35T7SDbN1aKTFwAQ9SoFRI+VydRHsPBcLU + p6jHwHGVHApkpfv4BtuJJwKBgQDOKx4JhJk760kYSJyFrUY8QH7EsZ14/ZFOjmB+ + dRz8aGdzeUsNM6sCTNQ2P6eZ1C2TEcKNv1ixaG24k2vZy+6dzYDrsFigTX4H6R1Z + v2BETgE6hQ3R/mFbyZyih9lZEO0XmtLDM4MiQbqx+zijCwmZnLWq35LpzUblgzfl + YtqEcQKBgQDsvhGMcEWtklbl/DPOzSiIBxpUQ6BWPEvEL63RFYIjA86sOgp0+zfC + rUJodBGeGmCmSK6hugodL8X9XbHDvvvtfnyF9uLLExR6e5qI39ofmt3KEaKjpYhQ + 0c7g51j4JmQsBgbUSHEMAhFV49WmEapOOqkZces+jSMYxEDOzlOl/Q== + -----END RSA PRIVATE KEY-----*/ + }; + + describe('crl', function() { + it('should be able to read a PEM encoded CRL', function() { + PKI.certificateRevocationListFromPem(_pem.crl); + }); + + it('should be able to read a CRL\'s properties', function() { + var crl = PKI.certificateRevocationListFromPem(_pem.crl); + ASSERT.equal(crl.version, 0x01); + + ASSERT.notEqual(crl.signature, undefined); + ASSERT.notEqual(crl.signature, null); + ASSERT.equal(crl.signatureOid, PKI.oids.sha256WithRSAEncryption); + ASSERT.notEqual(crl.signatureParameters, undefined); + + ASSERT.equal(crl.siginfo.algorithmOid, PKI.oids.sha256WithRSAEncryption); + ASSERT.notEqual(crl.siginfo.signatureParameters, undefined); + + ASSERT.notEqual(crl.tbsCertList, undefined); + + ASSERT.notEqual(crl.issuer.attributes, undefined); + ASSERT.notEqual(crl.issuer.attributes, null); + ASSERT.equal(crl.issuer.hash, '4c7be0031d89818ef4e069d62ae9e500ec2c5812'); + + ASSERT.equal(crl.thisUpdate.toUTCString(), 'Tue, 30 Jul 2024 00:00:00 GMT'); + ASSERT.equal(crl.nextUpdate.toUTCString(), 'Wed, 28 Aug 2024 23:59:59 GMT'); + + ASSERT.notEqual(crl.revocations.length, 0); + ASSERT.notEqual(crl.extensions.length, 0); + }); + + it('should be able to create a digest for a CRL', function() { + var crl = PKI.certificateRevocationListFromPem(_pem.crl, true); + ASSERT.notEqual(crl.md, undefined); + ASSERT.notEqual(crl.md, null); + }); + + it('should be able to identify a Delta CRL', function() { + var crl = PKI.certificateRevocationListFromPem(_pem.crl); + + ASSERT.equal(crl.isDelta(), false); + //TODO: Add delta CRL + }) + + it('should be able to read a CRL\'s extensions', function() { + var crl = PKI.certificateRevocationListFromPem(_pem.crl); + ASSERT.equal(crl.version, 0x01); + + ASSERT.equal(crl.getExtension({id: PKI.oids.cRLNumber}).number, 1); + ASSERT.equal(crl.getExtension({id: PKI.oids.authorityKeyIdentifier}).authorityKeyIdentifier, '8420c0120667a1ebd96e5155c3e45d486b0260ae'); + //TODO: Test more extensions when added + }); + + it('should be able to verify the CRL\'s signer', function() { + var crl = PKI.certificateRevocationListFromPem(_pem.crl, true); + var ca = PKI.certificateFromPem(_pem.caCert); + var nrClient = PKI.certificateFromPem(_pem.nonRevokedCert); + + ASSERT.equal(crl.verify(ca), true); + ASSERT.equal(crl.verify(nrClient), false); + }); + + it('should be able to identify revoked certificates', function() { + var crl = PKI.certificateRevocationListFromPem(_pem.crl); + var rClient = PKI.certificateFromPem(_pem.revokedCert); + var nrClient = PKI.certificateFromPem(_pem.nonRevokedCert); + + ASSERT.equal(crl.isCertRevoked(rClient), true); + ASSERT.equal(crl.isCertRevoked(nrClient), false); + }); + + it('should be able to read a CRL\'s revocation entry & its extensions', function() { + var crl = PKI.certificateRevocationListFromPem(_pem.crl); + var rClient = PKI.certificateFromPem(_pem.revokedCert); + + var revocationEntry = crl.getRevocation(rClient.serialNumber); + ASSERT.notEqual(revocationEntry, null); + + ASSERT.equal(revocationEntry.getExtension({id: PKI.oids.invalidityDate}).invalidSince.toUTCString(), 'Tue, 30 Jul 2024 08:30:00 GMT'); + ASSERT.equal(revocationEntry.getExtension({id: PKI.oids.cRLReason}).reason, 5); + //TODO: Add certificateIssuer when added + }); + }); + +})(); diff --git a/tests/unit/index.js b/tests/unit/index.js index c881a4366..28b4bbcc1 100644 --- a/tests/unit/index.js +++ b/tests/unit/index.js @@ -16,6 +16,7 @@ require('./kem'); require('./pkcs1'); require('./x509'); require('./csr'); +require('./crl') require('./aes'); require('./rc2'); require('./des'); From 49303a7edfad74d4e0605510639eb7b24f005a5b Mon Sep 17 00:00:00 2001 From: El-Virus Date: Tue, 30 Jul 2024 12:33:30 +0200 Subject: [PATCH 3/7] Add check for CRLs with no revocations --- lib/x509.js | 40 ++++++++++++++++++++++------------------ 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/lib/x509.js b/lib/x509.js index 57bc75c20..d312efe9e 100644 --- a/lib/x509.js +++ b/lib/x509.js @@ -3629,26 +3629,30 @@ pki.certificateRevocationListFromAsn1 = function(obj, computeHash) { } // convert revocations - for (const revoked of capture.revokedCertificates) { - // certificateListValidator has already verified the entries - var rci = {}; - - var serial = forge.util.createBuffer(revoked.value[0].value); - rci.serial = serial.toHex(); - - rci.date = asn1.utcTimeToDate(revoked.value[1].value); - - if (revoked.value[2]) { - rci.extensions = pki.certificateRevocationListEntryExtensionsFromAsn1(revoked.value[2]); - } else { - rci.extensions = []; - } + if (capture.revokedCertificates) { + for (const revoked of capture.revokedCertificates) { + // certificateListValidator has already verified the entries + var rci = {}; + + var serial = forge.util.createBuffer(revoked.value[0].value); + rci.serial = serial.toHex(); + + rci.date = asn1.utcTimeToDate(revoked.value[1].value); + + if (revoked.value[2]) { + rci.extensions = pki.certificateRevocationListEntryExtensionsFromAsn1(revoked.value[2]); + } else { + rci.extensions = []; + } - rci.getExtension = function(options) { - return _getExtension(rci.extensions, options); - } + rci.getExtension = function(options) { + return _getExtension(rci.extensions, options); + } - crl.revocations.push(rci); + crl.revocations.push(rci); + } + } else { + crl.revocations = []; } if (capture.tbsExtensions) { From 46bc0ce7662075c4c9e245a046837093a9fbc3c2 Mon Sep 17 00:00:00 2001 From: El-Virus Date: Wed, 14 Aug 2024 18:12:45 +0200 Subject: [PATCH 4/7] Add support for issuerAltName & authorityInfoAccess CRL extensions --- lib/x509.js | 106 ++++++++++++++++++++++++++++++---------------- tests/unit/crl.js | 8 ++-- 2 files changed, 74 insertions(+), 40 deletions(-) diff --git a/lib/x509.js b/lib/x509.js index d312efe9e..33f16b169 100644 --- a/lib/x509.js +++ b/lib/x509.js @@ -963,6 +963,57 @@ _getExtension = function(exts, options) { return rval; }; +/** + * Parses an ASN.1 typed GeneralName + * + * @param generalName ASN.1 typed GeneralName + * + * @returns the parsed GeneralName + */ +function _parseGeneralName(generalName) { + var altName = { + type: generalName.type, + value: generalName.value + }; + + // Defined in RFC 5280 § 4.2.1.6 + switch(generalName.type) { + case 0: // otherName (OtherName) + //TODO: Parse OtherName (RFC 5280 § 4.2.1.6) + break; + case 1: // rfc822Name (IA5String) + // No need to decode, as it's ASCII + break; + case 2: // dNSName (IA5String) + // No need to decode, as it's ASCII + break; + case 3: // x400Address (ORAddress) + //TODO: Parse ORAddress (ISO/IEC 10021-1:2003 § 12.4 / RFC 5280 § A.1) + break; + case 4: // directoryName (Name) + //TODO: Parse Name (RFC 5280 § 4.1.2.4) + break; + case 5: // ediPartyName (EDIPartyName) + //TODO: Parse EDIPartyName (RFC 5280 § 4.2.1.6) + break; + case 6: // uniformResourceIdentifier (IA5String) + // No need to decode, as it's ASCII + break; + case 7: // iPAddress (OCTET STRING) + // convert to IPv4/IPv6 string representation + altName.ip = forge.util.bytesToIP(generalName.value); + break; + case 8: // registeredID (OBJECT IDENTIFIER) + altName.oid = asn1.derToOid(generalName.value); + break; + default: + // this should never happen + //throw new Error("GeneralName type is invalid"); + } + + return altName; +} + /** * Converts an X.509 certificate from PEM format. * @@ -1789,40 +1840,9 @@ pki.certificateExtensionFromAsn1 = function(ext) { e.altNames = []; // ev is a SYNTAX SEQUENCE - var gn; var ev = asn1.fromDer(e.value); - for(var n = 0; n < ev.value.length; ++n) { - // get GeneralName - gn = ev.value[n]; - - var altName = { - type: gn.type, - value: gn.value - }; - e.altNames.push(altName); - - // Note: Support for types 1,2,6,7,8 - switch(gn.type) { - // rfc822Name - case 1: - // dNSName - case 2: - // uniformResourceIdentifier (URI) - case 6: - break; - // IPAddress - case 7: - // convert to IPv4/IPv6 string representation - altName.ip = forge.util.bytesToIP(gn.value); - break; - // registeredID - case 8: - altName.oid = asn1.derToOid(gn.value); - break; - default: - // unsupported - } - } + for (var n = 0; n < ev.value.length; ++n) + e.altNames.push(_parseGeneralName(ev.value[n])); } else if(e.name === 'subjectKeyIdentifier') { // value is an OCTETSTRING w/the hash of the key-type specific // public key structure (eg: RSAPublicKey) @@ -3805,7 +3825,7 @@ pki.certificateRevocationListExtensionFromAsn1 = function(ext) { if (e.name === 'authorityKeyIdentifier') { if (ev.tagClass !== asn1.Class.UNIVERSAL || ev.type !== asn1.Type.SEQUENCE || ev.value[0].tagClass !== asn1.Class.CONTEXT_SPECIFIC) - throw new Error("CRL\'s Authority Key Identifier is not tagged correctly"); + throw new Error('CRL\'s Authority Key Identifier is not tagged correctly'); if (ev.value[0].type === 0) { e.authorityKeyIdentifier = forge.util.bytesToHex(ev.value[0].value) } else if (ev.value[0].type === 1) { @@ -3814,10 +3834,14 @@ pki.certificateRevocationListExtensionFromAsn1 = function(ext) { var serial = forge.util.createBuffer(ev.value[0].value); e.authoritySerial = serial.toHex(); } else { - throw new Error("CRL\'s Authority Key Identifier is not typed correctly"); + throw new Error('CRL\'s Authority Key Identifier is not typed correctly'); } } else if (e.name === 'issuerAltName') { - //TODO: Implement this (RFC 5280 § 5.2.2) + if (ev.tagClass !== asn1.Class.UNIVERSAL || ev.type !== asn1.Type.SEQUENCE) + throw new Error('CRL\'s Issuer Alt Name is not SEQUENCE'); + e.altNames = []; + for (var n = 0; n < ev.value.length; ++n) + e.altNames.push(_parseGeneralName(ev.value[n])); } else if (e.name === 'cRLNumber') { if (ev.tagClass !== asn1.Class.UNIVERSAL || ev.type !== asn1.Type.INTEGER) throw new Error('CRL\'s CRLNumber is not INTEGER'); @@ -3831,7 +3855,15 @@ pki.certificateRevocationListExtensionFromAsn1 = function(ext) { } else if (e.name === 'freshestCRL') { //TODO: Implement this (RFC 5280 § 5.2.6), and activate OID Lookup } else if (e.name === 'authorityInfoAccess') { - //TODO: Implement this (RFC 5280 § 5.2.7) + if (ev.tagClass !== asn1.Class.UNIVERSAL || ev.type !== asn1.Type.SEQUENCE) + throw new Error('CRL\'s Authority Info Access is not SEQUENCE'); + e.authorityInfoAccess = []; + for (var n = 0; n < ev.value.length; ++n) { + var accessDesc = ev.value[n]; + if (accessDesc.value[0].tagClass !== asn1.Class.UNIVERSAL || accessDesc !== asn1.Type.OID) + throw new Error('CRL\'s uthority Info Access\' Access Method is not OID'); + e.authorityInfoAccess.push({method: asn1.derToOid(accessDesc.value[0].value), location: _parseGeneralName(accessDesc.value[1])}); + } } return e; diff --git a/tests/unit/crl.js b/tests/unit/crl.js index 300a42a96..90348111b 100644 --- a/tests/unit/crl.js +++ b/tests/unit/crl.js @@ -195,7 +195,7 @@ var PKI = require('../../lib/pki'); var crl = PKI.certificateRevocationListFromPem(_pem.crl); ASSERT.equal(crl.isDelta(), false); - //TODO: Add delta CRL + //TODO: Add tests for delta CRL }) it('should be able to read a CRL\'s extensions', function() { @@ -204,7 +204,9 @@ var PKI = require('../../lib/pki'); ASSERT.equal(crl.getExtension({id: PKI.oids.cRLNumber}).number, 1); ASSERT.equal(crl.getExtension({id: PKI.oids.authorityKeyIdentifier}).authorityKeyIdentifier, '8420c0120667a1ebd96e5155c3e45d486b0260ae'); - //TODO: Test more extensions when added + //TODO: Add test for issuerAltName + //TODO: Add test for authorityInfoAccess + //TODO: Test more extensions when implemented }); it('should be able to verify the CRL\'s signer', function() { @@ -234,7 +236,7 @@ var PKI = require('../../lib/pki'); ASSERT.equal(revocationEntry.getExtension({id: PKI.oids.invalidityDate}).invalidSince.toUTCString(), 'Tue, 30 Jul 2024 08:30:00 GMT'); ASSERT.equal(revocationEntry.getExtension({id: PKI.oids.cRLReason}).reason, 5); - //TODO: Add certificateIssuer when added + //TODO: Add certificateIssuer when implemented }); }); From bd08e337cc96d0f514e15658f2901f881089bf48 Mon Sep 17 00:00:00 2001 From: El-Virus Date: Sun, 1 Sep 2024 14:38:35 +0200 Subject: [PATCH 5/7] Remove TODO comments for issuerAltName & authorityInfoAccess --- lib/x509.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/x509.js b/lib/x509.js index 33f16b169..6536e2eca 100644 --- a/lib/x509.js +++ b/lib/x509.js @@ -3765,7 +3765,6 @@ pki.certificateRevocationListEntryExtensionFromAsn1 = function(ext) { * } * KeyIdentifier ::= OCTET STRING * - * TODO: * id-ce-issuerAltName OBJECT IDENTIFIER ::= { id-ce 18 } * IssuerAltName ::= GeneralNames * @@ -3792,7 +3791,6 @@ pki.certificateRevocationListEntryExtensionFromAsn1 = function(ext) { * id-ce-freshestCRL OBJECT IDENTIFIER ::= { id-ce 46 } * FreshestCRL ::= CRLDistributionPoints * - * TODO: * id-pe-authorityInfoAccess OBJECT IDENTIFIER ::= { id-pe 1 } * AuthorityInfoAccessSyntax ::= SEQUENCE SIZE (1..MAX) OF AccessDescription * AccessDescription ::= SEQUENCE { @@ -3827,7 +3825,7 @@ pki.certificateRevocationListExtensionFromAsn1 = function(ext) { || ev.value[0].tagClass !== asn1.Class.CONTEXT_SPECIFIC) throw new Error('CRL\'s Authority Key Identifier is not tagged correctly'); if (ev.value[0].type === 0) { - e.authorityKeyIdentifier = forge.util.bytesToHex(ev.value[0].value) + e.authorityKeyIdentifier = forge.util.bytesToHex(ev.value[0].value); } else if (ev.value[0].type === 1) { //TODO: Implement this } else if (ev.value[0].type === 2) { From 3b22c72fda82e0f7100201ad9ddc4496fe1fbbaf Mon Sep 17 00:00:00 2001 From: El-Virus Date: Sun, 1 Sep 2024 14:40:31 +0200 Subject: [PATCH 6/7] Add support for authorityCertIssuer in AuthorityKeyIdentifier CRL extension --- lib/x509.js | 6 ++++-- tests/unit/crl.js | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/x509.js b/lib/x509.js index 6536e2eca..9c9687cf9 100644 --- a/lib/x509.js +++ b/lib/x509.js @@ -3760,7 +3760,7 @@ pki.certificateRevocationListEntryExtensionFromAsn1 = function(ext) { * id-ce-authorityKeyIdentifier OBJECT IDENTIFIER ::= { id-ce 35 } * AuthorityKeyIdentifier ::= SEQUENCE { * keyIdentifier [0] KeyIdentifier OPTIONAL, - * !!!TODO: authorityCertIssuer [1] GeneralNames OPTIONAL, + * authorityCertIssuer [1] GeneralNames OPTIONAL, * authorityCertSerialNumber [2] CertificateSerialNumber OPTIONAL * } * KeyIdentifier ::= OCTET STRING @@ -3827,7 +3827,9 @@ pki.certificateRevocationListExtensionFromAsn1 = function(ext) { if (ev.value[0].type === 0) { e.authorityKeyIdentifier = forge.util.bytesToHex(ev.value[0].value); } else if (ev.value[0].type === 1) { - //TODO: Implement this + e.authorityIssuer = []; + for (var n = 0; n < ev.value[0].value.length; ++n) + e.authorityIssuer.push(_parseGeneralName(ev.value[0].value[n])); } else if (ev.value[0].type === 2) { var serial = forge.util.createBuffer(ev.value[0].value); e.authoritySerial = serial.toHex(); diff --git a/tests/unit/crl.js b/tests/unit/crl.js index 90348111b..6ac816fb3 100644 --- a/tests/unit/crl.js +++ b/tests/unit/crl.js @@ -236,7 +236,7 @@ var PKI = require('../../lib/pki'); ASSERT.equal(revocationEntry.getExtension({id: PKI.oids.invalidityDate}).invalidSince.toUTCString(), 'Tue, 30 Jul 2024 08:30:00 GMT'); ASSERT.equal(revocationEntry.getExtension({id: PKI.oids.cRLReason}).reason, 5); - //TODO: Add certificateIssuer when implemented + //TODO: Add test for certificateIssuer }); }); From 114254d6d822120b190df3f9efbc791504b13c71 Mon Sep 17 00:00:00 2001 From: El-Virus Date: Sun, 1 Sep 2024 14:41:19 +0200 Subject: [PATCH 7/7] Add support for CertificateIssuer CRL Entry extension --- lib/x509.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/x509.js b/lib/x509.js index 9c9687cf9..95a91a5ea 100644 --- a/lib/x509.js +++ b/lib/x509.js @@ -3708,7 +3708,6 @@ pki.certificateRevocationListFromAsn1 = function(obj, computeHash) { * id-ce-invalidityDate OBJECT IDENTIFIER ::= { id-ce 24 } * InvalidityDate ::= GeneralizedTime * - * TODO: * id-ce-certificateIssuer OBJECT IDENTIFIER ::= { id-ce 29 } * CertificateIssuer ::= GeneralNames * @@ -3744,7 +3743,11 @@ pki.certificateRevocationListEntryExtensionFromAsn1 = function(ext) { throw new Error('CRL Entry\'s Invalidity Date is not GENERALIZEDTIME'); e.invalidSince = asn1.generalizedTimeToDate(ev.value); } else if (e.name === 'certificateIssuer') { - // TODO: Implement this (RFC 5280 § 5.3.3) + if (ev.tagClass !== asn1.Class.UNIVERSAL || ev.type !== asn1.Type.SEQUENCE) + throw new Error('Crl Entry\'s CertificateIssuer is not SEQUENCE') + e.certificateIssuer = []; + for (var n = 0; n < ev.value.length; n++) + e.certificateIssuer.push(_parseGeneralName(ev.value[n])) } return e;