@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.
- package/CHANGELOG.md +24 -0
- package/README.md +2 -1
- package/index.js +2 -0
- package/lib/cms-sign.js +7 -218
- package/lib/cms-verify.js +3 -2
- package/lib/lint.js +43 -2
- package/lib/ocsp-verify.js +226 -0
- package/lib/ocsp.js +378 -0
- package/lib/oid.js +1 -1
- package/lib/path-validate.js +131 -197
- package/lib/schema-pkix.js +8 -0
- package/lib/sign-scheme.js +206 -0
- package/lib/webcrypto.js +112 -10
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/path-validate.js
CHANGED
|
@@ -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
|
-
//
|
|
1809
|
-
//
|
|
1810
|
-
//
|
|
1811
|
-
//
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
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
|
|
2025
|
-
if (
|
|
2026
|
-
|
|
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
|
};
|
package/lib/schema-pkix.js
CHANGED
|
@@ -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
|
+
};
|