@blamejs/pki 0.2.20 → 0.2.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -38,6 +38,7 @@ var schema = require("./schema-engine");
38
38
  var x509 = require("./schema-x509");
39
39
  var crl = require("./schema-crl");
40
40
  var ocsp = require("./schema-ocsp");
41
+ var ocspVerify = require("./ocsp-verify");
41
42
  var guard = require("./guard-all");
42
43
  var constants = require("./constants");
43
44
  var validator = require("./validator-all");
@@ -331,6 +332,32 @@ function compositeKeyUsageCheck(cert) {
331
332
  return { ok: true };
332
333
  }
333
334
 
335
+ // The ML-KEM SubjectPublicKeyInfo OIDs (RFC 9935 / FIPS 203). A certificate carrying one of
336
+ // these keys is a KEM key-establishment certificate: it can neither sign nor agree.
337
+ var ML_KEM_OIDS = {};
338
+ ["id-ml-kem-512", "id-ml-kem-768", "id-ml-kem-1024"].forEach(function (n) { ML_KEM_OIDS[oid.byName(n)] = true; });
339
+
340
+ // RFC 9935 sec. 5: a certificate whose SubjectPublicKeyInfo carries an id-ml-kem-* OID, IF it
341
+ // has a keyUsage extension, MUST assert keyEncipherment as the ONLY key usage set -- an ML-KEM
342
+ // key is a key-establishment-only key (it cannot sign or agree, so no other bit is legitimate,
343
+ // and an unnamed/reserved bit set alongside keyEncipherment is equally forbidden). The caller
344
+ // invokes this only for an ML-KEM-keyed certificate; an absent keyUsage places no restriction
345
+ // (RFC 5280 sec. 4.2.1.3). This also makes an ML-KEM "CA" (keyCertSign) an explicit reject.
346
+ function kemKeyUsageCheck(cert) {
347
+ var ku;
348
+ try { ku = decodeExt(cert, OID.keyUsage); }
349
+ catch (e) { return { ok: false, code: "path/kem-key-usage", error: e }; }
350
+ if (!ku) return { ok: true };
351
+ var v = ku.value;
352
+ var others = v.digitalSignature || v.nonRepudiation || v.dataEncipherment || v.keyAgreement ||
353
+ v.keyCertSign || v.cRLSign || v.encipherOnly || v.decipherOnly || (v.reservedBitsSet === true);
354
+ if (!v.keyEncipherment || others) {
355
+ return { ok: false, code: "path/kem-key-usage",
356
+ error: E("path/kem-key-usage", "an ML-KEM key's keyUsage must assert keyEncipherment as the only bit (RFC 9935 sec. 5)") };
357
+ }
358
+ return { ok: true };
359
+ }
360
+
334
361
  // Import a descriptor's verification key, validating an EdDSA point FIRST: node/OpenSSL import a
335
362
  // low-order (e.g. identity or all-zeroes) Ed25519/Ed448 SPKI without complaint and such a key
336
363
  // verifies a forged signature. This is the ONE seam both the certificate path and the revocation
@@ -1215,6 +1242,14 @@ async function validate(path, opts) {
1215
1242
  if (!cku.ok) failed = true;
1216
1243
  }
1217
1244
 
1245
+ // RFC 9935 sec. 5: an ML-KEM-keyed certificate's keyUsage must be keyEncipherment-only.
1246
+ // Runs for the target AND every intermediate whose own subject key is ML-KEM.
1247
+ if (ML_KEM_OIDS[cert.subjectPublicKeyInfo.algorithm.oid]) {
1248
+ var kku = kemKeyUsageCheck(cert);
1249
+ checks.push({ name: "kemKeyUsage", ok: kku.ok, code: kku.ok ? undefined : kku.code });
1250
+ if (!kku.ok) failed = true;
1251
+ }
1252
+
1218
1253
  // 6.1.3(a)(2) validity window.
1219
1254
  var t = opts.time;
1220
1255
  var vOk = true, vCode;
@@ -1805,163 +1840,19 @@ function verifyCrlSignature(theCrl, issuer) {
1805
1840
 
1806
1841
  // ---- the OCSP revocation checker (RFC 6960) -------------------------------
1807
1842
 
1808
- // CertID identity-hash algorithms (RFC 6960 sec. 4.1.1). Kept SEPARATE from the
1809
- // signature SIG_ALGS/HASH_BY_OID set (which omits SHA-1 -- a SHA-1 *signature* is
1810
- // refused, SHAttered): a CertID hash is an identity binding of an already-known
1811
- // issuer, not a signature, so collision resistance is irrelevant to the lookup
1812
- // and RFC 6960's default SHA-1 CertID MUST interoperate. A hash OID outside this
1813
- // set cannot be reproduced -> no CertID match (fail closed), never an assumption.
1814
- var OCSP_CERTID_HASHES = {};
1815
- OCSP_CERTID_HASHES[oid.byName("sha1")] = "SHA-1";
1816
- OCSP_CERTID_HASHES[oid.byName("sha256")] = "SHA-256";
1817
- OCSP_CERTID_HASHES[oid.byName("sha384")] = "SHA-384";
1818
- OCSP_CERTID_HASHES[oid.byName("sha512")] = "SHA-512";
1819
- var OID_OCSP_SIGNING = oid.byName("ocspSigning");
1820
- var OID_OCSP_NOCHECK = oid.byName("ocspNoCheck");
1821
-
1822
- // The subjectPublicKey BIT STRING VALUE (excluding the unused-bits octet) of an
1823
- // SPKI DER -- the exact bytes an OCSP CertID issuerKeyHash / byKey KeyHash hash
1824
- // over (RFC 6960 sec. 4.1.1). Throws on a malformed SPKI; the caller fails closed.
1825
- function ocspKeyValue(spkiDer) {
1826
- return asn1.read.bitString(asn1.decode(spkiDer).children[1]).bytes;
1827
- }
1828
-
1829
- // The delegate responder's importable SPKI. If its key omits algorithm parameters
1830
- // and inherits them from the issuing CA (same key algorithm -- an EC public key
1831
- // whose SPKI omits the namedCurve, RFC 5280 sec. 4.1.2.7), splice the issuer's
1832
- // parameters in so importKey("spki", ...) has a complete key; otherwise a valid
1833
- // response would verify as unknown. Mirrors updateWorkingKey's inheritance.
1834
- function ocspResponderSpki(rc, issuer) {
1835
- var keyAlg = rc.subjectPublicKeyInfo.algorithm;
1836
- if (!isNullOrAbsentParams(keyAlg.parameters)) return rc.subjectPublicKeyInfo.bytes;
1837
- var issuerOid, issuerParams;
1838
- // Coverage residual -- the catch is unreachable: this function runs only after the
1839
- // delegate's signature verified under issuer.workingPublicKey, so that SPKI already
1840
- // imported and decodes cleanly here. (The children[1] ? : null false side IS covered.)
1841
- try {
1842
- var alg = asn1.decode(issuer.workingPublicKey).children[0];
1843
- issuerOid = asn1.read.oid(alg.children[0]);
1844
- issuerParams = alg.children[1] ? alg.children[1].bytes : null;
1845
- } catch (_e) { return rc.subjectPublicKeyInfo.bytes; }
1846
- if (issuerOid === keyAlg.oid && !isNullOrAbsentParams(issuerParams)) {
1847
- return spliceSpkiParameters(rc.subjectPublicKeyInfo, keyAlg.oid, issuerParams);
1848
- }
1849
- return rc.subjectPublicKeyInfo.bytes;
1850
- }
1851
- function ocspDigest(alg, buf) { return subtle.digest(alg, buf).then(function (h) { return Buffer.from(h); }); }
1852
-
1853
- // The checker implements NO OCSP response-extension semantics (the nonce is the
1854
- // live client's, RFC 6960 sec. 4.4.1; CRL References / Archive Cutoff / Service
1855
- // Locator are not consulted), so a CRITICAL responseExtension / singleExtension
1856
- // may change a response's meaning in a way this code cannot honor -- treat it as
1857
- // unusable (RFC 6960 sec. 4.4 / the RFC 5280 critical-extension contract, the OCSP
1858
- // analogue of the crlChecker unhandled-critical skip). Returns true if `extList`
1859
- // carries any critical extension.
1860
- function ocspHasCriticalExtension(extList) {
1861
- if (!extList) return false;
1862
- for (var i = 0; i < extList.length; i++) if (extList[i].critical) return true;
1863
- return false;
1864
- }
1865
-
1866
- // A SingleResponse's CertID names the target cert IFF its serial equals the
1867
- // target's AND issuerNameHash + issuerKeyHash, recomputed under the CertID's OWN
1868
- // hashAlgorithm, match the issuer (RFC 6960 sec. 4.1.1). A serial-only match would
1869
- // let a "good" for issuer A serial N answer a query for issuer B serial N
1870
- // (cross-CA substitution), so BOTH issuer hashes must match under the responder's
1871
- // chosen algorithm -- never an assumed SHA-1.
1872
- //
1873
- // `issuerNameCandidates` is every byte encoding of the (name-chaining-validated)
1874
- // issuer DN to try: the checked cert's issuer field (RFC 6960 sec. 4.1.1) plus,
1875
- // when available, the issuer certificate's own subject encoding. These are RFC
1876
- // 5280 sec. 7.1-equal (they chained) but MAY differ in DER -- case, whitespace, a
1877
- // PrintableString-vs-UTF8String DirectoryString choice -- and a responder MAY have
1878
- // hashed any of them; matching the CertID against any is still the SAME validated
1879
- // issuer, because the issuerKeyHash + serial + authorized signature carry the
1880
- // binding. The key (raw octets, no encoding variance) is matched once.
1881
- async function ocspCertIdMatches(certID, cert, issuerNameCandidates, issuerKeyBits) {
1882
- if (certID.serialNumberHex !== cert.serialNumberHex) return false;
1883
- var hashName = OCSP_CERTID_HASHES[certID.hashAlgorithm.oid];
1884
- if (!hashName) return false; // an unreproducible hash -> the match cannot be confirmed
1885
- var keyHash = await ocspDigest(hashName, issuerKeyBits);
1886
- if (!certID.issuerKeyHash.equals(keyHash)) return false;
1887
- for (var i = 0; i < issuerNameCandidates.length; i++) {
1888
- if (certID.issuerNameHash.equals(await ocspDigest(hashName, issuerNameCandidates[i]))) return true;
1889
- }
1890
- return false;
1891
- }
1892
-
1893
- // Resolve the signer of a BasicOCSPResponse to an AUTHORIZED responder's SPKI DER,
1894
- // or null when none is authorized (RFC 6960 sec. 4.2.2.2). Two models: the issuing
1895
- // CA signing directly (responderID identifies the issuer), or a CA-delegated
1896
- // responder -- a certificate in `certs` issued by the SAME CA, valid at `time`,
1897
- // bearing id-kp-OCSPSigning in its extendedKeyUsage. anyExtendedKeyUsage and an
1898
- // ABSENT EKU do NOT authorize an OCSP delegate (the opposite of the path-validation
1899
- // EKU default), so an ordinary leaf the CA issued cannot forge revocation status.
1900
- // Fails closed at every branch (a malformed delegate, a control-byte DN, an
1901
- // unreadable EKU -> skip that candidate).
1902
- async function ocspAuthorizeResponder(basicResponse, cert, issuer, issuerKeyBits, time) {
1903
- var rid = basicResponse.responderID;
1904
- var matchesIssuer = false;
1905
- try {
1906
- if (rid.byName) matchesIssuer = dnEqual(rid.byName.rdns, cert.issuer.rdns);
1907
- else if (rid.byKey) matchesIssuer = rid.byKey.equals(await ocspDigest("SHA-1", issuerKeyBits));
1908
- } catch (_e) { matchesIssuer = false; }
1909
- if (matchesIssuer) return issuer.workingPublicKey;
1910
-
1911
- for (var i = 0; i < basicResponse.certs.length; i++) {
1912
- var rc;
1913
- try { rc = x509.parse(basicResponse.certs[i]); }
1914
- catch (_e) { continue; }
1915
- var identifies = false;
1916
- try {
1917
- if (rid.byName) identifies = dnEqual(rid.byName.rdns, rc.subject.rdns);
1918
- else if (rid.byKey) identifies = rid.byKey.equals(await ocspDigest("SHA-1", ocspKeyValue(rc.subjectPublicKeyInfo.bytes)));
1919
- } catch (_e) { identifies = false; }
1920
- if (!identifies) continue;
1921
- // The delegate MUST be issued directly by the CA that issued the target.
1922
- var issuedByCa;
1923
- try { issuedByCa = dnEqual(rc.issuer.rdns, cert.issuer.rdns); }
1924
- catch (_e) { continue; }
1925
- if (!issuedByCa) continue;
1926
- if (!guard.crypto.isOctetAligned(rc.signatureValue)) continue;
1927
- if (!(await _verifyWithSpki(rc.signatureAlgorithm, rc.signatureValue.bytes, issuer.workingPublicKey, rc.tbsBytes))) continue;
1928
- // The delegate certificate MUST itself be valid at the validation instant.
1929
- if (time < rc.validity.notBefore || time > rc.validity.notAfter) continue;
1930
- // The delegate MUST assert id-kp-OCSPSigning; anyEKU / an absent EKU do not.
1931
- var eku;
1932
- try { eku = decodeExt(rc, OID.extKeyUsage); }
1933
- catch (_e) { continue; }
1934
- if (!eku || eku.value.indexOf(OID_OCSP_SIGNING) === -1) continue;
1935
- // A delegate asserting keyUsage MUST permit digitalSignature -- signing an OCSP
1936
- // response is a digitalSignature operation (RFC 5280 sec. 4.2.1.3); an absent
1937
- // keyUsage is unrestricted, an unreadable one is not authoritative.
1938
- var ku;
1939
- try { ku = decodeExt(rc, OID.keyUsage); }
1940
- catch (_e) { continue; }
1941
- if (ku && ku.value.digitalSignature !== true) continue;
1942
- // A delegate carrying a critical extension this code does not process is
1943
- // unusable, the same fail-closed rule the path validator applies to any cert
1944
- // (RFC 5280 sec. 6.1.4(o)) -- an unknown critical constraint must not be ignored
1945
- // while its key is trusted to authenticate revocation status. A RECOGNIZED
1946
- // critical extension whose value is malformed is likewise rejected via the
1947
- // shared structure validation, exactly as the path validator rejects it.
1948
- if (unrecognizedCriticalExtension(rc, false)) continue;
1949
- if (validateCriticalExtensionStructure(rc)) continue;
1950
- // The delegate MUST carry id-pkix-ocsp-nocheck (RFC 6960 sec. 4.2.2.2.1): a
1951
- // transport-free checker cannot otherwise confirm the responder certificate has
1952
- // not itself been revoked, and a revoked OCSP-signing certificate would keep
1953
- // signing "good" until its notAfter. A deployment that instead supplies the
1954
- // responder's own status opts in through a future checker; absent nocheck, fail
1955
- // closed (unknown) rather than trust an unvalidated responder.
1956
- if (!findExt(rc, OID_OCSP_NOCHECK)) continue;
1957
- // A composite-keyed delegate is an out-of-path signer cert and gets the same
1958
- // composite keyUsage gate the path certificates do (draft sec. 5.2): a dual-usage
1959
- // composite responder key (a forbidden encryption bit set) is not authorized.
1960
- if (compositeSig.COMPOSITE_ALGS[rc.subjectPublicKeyInfo.algorithm.oid] && !compositeKeyUsageCheck(rc).ok) continue;
1961
- return ocspResponderSpki(rc, issuer);
1962
- }
1963
- return null;
1964
- }
1843
+ // The OCSP response-verification core (responder authorization + CertID binding + currency +
1844
+ // status) lives ONCE in lib/ocsp-verify.js, composed here by pki.path.ocspChecker AND by
1845
+ // pki.ocsp.verify -- there is no second, weaker OCSP verify path. This binds the path-validate-
1846
+ // owned signature engine + RFC 5280 cert-profile gates into that shared core.
1847
+ var ocspCore = ocspVerify.makeOcspVerify({
1848
+ verifyWithSpki: _verifyWithSpki,
1849
+ decodeExt: decodeExt, findExt: findExt,
1850
+ unrecognizedCriticalExtension: unrecognizedCriticalExtension,
1851
+ validateCriticalExtensionStructure: validateCriticalExtensionStructure,
1852
+ compositeKeyUsageCheck: compositeKeyUsageCheck,
1853
+ isNullOrAbsentParams: isNullOrAbsentParams, spliceSpkiParameters: spliceSpkiParameters,
1854
+ dnEqual: dnEqual,
1855
+ });
1965
1856
 
1966
1857
  /**
1967
1858
  * @primitive pki.path.ocspChecker
@@ -2009,57 +1900,26 @@ function ocspChecker(responses) {
2009
1900
  if (issuer.issuerCert) addNameCandidate(issuer.issuerCert.subject);
2010
1901
  addNameCandidate(issuer.workingIssuerName);
2011
1902
  var issuerKeyBits;
2012
- try { issuerKeyBits = ocspKeyValue(issuer.workingPublicKey); }
1903
+ try { issuerKeyBits = ocspVerify.ocspKeyValue(issuer.workingPublicKey); }
2013
1904
  catch (_e) { return { status: "unknown", reason: "the issuer public key could not be read to recompute the OCSP CertID" }; }
2014
1905
 
2015
1906
  // A serial is revoked if ANY authoritative, verified, current response says
2016
1907
  // so -- a clean response must never shadow a revoking one (the crlChecker
2017
1908
  // fail-closed law). "good" needs at least one authoritative match; every
2018
- // other outcome is undetermined.
1909
+ // other outcome is undetermined. The shared verify core evaluates each
1910
+ // response (responder authorization + signature + CertID + currency +
1911
+ // status) and returns a per-response summary this aggregates.
2019
1912
  var revokedResult = null;
2020
1913
  var sawGood = false;
2021
1914
  var sawUnknownStatus = false;
2022
1915
 
2023
1916
  for (var k = 0; k < parsed.length; k++) {
2024
- var resp = parsed[k];
2025
- if (resp.responseStatus.code !== 0) continue; // non-successful -> conveys no status
2026
- var br = resp.basicResponse;
2027
- // Coverage residual -- unreachable for a parsed response: schema-ocsp enforces
2028
- // the successful-status <-> responseBytes biconditional, so a code-0 response
2029
- // always carries a basicResponse. Backstop for a hand-built object.
2030
- if (!br) continue;
2031
- var signerSpki = await ocspAuthorizeResponder(br, cert, issuer, issuerKeyBits, time);
2032
- if (!signerSpki) continue; // unauthorized responder
2033
- if (!(await _verifyWithSpki(br.signatureAlgorithm, br.signature, signerSpki, br.tbsResponseDataBytes))) continue;
2034
- // A critical responseExtension changes the meaning of the WHOLE response
2035
- // and this code processes none -> the response is unusable.
2036
- if (ocspHasCriticalExtension(br.responseExtensions)) continue;
2037
- for (var s = 0; s < br.responses.length; s++) {
2038
- var sr = br.responses[s];
2039
- if (!(await ocspCertIdMatches(sr.certID, cert, issuerNameCandidates, issuerKeyBits))) continue; // not about this cert
2040
- if (ocspHasCriticalExtension(sr.singleExtensions)) continue; // a critical per-response extension this code cannot process -> skip
2041
- if (sr.thisUpdate > time) continue; // not yet valid
2042
- if (!sr.nextUpdate || sr.nextUpdate < time) continue; // no bounded validity / stale -> fail closed
2043
- var st = sr.certStatus;
2044
- if (st.type === "revoked") {
2045
- // A revocation is effective as of its revocationTime. Present-time
2046
- // validation revokes regardless (a future revocationTime is
2047
- // post-dating/skew, never "good"); only an EXPLICIT historical
2048
- // validation defers a strictly-future revocation.
2049
- if (historical && st.revocationTime instanceof Date && st.revocationTime.getTime() > time.getTime()) { sawGood = true; }
2050
- else {
2051
- revokedResult = {
2052
- status: "revoked",
2053
- revocationReason: st.revocationReason || null,
2054
- reason: "certificate reported revoked by an authorized OCSP responder" + (st.revocationReason ? " (" + st.revocationReason + ")" : ""),
2055
- };
2056
- }
2057
- } else if (st.type === "good") {
2058
- sawGood = true;
2059
- } else {
2060
- sawUnknownStatus = true; // an explicit responder "unknown"
2061
- }
1917
+ var v = await ocspCore.evaluateResponse(parsed[k], cert, issuer, issuerKeyBits, issuerNameCandidates, time, historical);
1918
+ if (v.revoked && !revokedResult) {
1919
+ revokedResult = { status: "revoked", revocationReason: v.revoked.revocationReason, reason: v.revoked.reason };
2062
1920
  }
1921
+ if (v.sawGood) sawGood = true;
1922
+ if (v.sawUnknownStatus) sawUnknownStatus = true;
2063
1923
  }
2064
1924
  if (revokedResult) return revokedResult;
2065
1925
  if (sawGood) return { status: "good" };
@@ -2073,8 +1933,82 @@ function ocspChecker(responses) {
2073
1933
  };
2074
1934
  }
2075
1935
 
1936
+ /**
1937
+ * @primitive pki.path.verifyOcspResponse
1938
+ * @signature pki.path.verifyOcspResponse(parsedResponse, cert, issuerCert, time, opts?) -> Promise<{ status, responderAuthorized, signatureValid, matched, thisUpdate, nextUpdate, revocationReason?, reason }>
1939
+ * @since 0.2.22
1940
+ * @status experimental
1941
+ * @spec RFC 6960
1942
+ * @related pki.ocsp.verify, pki.path.ocspChecker
1943
+ *
1944
+ * Verify a single already-parsed OCSP response for one certificate against its
1945
+ * already-parsed issuer certificate at `time` -- the lower-level primitive
1946
+ * `pki.ocsp.verify` composes after parsing its inputs (most callers want that
1947
+ * ergonomic entry, which also handles DER/PEM decoding and request-nonce
1948
+ * matching). It runs the EXACT SAME gates the path validator's `ocspChecker`
1949
+ * does: it locates the SingleResponse whose CertID binds this cert's serial to
1950
+ * its issuer (recomputing `issuerNameHash`/`issuerKeyHash` under the CertID's
1951
+ * own hashAlgorithm), confirms the responder is authorized (the issuing CA
1952
+ * directly, or a CA-issued delegate bearing both id-kp-OCSPSigning and
1953
+ * id-pkix-ocsp-nocheck and passing the full out-of-path certificate gates),
1954
+ * verifies the response signature over `tbsResponseDataBytes`, and checks
1955
+ * currency (`thisUpdate`/`nextUpdate`) -- there is no weaker second OCSP verify
1956
+ * path. It is fail-closed and never throws on an unauthorized, stale, or
1957
+ * unverifiable response: those yield `{ status: "unknown" }` with the granular
1958
+ * `responderAuthorized`/`signatureValid`/`matched` flags and a `reason`; a
1959
+ * `revoked` status surfaces its `revocationReason`. Setting `opts.historicalMode`
1960
+ * treats a revocation whose `revocationTime` is strictly after `time` as not-yet-
1961
+ * revoked (`good`) -- for validating a signature as of a past `time`, before the
1962
+ * certificate was later revoked; the responder certificate is still validated at
1963
+ * `time` either way. `time` must be a valid `Date`. A malformed response's parse
1964
+ * fault surfaces as the parser's typed `ocsp/*` / `asn1/*` error.
1965
+ *
1966
+ * @example
1967
+ * var resp = pki.schema.ocsp.parseResponse(der);
1968
+ * var v = await pki.path.verifyOcspResponse(resp, cert, issuerCert, new Date());
1969
+ * v.status; // "good" | "revoked" | "unknown"
1970
+ */
1971
+ function verifyOcspResponse(parsedResponse, cert, issuerCert, time, opts) {
1972
+ opts = opts || {};
1973
+ // The currency + responder-cert validity windows compare against `time`; a missing or invalid
1974
+ // check date must fail closed (a NaN compares false against every bound), never silently pass.
1975
+ if (!(time instanceof Date) || isNaN(time.getTime())) {
1976
+ return Promise.reject(E("path/bad-input", "verifyOcspResponse: time must be a valid Date (the currency + responder-validity check date)"));
1977
+ }
1978
+ // Bind the supplied issuer certificate to the target: the target's issuer DN must equal the
1979
+ // issuer cert's subject DN AND the target's signature must verify under the issuer's key. A
1980
+ // direct-CA responder is authorized by exactly this issuer identity and the CertID is recomputed
1981
+ // under the issuer's key, so without the cryptographic binding a rogue certificate sharing the
1982
+ // issuer's subject DN (but a different key) could recompute a matching CertID and sign a "good"
1983
+ // response for a certificate that CA never issued. ocspChecker gets an issuer already chained by
1984
+ // the path validator; the standalone entry must establish the binding itself. Fail closed.
1985
+ function unbound(reason) { return { status: "unknown", responderAuthorized: false, signatureValid: false, matched: false, reason: reason }; }
1986
+ var boundName;
1987
+ try { boundName = dnEqual(cert.issuer.rdns, issuerCert.subject.rdns); }
1988
+ catch (e) { return Promise.reject(e); } // an embedded control byte in a DN -> path/name-chaining
1989
+ if (!boundName) {
1990
+ return Promise.resolve(unbound("the supplied issuer certificate's subject does not match the target certificate's issuer"));
1991
+ }
1992
+ return builtinVerify({ workingPublicKey: issuerCert.subjectPublicKeyInfo.bytes }, cert).then(function (sig) {
1993
+ if (!sig.ok) return unbound("the target certificate's signature does not verify under the supplied issuer certificate's key");
1994
+ var issuerCtx = { workingPublicKey: issuerCert.subjectPublicKeyInfo.bytes, workingIssuerName: issuerCert.subject, issuerCert: issuerCert };
1995
+ var issuerNameCandidates = [cert.issuer.bytes];
1996
+ function add(nm) { if (nm && nm.bytes && !issuerNameCandidates.some(function (e) { return e.equals(nm.bytes); })) issuerNameCandidates.push(nm.bytes); }
1997
+ add(issuerCert.subject);
1998
+ var issuerKeyBits;
1999
+ try { issuerKeyBits = ocspVerify.ocspKeyValue(issuerCert.subjectPublicKeyInfo.bytes); }
2000
+ catch (_e) { return unbound("the issuer public key could not be read to recompute the OCSP CertID"); }
2001
+ return ocspCore.evaluateResponse(parsedResponse, cert, issuerCtx, issuerKeyBits, issuerNameCandidates, time, opts.historicalMode === true).then(function (v) {
2002
+ if (v.revoked) return { status: "revoked", responderAuthorized: true, signatureValid: true, matched: true, thisUpdate: v.thisUpdate, nextUpdate: v.nextUpdate, revocationReason: v.revoked.revocationReason, reason: v.revoked.reason };
2003
+ if (v.sawGood) return { status: "good", responderAuthorized: true, signatureValid: true, matched: true, thisUpdate: v.thisUpdate, nextUpdate: v.nextUpdate, reason: "good" };
2004
+ return { status: "unknown", responderAuthorized: v.responderAuthorized === true, signatureValid: v.signatureValid === true, matched: v.matched === true, thisUpdate: v.thisUpdate, nextUpdate: v.nextUpdate, reason: v.reason };
2005
+ });
2006
+ });
2007
+ }
2008
+
2076
2009
  module.exports = {
2077
2010
  validate: validate,
2078
2011
  crlChecker: crlChecker,
2079
2012
  ocspChecker: ocspChecker,
2013
+ verifyOcspResponse: verifyOcspResponse,
2080
2014
  };
@@ -660,6 +660,14 @@ function certExtensionDecoders(ns) {
660
660
  var byte = bit >> 3, mask = 0x80 >> (bit & 7);
661
661
  out[nm] = byte < bs.bytes.length && (bs.bytes[byte] & mask) !== 0;
662
662
  });
663
+ // Any bit beyond the 9 named positions is a reserved/unknown usage. RFC 5280 tolerates it,
664
+ // but a "MUST be the ONLY bit" rule (e.g. RFC 9935 sec. 5 ML-KEM keyEncipherment-only) needs
665
+ // to see it -- so surface it rather than silently drop bits >= 9.
666
+ var reserved = false;
667
+ for (var bit2 = KU_BITS.length; bit2 < bs.bytes.length * 8 && !reserved; bit2++) {
668
+ if ((bs.bytes[bit2 >> 3] & (0x80 >> (bit2 & 7))) !== 0) reserved = true;
669
+ }
670
+ out.reservedBitsSet = reserved;
663
671
  return out;
664
672
  }
665
673
 
@@ -0,0 +1,206 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ //
5
+ // @internal -- no operator-facing namespace. The signature-scheme resolver + signer shared by
6
+ // every producer that signs a raw preimage with a certificate's key: pki.cms.sign (SignerInfo),
7
+ // pki.ocsp.sign (BasicOCSPResponse), and future X.509 / CRL issuance. It owns the ONE key ->
8
+ // { signatureAlgorithm, WebCrypto import/sign params } dispatch across the whole algorithm set
9
+ // (RSA-PKCS1 / RSA-PSS / ECDSA P-256/384/521 / Ed25519 / Ed448 / ML-DSA / SLH-DSA / composite
10
+ // ML-DSA), so a new signer surface inherits the full registry -- PQC included -- instead of
11
+ // re-deriving a partial, drifting copy (Hard rule #2: registry not switch; and no second signer).
12
+ //
13
+ // Error-parameterized like the guard / validator families: the caller passes its typed error
14
+ // factory `E(kind, message, cause)` where kind is "bad-input" | "unsupported-algorithm", so every
15
+ // domain keeps its own code (cms/bad-input vs ocsp/bad-input). The digest choice (a CMS
16
+ // digestAlgorithm concern) is surfaced as `scheme.digest`; the caller that has a digestAlgorithm
17
+ // field (CMS) builds it, the caller that signs raw bytes (OCSP) ignores it.
18
+
19
+ var asn1 = require("./asn1-der");
20
+ var oid = require("./oid");
21
+ var pkcs8 = require("./schema-pkcs8");
22
+ var webcrypto = require("./webcrypto");
23
+ var subtle = webcrypto.webcrypto.subtle;
24
+ var validator = require("./validator-all");
25
+ var compositeSig = require("./composite-sig");
26
+ var b = asn1.build;
27
+ function O(name) { return oid.byName(name); }
28
+
29
+ var HASH = { sha256: "SHA-256", sha384: "SHA-384", sha512: "SHA-512" };
30
+ var NODE_DIGEST = { sha256: "sha256", sha384: "sha384", sha512: "sha512", shake128: "shake128", shake256: "shake256" };
31
+ var PSS_SALT = { "SHA-256": 32, "SHA-384": 48, "SHA-512": 64 };
32
+ var ECDSA_ALG = { sha256: "ecdsaWithSHA256", sha384: "ecdsaWithSHA384", sha512: "ecdsaWithSHA512" };
33
+ // RSA-PKCS1 signatures fold the digest into a combined signature OID when there is NO separate
34
+ // digestAlgorithm field to carry it (X.509 / CRL / OCSP -- RFC 5280 sec. 4.1.1.2, RFC 6960). CMS
35
+ // SignerInfo has that field, so it signs under the bare rsaEncryption key OID instead.
36
+ var RSA_PKCS1_SIG = { sha256: "sha256WithRSAEncryption", sha384: "sha384WithRSAEncryption", sha512: "sha512WithRSAEncryption" };
37
+ var HASH_NAME_BY_OID = {};
38
+ HASH_NAME_BY_OID[O("sha256")] = "sha256";
39
+ HASH_NAME_BY_OID[O("sha384")] = "sha384";
40
+ HASH_NAME_BY_OID[O("sha512")] = "sha512";
41
+ var EC_BY_CURVE_OID = {};
42
+ EC_BY_CURVE_OID[O("prime256v1")] = { curve: "P-256", coordLen: 32 };
43
+ EC_BY_CURVE_OID[O("secp384r1")] = { curve: "P-384", coordLen: 48 };
44
+ EC_BY_CURVE_OID[O("secp521r1")] = { curve: "P-521", coordLen: 66 };
45
+ var MLDSA_BY_OID = {};
46
+ MLDSA_BY_OID[O("id-ml-dsa-44")] = "ML-DSA-44";
47
+ MLDSA_BY_OID[O("id-ml-dsa-65")] = "ML-DSA-65";
48
+ MLDSA_BY_OID[O("id-ml-dsa-87")] = "ML-DSA-87";
49
+ var MLDSA_SUITABLE_DIGEST = {
50
+ "ML-DSA-44": { sha256: 1, sha384: 1, sha512: 1, shake256: 1 },
51
+ "ML-DSA-65": { sha384: 1, sha512: 1, shake256: 1 },
52
+ "ML-DSA-87": { sha512: 1, shake256: 1 },
53
+ };
54
+ var SLHDSA_BY_OID = {};
55
+ [["sha2-128s", "sha256"], ["sha2-128f", "sha256"], ["sha2-192s", "sha512"], ["sha2-192f", "sha512"],
56
+ ["sha2-256s", "sha512"], ["sha2-256f", "sha512"], ["shake-128s", "shake128"], ["shake-128f", "shake128"],
57
+ ["shake-192s", "shake256"], ["shake-192f", "shake256"], ["shake-256s", "shake256"], ["shake-256f", "shake256"]
58
+ ].forEach(function (r) { SLHDSA_BY_OID[O("id-slh-dsa-" + r[0])] = { wc: "SLH-DSA-" + r[0].toUpperCase(), digest: r[1] }; });
59
+
60
+ // An AlgorithmIdentifier { OID } (absent parameters) or { OID, NULL }.
61
+ function _algId(name, shape) { return shape === "null" ? b.sequence([b.oid(O(name)), b.nullValue()]) : b.sequence([b.oid(O(name))]); }
62
+ // The RSASSA-PSS AlgorithmIdentifier: explicit SHA-2 hash, MGF1 keyed to it, hash-length salt,
63
+ // default trailerField omitted (RFC 4055).
64
+ function _pssAlgId(digestName) {
65
+ var hashAlg = b.sequence([b.oid(O(digestName)), b.nullValue()]);
66
+ var mgf = b.sequence([b.oid(O("mgf1")), hashAlg]);
67
+ var params = b.sequence([b.explicit(0, hashAlg), b.explicit(1, mgf), b.explicit(2, b.integer(BigInt(PSS_SALT[HASH[digestName]])))]);
68
+ return b.sequence([b.oid(O("rsassaPss")), params]);
69
+ }
70
+ // An id-RSASSA-PSS SPKI MAY pin its permitted hash in the params (RFC 4055 sec. 1.2 / 3.1). Read
71
+ // it so signing honors the restriction; absent params or an unrecognized hash returns null.
72
+ function _pssHashFromSpki(cert, E) {
73
+ var params = cert.subjectPublicKeyInfo.algorithm.parameters;
74
+ if (params == null) return null;
75
+ var node = asn1.decode(params);
76
+ if (node.tagClass !== "universal" || node.tagNumber !== asn1.TAGS.SEQUENCE || !node.children) return null;
77
+ var hashField = node.children.filter(function (c) { return c.tagClass === "context" && c.tagNumber === 0; })[0];
78
+ if (!hashField || !hashField.children || !hashField.children[0] || !hashField.children[0].children) return null;
79
+ var oidNode = hashField.children[0].children[0];
80
+ if (!oidNode || oidNode.tagClass !== "universal" || oidNode.tagNumber !== asn1.TAGS.OBJECT_IDENTIFIER) return null;
81
+ var pinnedOid = asn1.read.oid(oidNode);
82
+ var name = HASH_NAME_BY_OID[pinnedOid];
83
+ if (!name) throw E("unsupported-algorithm", "the id-RSASSA-PSS signer key pins an unsupported hash algorithm (" + pinnedOid + ")");
84
+ return name;
85
+ }
86
+
87
+ // resolveSignScheme(cert, so, noSignedAttrs, E) -> the signature scheme from the signer cert's
88
+ // public-key algorithm + per-signer opts (so.digestAlgorithm / so.pss / so.combinedRsaSig -- the
89
+ // last folds the digest into a combined RSA signature OID for a caller with no digestAlgorithm
90
+ // field). Returns
91
+ // { composite?, digest, digestAlgId, sigAlgId, imp?, sign?, ecdsaDer?, coordLen? }. `digest`/
92
+ // `digestAlgId` matter only to a caller with a CMS digestAlgorithm field; OCSP ignores them.
93
+ function resolveSignScheme(cert, so, noSignedAttrs, E) {
94
+ so = so || {};
95
+ var alg = cert.subjectPublicKeyInfo.algorithm;
96
+ var keyOid = alg.oid;
97
+ var comp = compositeSig.COMPOSITE_ALGS[keyOid];
98
+ if (comp) {
99
+ if (comp.trad.unsupported) throw E("unsupported-algorithm", "composite " + comp.name + ": " + comp.trad.unsupported);
100
+ if (so.digestAlgorithm && so.digestAlgorithm !== comp.phCms) throw E("bad-input", "composite " + comp.name + " fixes the digestAlgorithm to " + comp.phCms + " (draft-ietf-lamps-cms-composite-sigs sec. 3.4); " + JSON.stringify(so.digestAlgorithm) + " conflicts");
101
+ return { composite: comp, digest: comp.phCms, digestAlgId: _algId(comp.phCms, "absent"), sigAlgId: _algId(comp.name, "absent") };
102
+ }
103
+ if (keyOid === O("rsaEncryption") || keyOid === O("rsassaPss")) {
104
+ var isPssKey = keyOid === O("rsassaPss");
105
+ var pinned = isPssKey ? _pssHashFromSpki(cert, E) : null;
106
+ if (pinned && so.digestAlgorithm && so.digestAlgorithm !== pinned) throw E("bad-input", "the signer key restricts the RSASSA-PSS digest to " + pinned + ", but digestAlgorithm " + JSON.stringify(so.digestAlgorithm) + " was requested");
107
+ var d = so.digestAlgorithm || pinned || "sha256";
108
+ if (!HASH[d]) throw E("unsupported-algorithm", "unsupported RSA digest algorithm " + JSON.stringify(d));
109
+ if (so.pss || isPssKey) return { digest: d, digestAlgId: _algId(d, "absent"), sigAlgId: _pssAlgId(d), imp: { name: "RSA-PSS", hash: HASH[d] }, sign: { name: "RSA-PSS", saltLength: PSS_SALT[HASH[d]] }, ecdsaDer: false };
110
+ var rsaSigAlgId = so.combinedRsaSig ? _algId(RSA_PKCS1_SIG[d], "null") : _algId("rsaEncryption", "null");
111
+ return { digest: d, digestAlgId: _algId(d, "absent"), sigAlgId: rsaSigAlgId, imp: { name: "RSASSA-PKCS1-v1_5", hash: HASH[d] }, sign: { name: "RSASSA-PKCS1-v1_5" }, ecdsaDer: false };
112
+ }
113
+ if (keyOid === O("ecPublicKey")) {
114
+ var curveOid;
115
+ try { curveOid = asn1.read.oid(asn1.decode(alg.parameters)); }
116
+ catch (e) { throw E("unsupported-algorithm", "the signer EC key parameters are not a named-curve OID", e); }
117
+ var ec = EC_BY_CURVE_OID[curveOid];
118
+ if (!ec) throw E("unsupported-algorithm", "the signer key is on an unsupported EC curve");
119
+ var de = so.digestAlgorithm || "sha256";
120
+ if (!HASH[de]) throw E("unsupported-algorithm", "unsupported ECDSA digest algorithm " + JSON.stringify(de));
121
+ return { digest: de, digestAlgId: _algId(de, "absent"), sigAlgId: _algId(ECDSA_ALG[de], "absent"), imp: { name: "ECDSA", namedCurve: ec.curve }, sign: { name: "ECDSA", hash: HASH[de] }, ecdsaDer: true, coordLen: ec.coordLen };
122
+ }
123
+ if (keyOid === O("Ed25519") || keyOid === O("Ed448")) {
124
+ var name = keyOid === O("Ed25519") ? "Ed25519" : "Ed448";
125
+ var dd = so.digestAlgorithm || (name === "Ed25519" ? "sha512" : "shake256");
126
+ if (!NODE_DIGEST[dd]) throw E("unsupported-algorithm", "unsupported " + name + " digest algorithm " + JSON.stringify(dd));
127
+ return { digest: dd, digestAlgId: _algId(dd, "absent"), sigAlgId: _algId(name, "absent"), imp: { name: name }, sign: { name: name }, ecdsaDer: false };
128
+ }
129
+ if (MLDSA_BY_OID[keyOid]) {
130
+ var mlName = MLDSA_BY_OID[keyOid];
131
+ var md;
132
+ if (noSignedAttrs) {
133
+ md = "sha512";
134
+ } else {
135
+ md = so.digestAlgorithm || "sha512";
136
+ if (!NODE_DIGEST[md]) throw E("unsupported-algorithm", "unsupported ML-DSA message digest " + JSON.stringify(md));
137
+ if (!MLDSA_SUITABLE_DIGEST[mlName][md]) throw E("unsupported-algorithm", "the " + md + " message digest is below the security strength of " + mlName + " (RFC 9882 sec. 3.3)");
138
+ }
139
+ return { digest: md, digestAlgId: _algId(md, "absent"), sigAlgId: _algId(oid.name(keyOid), "absent"), imp: { name: mlName }, sign: { name: mlName }, ecdsaDer: false };
140
+ }
141
+ if (SLHDSA_BY_OID[keyOid]) {
142
+ var slh = SLHDSA_BY_OID[keyOid];
143
+ if (so.digestAlgorithm && so.digestAlgorithm !== slh.digest) throw E("bad-input", "SLH-DSA " + slh.wc + " requires the " + slh.digest + " message digest (RFC 9814 sec. 4); digestAlgorithm " + JSON.stringify(so.digestAlgorithm) + " conflicts");
144
+ return { digest: slh.digest, digestAlgId: _algId(slh.digest, "absent"), sigAlgId: _algId(oid.name(keyOid), "absent"), imp: { name: slh.wc }, sign: { name: slh.wc }, ecdsaDer: false };
145
+ }
146
+ throw E("unsupported-algorithm", "unsupported signer key algorithm " + keyOid);
147
+ }
148
+
149
+ // A CryptoKey's algorithm must match the resolved scheme (name / hash / curve).
150
+ function _assertKeyMatchesScheme(key, imp, E) {
151
+ var ka = key.algorithm || {};
152
+ if (ka.name !== imp.name) throw E("bad-input", "the signer CryptoKey algorithm (" + ka.name + ") does not match the certificate's key algorithm (" + imp.name + ")");
153
+ if (imp.hash && (!ka.hash || ka.hash.name !== imp.hash)) throw E("bad-input", "the signer CryptoKey hash (" + (ka.hash && ka.hash.name) + ") does not match the signing digest (" + imp.hash + ")");
154
+ if (imp.namedCurve && ka.namedCurve !== imp.namedCurve) throw E("bad-input", "the signer CryptoKey curve (" + ka.namedCurve + ") does not match the certificate curve (" + imp.namedCurve + ")");
155
+ }
156
+ function _normPkcs8(k, label, E) {
157
+ if (Buffer.isBuffer(k)) return k;
158
+ if (k instanceof Uint8Array) return Buffer.from(k);
159
+ if (typeof k === "string") { try { return pkcs8.pemDecode(k); } catch (e) { throw E("bad-input", label + " PEM could not be decoded", e); } }
160
+ throw E("bad-input", label + " must be a PKCS#8 DER Buffer, Uint8Array, or PEM string");
161
+ }
162
+ function _normCompositeKeys(key, comp, E) {
163
+ if (!key || typeof key !== "object" || Buffer.isBuffer(key) || key instanceof Uint8Array || key.mldsa == null || key.trad == null) {
164
+ throw E("bad-input", "a composite " + comp.name + " signer key must be { mldsa: <PKCS#8>, trad: <PKCS#8> }");
165
+ }
166
+ return { mldsa: _normPkcs8(key.mldsa, "the composite ML-DSA component key", E), trad: _normPkcs8(key.trad, "the composite traditional component key", E) };
167
+ }
168
+ function _importKey(key, imp, E) {
169
+ if (key && typeof key === "object" && !Buffer.isBuffer(key) && !(key instanceof Uint8Array) && key.type === "private") {
170
+ _assertKeyMatchesScheme(key, imp, E);
171
+ return Promise.resolve(key);
172
+ }
173
+ var der;
174
+ if (Buffer.isBuffer(key)) der = key;
175
+ else if (key instanceof Uint8Array) der = Buffer.from(key);
176
+ else if (typeof key === "string") { try { der = pkcs8.pemDecode(key); } catch (e) { throw E("bad-input", "the signer PEM private key could not be decoded", e); } }
177
+ else throw E("bad-input", "a signer key must be a CryptoKey, a PKCS#8 DER Buffer, or a PKCS#8 PEM string");
178
+ return subtle.importKey("pkcs8", der, imp, false, ["sign"]);
179
+ }
180
+
181
+ // signOverTbs(scheme, key, signedBytes, E) -> Promise<Buffer> the raw signature over signedBytes.
182
+ // The classical path imports the key + signs (re-encoding ECDSA to canonical DER); the composite
183
+ // path signs BOTH component keys and returns mldsaSig || tradSig (composite-sig.js owns it).
184
+ function signOverTbs(scheme, key, signedBytes, E) {
185
+ if (scheme.composite) {
186
+ return compositeSig.compositeSign(scheme.composite, _normCompositeKeys(key, scheme.composite, E), signedBytes).then(function (sig) { return Buffer.from(sig); });
187
+ }
188
+ return _importKey(key, scheme.imp, E).then(function (priv) {
189
+ return subtle.sign(scheme.sign, priv, signedBytes).then(function (sigRaw) {
190
+ var sig = Buffer.from(sigRaw);
191
+ if (scheme.ecdsaDer) sig = validator.sig.rawToEcdsaDer(sig, scheme.coordLen);
192
+ return sig;
193
+ });
194
+ });
195
+ }
196
+
197
+ // MLDSA_SUITABLE_DIGEST + SLHDSA_BY_OID are the RFC 9882 sec. 3.3 / RFC 9814 sec. 4 per-parameter-
198
+ // set digest policy, exported so the VERIFY side (cms-verify) shares the EXACT same table this
199
+ // resolver signs under -- a digest accepted on sign is precisely the set accepted on verify, with
200
+ // no drift between the two.
201
+ module.exports = {
202
+ resolveSignScheme: resolveSignScheme,
203
+ signOverTbs: signOverTbs,
204
+ MLDSA_SUITABLE_DIGEST: MLDSA_SUITABLE_DIGEST,
205
+ SLHDSA_BY_OID: SLHDSA_BY_OID,
206
+ };