@blamejs/pki 0.2.21 → 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 +10 -0
- package/README.md +1 -0
- package/index.js +2 -0
- package/lib/cms-sign.js +7 -218
- package/lib/cms-verify.js +3 -2
- package/lib/ocsp-verify.js +226 -0
- package/lib/ocsp.js +378 -0
- package/lib/path-validate.js +97 -197
- package/lib/sign-scheme.js +206 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,16 @@ All notable changes to `@blamejs/pki` are documented here. The format
|
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
|
|
5
5
|
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## v0.2.22 — 2026-07-16
|
|
8
|
+
|
|
9
|
+
The RFC 6960 OCSP producer and relying-party surface arrives as pki.ocsp: build and sign OCSP requests and responses, mint unsigned error responses, and verify a response end to end against the same hardened responder-authorization, signature, CertID, currency, and nonce gates the certification-path revocation checker runs.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- pki.ocsp is the RFC 6960 OCSP request/response surface. pki.ocsp.buildRequest(query, opts) builds an OCSPRequest (one or many { cert, issuer } queries; CertID under SHA-1 or SHA-2; optional RFC 9654 nonce; optional requestor signature; the RFC 5019 lightweight profile). pki.ocsp.sign(responseData, responder, opts) signs a BasicOCSPResponse over the exact ResponseData DER for the issuing CA or a delegated responder, with good / revoked / unknown per-certificate status and any pki.cms.sign signature algorithm (RSA, RSASSA-PSS, ECDSA, EdDSA, the post-quantum ML-DSA and SLH-DSA sets). pki.ocsp.buildErrorResponse(status) mints the unsigned section 2.3 error response. Transport-free; malformed input throws a typed OcspError.
|
|
14
|
+
- pki.ocsp.verify(response, opts) verifies an OCSP response as a relying party, fail-closed: it binds the supplied issuer certificate to the target certificate (the target's issuer name must equal the issuer's subject and the target's signature must verify under the issuer's key), recomputes the CertID under its own hash algorithm to bind the checked certificate to its issuer, requires an authorized responder (the issuing CA, or a CA-issued delegate bearing id-kp-OCSPSigning and id-pkix-ocsp-nocheck that passes the full out-of-path certificate gates), verifies the signature over tbsResponseDataBytes, enforces thisUpdate / nextUpdate currency, and binds the request nonce (RFC 9654) under a constant-time comparison. A revoked status shadows good within a response; an unbound issuer, an unauthorized responder, a mismatched CertID, or a stale or unverifiable response returns { status: "unknown" } with granular responderAuthorized / signatureValid / matched flags -- never a silent accept. It runs the exact responder-authorization, signature, and currency gates pki.path.ocspChecker runs, through one shared core.
|
|
15
|
+
- pki.path.verifyOcspResponse(parsedResponse, cert, issuerCert, time, opts) is the lower-level primitive pki.ocsp.verify composes: it verifies a single already-parsed OCSP response for one certificate against its already-parsed issuer, returning the same fail-closed granular verdict, for callers that have already decoded their inputs.
|
|
16
|
+
|
|
7
17
|
## v0.2.21 — 2026-07-15
|
|
8
18
|
|
|
9
19
|
ML-KEM public keys in X.509 certificates and PKCS#8 private keys (RFC 9935 / FIPS 203) become a first-class, fail-closed surface: certification-path validation enforces the keyEncipherment-only key usage, key import validates the private-key CHOICE and rejects an inconsistent key with a typed error, and pki.lint gains the RFC 9935 certificate rows.
|
package/README.md
CHANGED
|
@@ -223,6 +223,7 @@ is callable today; nothing below is a stub.
|
|
|
223
223
|
| `pki.path` | RFC 5280 §6 certification-path validation — `validate` runs the §6.1 state machine (signature chaining across RSA, ECDSA, EdDSA, ML-DSA, SLH-DSA and hybrid composite ML-DSA signatures — a composite is accepted only when **both** its post-quantum and traditional components verify; validity windows, name chaining, basic constraints and path length, key usage, name constraints, the certificate-policy tree) over an ordered path and a trust anchor, returning a structured verdict with per-check reason codes, and enforces a `pki.trust` anchor's per-purpose distrust-after dates and delegator purposes via `checkPurpose`; `crlChecker` supplies CRL-based revocation — including partitioned/sharded CRLs, whose §6.3.3 Distribution Point ↔ IDP correspondence lets a corresponding full-reason shard establish non-revocation — and `ocspChecker` supplies OCSP-based revocation (RFC 6960 — CertID binding, responder authorization, signature, currency) over the same pluggable hook. Pure and re-entrant, fail-closed — `validate`, `crlChecker`, `ocspChecker` |
|
|
224
224
|
| `pki.cms` | RFC 5652 §5 CMS SignedData signing + signature verification — `sign(content, signers, opts)` produces a SignedData (attached or detached, one or many signers, RSA / RSASSA-PSS / ECDSA / EdDSA, the post-quantum ML-DSA-44/65/87 (RFC 9882) and SLH-DSA (all twelve FIPS 205 sets, RFC 9814), and composite ML-DSA (pairing ML-DSA with a traditional RSA / ECDSA / EdDSA — accepted only when **both** components verify — draft-ietf-lamps-cms-composite-sigs)); it builds the signed attributes (content-type, message-digest, signing-time) as canonical DER, signs the exact §5.4 preimage, and emits a DER `Buffer` or PEM. `verify(input, opts)` parses a SignedData over the strict `pki.schema.cms` codec, locates each SignerInfo's signer certificate by its issuerAndSerialNumber or subjectKeyIdentifier, and checks the signature over the exact §5.4 preimage: when signed attributes are present it confirms the message-digest attribute equals the content digest and verifies over the DER re-encoding of the SignedAttributes (the on-wire `[0]` tag replaced by a universal SET OF), otherwise directly over the content. It returns a per-signer verdict with the matched signer certificate; it does not chain that certificate to a trust anchor — that is the caller's step through `pki.path.validate`. Fail-closed with typed `cms/*` errors — `sign`, `verify` |
|
|
225
225
|
| `pki.tsp` | RFC 3161 Time-Stamp Protocol — `sign(messageImprint, tsa, opts)` produces a TimeStampToken: a CMS SignedData (over `pki.cms.sign`) whose content is a `TSTInfo` carrying the timestamped message imprint, the TSA policy, a serial number, and `genTime` (with optional accuracy / nonce / ordering), plus the RFC 3161 §2.4.2 signing-certificate attribute binding the token to the TSA certificate (SHA-2 imprints, any `pki.cms.sign` TSA key). `request` / `parseRequest` build and parse the TimeStampReq a client sends (imprint, requested policy, nonce, certReq), `response` / `parseResponse` the TimeStampResp a TSA returns — a granted status wrapping a token, or a rejection with PKIStatus and failure info, the §2.4.2 status↔token coupling enforced in both directions. `verify(token, data, opts)` verifies a token fail-closed: the CMS signature over the exact signed bytes, the message imprint recomputed from the data, the TSTInfo content type, the ESSCertID(V2) binding to the TSA certificate, the §2.3 critical timeStamping-only extendedKeyUsage, the request nonce when used, and — with a trust anchor supplied — full certification-path validation of the TSA certificate at the token's `genTime`, returning `{ valid, genTime, serialNumber, tstInfo, … }` — `sign`, `request`, `parseRequest`, `response`, `parseResponse`, `verify` |
|
|
226
|
+
| `pki.ocsp` | RFC 6960 Online Certificate Status Protocol — the responder and relying-party surface. `buildRequest(query, opts)` builds an OCSPRequest for one or more `{ cert, issuer }` pairs (CertID hashed under SHA-1 by default per the RFC 5019 lightweight profile, or SHA-2; optional RFC 9654 nonce, optional requestor signature). `sign(responseData, responder, opts)` produces a signed BasicOCSPResponse over the exact `ResponseData` DER — the issuing CA directly or a delegated responder, any `pki.cms.sign` key including the post-quantum ML-DSA / SLH-DSA sets, with `good` / `revoked` (reason + time) / `unknown` per-certificate status, and `buildErrorResponse(status)` the unsigned §2.3 error (`tryLater` / `unauthorized` / …). `verify(response, opts)` verifies a response fail-closed against the same hardened gates `pki.path.ocspChecker` runs: the CertID binding, responder authorization (the issuing CA or a CA-issued delegate bearing id-kp-OCSPSigning **and** id-pkix-ocsp-nocheck, passing the full out-of-path certificate gates), the signature over `tbsResponseDataBytes`, currency (`thisUpdate`/`nextUpdate`), and the request-nonce echo — returning `{ status: "good" / "revoked" / "unknown", … }`, never a silent accept. Transport-free — `buildRequest`, `sign`, `buildErrorResponse`, `verify` |
|
|
226
227
|
| `pki.ct` | RFC 6962 Certificate Transparency SCTs — `parseSctList` decodes the `SignedCertificateTimestampList` a certificate or OCSP response carries in the SCT extension (a TLS-presentation-language payload inside the §3.3 double DER wrap) into per-SCT log id, exact `timestamp` (BigInt), named signature algorithm, and raw signature; `reconstructSignedData` rebuilds the exact `digitally-signed` preimage; `verifySct` verifies an SCT signature against a log's public key by reconstructing the signed data, routing an ECDSA signature through the strict DER-conformance gate, and verifying through the crypto engine — resolving true or false, and throwing a typed error on a structural fault. Structure decoded, crypto fail-closed — `parseSctList`, `reconstructSignedData`, `verifySct` |
|
|
227
228
|
| `pki.merkle` | RFC 6962 / RFC 9162 Merkle-tree proof verification — `leafHash` / `nodeHash` / `emptyRootHash` build the domain-separated (0x00 leaf / 0x01 node) SHA-256 tree hashes; `verifyInclusion` folds an audit proof back to a root and `verifyConsistency` reconstructs both the old and new root (the append-only guarantee), each constant-time-compared to a trusted checkpoint root. Fail-closed on bad geometry, sync hashing, transport-free — `leafHash`, `nodeHash`, `emptyRootHash`, `verifyInclusion`, `verifyConsistency` |
|
|
228
229
|
| `pki.trust` | Mozilla / CCADB trust-store ingestion — `parseCertdata` reads the NSS `certdata.txt` object stream and `parseCcadbCsv` the CCADB CSV export into one identical constraint-carrying anchor shape: the per-purpose trust bits (only `CKT_NSS_TRUSTED_DELEGATOR` grants) and the per-purpose distrust-after dates the bare root list omits. Certificate and trust objects pair by byte-exact issuer + serial (never adjacency) and are cross-checked against the parsed DER, so metadata can never attach to the wrong root; `anchor()` hands an entry to `pki.path.validate({ trustAnchor, checkPurpose })`. Offline, fail-closed, bounded — `parseCertdata`, `parseCcadbCsv`, `anchor` |
|
package/index.js
CHANGED
|
@@ -38,6 +38,7 @@ var path = require("./lib/path-validate");
|
|
|
38
38
|
var ct = require("./lib/ct");
|
|
39
39
|
var cms = require("./lib/cms-verify");
|
|
40
40
|
var tsp = require("./lib/tsp-sign");
|
|
41
|
+
var ocsp = require("./lib/ocsp");
|
|
41
42
|
var merkle = require("./lib/merkle");
|
|
42
43
|
var shbs = require("./lib/shbs");
|
|
43
44
|
var hpke = require("./lib/hpke");
|
|
@@ -77,6 +78,7 @@ module.exports = {
|
|
|
77
78
|
ct: ct,
|
|
78
79
|
cms: cms,
|
|
79
80
|
tsp: tsp,
|
|
81
|
+
ocsp: ocsp,
|
|
80
82
|
// `merkle` is the RFC 6962 / RFC 9162 Merkle-tree proof-verification core --
|
|
81
83
|
// pki.merkle.leafHash / nodeHash / emptyRootHash build the domain-separated
|
|
82
84
|
// (0x00 leaf / 0x01 node) SHA-256 tree hashes; pki.merkle.verifyInclusion and
|
package/lib/cms-sign.js
CHANGED
|
@@ -17,101 +17,25 @@ var nodeCrypto = require("crypto");
|
|
|
17
17
|
var asn1 = require("./asn1-der");
|
|
18
18
|
var oid = require("./oid");
|
|
19
19
|
var x509 = require("./schema-x509");
|
|
20
|
-
var pkcs8 = require("./schema-pkcs8");
|
|
21
20
|
var pkix = require("./schema-pkix");
|
|
22
|
-
var webcrypto = require("./webcrypto");
|
|
23
|
-
var subtle = webcrypto.webcrypto.subtle;
|
|
24
|
-
var validator = require("./validator-all");
|
|
25
|
-
var compositeSig = require("./composite-sig");
|
|
26
21
|
var frameworkError = require("./framework-error");
|
|
27
22
|
|
|
23
|
+
var signScheme = require("./sign-scheme");
|
|
28
24
|
var CmsError = frameworkError.CmsError;
|
|
29
25
|
var b = asn1.build;
|
|
30
26
|
function _err(code, message, cause) { return new CmsError(code, message, cause); }
|
|
27
|
+
// The domain error factory the shared sign-scheme resolver/signer throws through (kind ->
|
|
28
|
+
// cms/<kind>), so its faults keep the cms/* codes.
|
|
29
|
+
function _signE(kind, message, cause) { return new CmsError("cms/" + kind, message, cause); }
|
|
31
30
|
function O(name) { return oid.byName(name); }
|
|
32
31
|
|
|
33
|
-
// A digest-algorithm name -> the WebCrypto hash (sign path). SHAKE256 has no WebCrypto hash, so
|
|
34
|
-
// the message digest is always computed with node:crypto (below), uniform across the family.
|
|
35
|
-
var HASH = { sha256: "SHA-256", sha384: "SHA-384", sha512: "SHA-512" };
|
|
36
32
|
var NODE_DIGEST = { sha256: "sha256", sha384: "sha384", sha512: "sha512", shake128: "shake128", shake256: "shake256" };
|
|
37
33
|
var SHAKE_OUT = { shake128: 32, shake256: 64 }; // SHAKE output lengths: SHAKE128 256-bit (RFC 9814 sec. 4), SHAKE256 512-bit (RFC 8419 sec. 2.3 / RFC 9814)
|
|
38
|
-
var PSS_SALT = { "SHA-256": 32, "SHA-384": 48, "SHA-512": 64 };
|
|
39
|
-
var ECDSA_ALG = { sha256: "ecdsaWithSHA256", sha384: "ecdsaWithSHA384", sha512: "ecdsaWithSHA512" };
|
|
40
|
-
var HASH_NAME_BY_OID = {};
|
|
41
|
-
HASH_NAME_BY_OID[O("sha256")] = "sha256";
|
|
42
|
-
HASH_NAME_BY_OID[O("sha384")] = "sha384";
|
|
43
|
-
HASH_NAME_BY_OID[O("sha512")] = "sha512";
|
|
44
|
-
var EC_BY_CURVE_OID = {};
|
|
45
|
-
EC_BY_CURVE_OID[O("prime256v1")] = { curve: "P-256", coordLen: 32 };
|
|
46
|
-
EC_BY_CURVE_OID[O("secp384r1")] = { curve: "P-384", coordLen: 48 };
|
|
47
|
-
EC_BY_CURVE_OID[O("secp521r1")] = { curve: "P-521", coordLen: 66 };
|
|
48
|
-
// ML-DSA (RFC 9882): the signer cert SPKI algorithm OID -> its WebCrypto name. Registry-keyed
|
|
49
|
-
// (Hard rule #2, no dotted-decimal literal). The same OID identifies both the key and the signature.
|
|
50
|
-
var MLDSA_BY_OID = {};
|
|
51
|
-
MLDSA_BY_OID[O("id-ml-dsa-44")] = "ML-DSA-44";
|
|
52
|
-
MLDSA_BY_OID[O("id-ml-dsa-65")] = "ML-DSA-65";
|
|
53
|
-
MLDSA_BY_OID[O("id-ml-dsa-87")] = "ML-DSA-87";
|
|
54
|
-
// The message-digest algorithms suitable for each ML-DSA parameter set (RFC 9882 sec. 3.3 /
|
|
55
|
-
// Table 1): a digest MUST offer at least the parameter set's collision strength (lambda) and at
|
|
56
|
-
// least 2*lambda bits of output. SHA-512 is suitable for all sets; SHAKE256 (512-bit output) too.
|
|
57
|
-
// Below-strength digests downgrade the signature to the weaker of the two (the whichever-is-lower
|
|
58
|
-
// rule), so they are refused fail-closed on sign and verify -- the RFC's "verifiers MAY reject".
|
|
59
|
-
var MLDSA_SUITABLE_DIGEST = {
|
|
60
|
-
"ML-DSA-44": { sha256: 1, sha384: 1, sha512: 1, shake256: 1 },
|
|
61
|
-
"ML-DSA-65": { sha384: 1, sha512: 1, shake256: 1 },
|
|
62
|
-
"ML-DSA-87": { sha512: 1, shake256: 1 },
|
|
63
|
-
};
|
|
64
|
-
// SLH-DSA (RFC 9814): the twelve pure FIPS 205 sets. The signer cert SPKI algorithm OID -> its
|
|
65
|
-
// WebCrypto set name ("SLH-DSA-"+SET, mirroring webcrypto's SLH_DSA_NODE / path-validate) + the
|
|
66
|
-
// RFC 9814 sec. 4 pinned message digest for that set. The sig and key share one OID; the digest is
|
|
67
|
-
// FIXED per set (no caller choice, unlike ML-DSA's suitable-set). Registry-keyed (Hard rule #2).
|
|
68
|
-
var SLHDSA_BY_OID = {};
|
|
69
|
-
[["sha2-128s", "sha256"], ["sha2-128f", "sha256"], ["sha2-192s", "sha512"], ["sha2-192f", "sha512"],
|
|
70
|
-
["sha2-256s", "sha512"], ["sha2-256f", "sha512"], ["shake-128s", "shake128"], ["shake-128f", "shake128"],
|
|
71
|
-
["shake-192s", "shake256"], ["shake-192f", "shake256"], ["shake-256s", "shake256"], ["shake-256f", "shake256"]
|
|
72
|
-
].forEach(function (r) { SLHDSA_BY_OID[O("id-slh-dsa-" + r[0])] = { wc: "SLH-DSA-" + r[0].toUpperCase(), digest: r[1] }; });
|
|
73
34
|
|
|
74
35
|
var OID_DATA = O("data");
|
|
75
36
|
var OID_SIGNED_DATA = O("signedData");
|
|
76
37
|
var OID_SKI = O("subjectKeyIdentifier");
|
|
77
38
|
|
|
78
|
-
// An AlgorithmIdentifier { OID } (absent parameters) or { OID, NULL }.
|
|
79
|
-
function _algId(name, shape) { return shape === "null" ? b.sequence([b.oid(O(name)), b.nullValue()]) : b.sequence([b.oid(O(name))]); }
|
|
80
|
-
// The RSASSA-PSS AlgorithmIdentifier with the params SEQUENCE cms.verify's _resolvePss accepts:
|
|
81
|
-
// an explicit SHA-2 hashAlgorithm, MGF1 keyed to the same hash, the hash-length saltLength, and
|
|
82
|
-
// the default trailerField (omitted). RFC 4055.
|
|
83
|
-
function _pssAlgId(digestName) {
|
|
84
|
-
var hashAlg = b.sequence([b.oid(O(digestName)), b.nullValue()]);
|
|
85
|
-
var mgf = b.sequence([b.oid(O("mgf1")), hashAlg]);
|
|
86
|
-
var params = b.sequence([b.explicit(0, hashAlg), b.explicit(1, mgf), b.explicit(2, b.integer(BigInt(PSS_SALT[HASH[digestName]])))]);
|
|
87
|
-
return b.sequence([b.oid(O("rsassaPss")), params]);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// An id-RSASSA-PSS SubjectPublicKeyInfo MAY pin the permitted hash in its RSASSA-PSS-params
|
|
91
|
-
// (RFC 4055 sec. 1.2 / sec. 3.1): a key generated for SHA-384 cannot sign under SHA-256. Read the
|
|
92
|
-
// pinned digest name from the SPKI algorithm parameters so signing honors the key's restriction
|
|
93
|
-
// instead of hard-coding SHA-256. Absent params (an unrestricted key) or an unrecognized hash
|
|
94
|
-
// returns null -- the caller then falls back to its opts.digestAlgorithm or the SHA-256 default.
|
|
95
|
-
function _pssHashFromSpki(cert) {
|
|
96
|
-
var params = cert.subjectPublicKeyInfo.algorithm.parameters;
|
|
97
|
-
if (params == null) return null; // an unrestricted id-RSASSA-PSS key
|
|
98
|
-
// params is the raw parameters TLV the strict codec already validated when it parsed the cert,
|
|
99
|
-
// so re-decoding a single valid TLV cannot throw. Read the pinned hash structurally, defaulting
|
|
100
|
-
// to null (caller falls back) whenever the shape is not the expected RSASSA-PSS-params.
|
|
101
|
-
var node = asn1.decode(params);
|
|
102
|
-
if (node.tagClass !== "universal" || node.tagNumber !== asn1.TAGS.SEQUENCE || !node.children) return null;
|
|
103
|
-
var hashField = node.children.filter(function (c) { return c.tagClass === "context" && c.tagNumber === 0; })[0];
|
|
104
|
-
if (!hashField || !hashField.children || !hashField.children[0] || !hashField.children[0].children) return null;
|
|
105
|
-
var oidNode = hashField.children[0].children[0]; // [0] EXPLICIT AlgorithmIdentifier { hash OID, ... }
|
|
106
|
-
if (!oidNode || oidNode.tagClass !== "universal" || oidNode.tagNumber !== asn1.TAGS.OBJECT_IDENTIFIER) return null;
|
|
107
|
-
var pinnedOid = asn1.read.oid(oidNode);
|
|
108
|
-
var name = HASH_NAME_BY_OID[pinnedOid];
|
|
109
|
-
// A recognized SHA-2 pin resolves to its name; an unsupported pinned hash (e.g. SHA-3) fails
|
|
110
|
-
// closed -- we cannot honor the key's restriction, so we never silently sign under a different
|
|
111
|
-
// digest the key forbids.
|
|
112
|
-
if (!name) throw _err("cms/unsupported-algorithm", "the id-RSASSA-PSS signer key pins an unsupported hash algorithm (" + pinnedOid + ")");
|
|
113
|
-
return name;
|
|
114
|
-
}
|
|
115
39
|
|
|
116
40
|
// The message digest of the content under the digest algorithm (SHA-2 or SHAKE256).
|
|
117
41
|
function _digest(digestName, content) {
|
|
@@ -121,83 +45,6 @@ function _digest(digestName, content) {
|
|
|
121
45
|
return h.update(content).digest();
|
|
122
46
|
}
|
|
123
47
|
|
|
124
|
-
// Resolve the sign scheme from the signer certificate's public-key algorithm + per-signer opts:
|
|
125
|
-
// the digest, the digestAlgorithm and signatureAlgorithm AlgorithmIdentifiers (with the exact
|
|
126
|
-
// parameter shape cms.verify requires), and the WebCrypto import + sign algorithms.
|
|
127
|
-
function _scheme(cert, so, noSignedAttrs) {
|
|
128
|
-
var alg = cert.subjectPublicKeyInfo.algorithm;
|
|
129
|
-
var keyOid = alg.oid;
|
|
130
|
-
var comp = compositeSig.COMPOSITE_ALGS[keyOid];
|
|
131
|
-
if (comp) {
|
|
132
|
-
// Composite ML-DSA (draft-ietf-lamps-cms-composite-sigs): the arm DICTATES the CMS
|
|
133
|
-
// digestAlgorithm (its pre-hash, Table 1) -- unlike RSA/ECDSA the digest is not a caller
|
|
134
|
-
// choice, so a conflicting so.digestAlgorithm is rejected. The 3 arms Node's WebCrypto surface
|
|
135
|
-
// cannot sign (brainpool curves; the SHAKE256/64 pre-hash) fail closed at config time -- never
|
|
136
|
-
// a partial single-component signature.
|
|
137
|
-
if (comp.trad.unsupported) throw _err("cms/unsupported-algorithm", "composite " + comp.name + ": " + comp.trad.unsupported);
|
|
138
|
-
if (so.digestAlgorithm && so.digestAlgorithm !== comp.phCms) throw _err("cms/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");
|
|
139
|
-
return { composite: comp, digest: comp.phCms, digestAlgId: _algId(comp.phCms, "absent"), sigAlgId: _algId(comp.name, "absent") };
|
|
140
|
-
}
|
|
141
|
-
if (keyOid === O("rsaEncryption") || keyOid === O("rsassaPss")) {
|
|
142
|
-
var isPssKey = keyOid === O("rsassaPss");
|
|
143
|
-
// An id-RSASSA-PSS key MAY pin its permitted hash in the SPKI params (RFC 4055 sec. 1.2): honor
|
|
144
|
-
// that over the SHA-256 default so Node/OpenSSL does not reject the otherwise-valid signer with
|
|
145
|
-
// ERR_OSSL_DIGEST_NOT_ALLOWED. An explicit digestAlgorithm that contradicts the pin is rejected
|
|
146
|
-
// fail-closed rather than silently signed under a digest the key forbids.
|
|
147
|
-
var pinned = isPssKey ? _pssHashFromSpki(cert) : null;
|
|
148
|
-
if (pinned && so.digestAlgorithm && so.digestAlgorithm !== pinned) throw _err("cms/bad-input", "the signer key restricts the RSASSA-PSS digest to " + pinned + ", but digestAlgorithm " + JSON.stringify(so.digestAlgorithm) + " was requested");
|
|
149
|
-
var d = so.digestAlgorithm || pinned || "sha256";
|
|
150
|
-
if (!HASH[d]) throw _err("cms/unsupported-algorithm", "unsupported RSA digest algorithm " + JSON.stringify(d));
|
|
151
|
-
// A general rsaEncryption key signs PKCS#1 v1.5 by default, or RSASSA-PSS when opts.pss is set.
|
|
152
|
-
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 };
|
|
153
|
-
return { digest: d, digestAlgId: _algId(d, "absent"), sigAlgId: _algId("rsaEncryption", "null"), imp: { name: "RSASSA-PKCS1-v1_5", hash: HASH[d] }, sign: { name: "RSASSA-PKCS1-v1_5" }, ecdsaDer: false };
|
|
154
|
-
}
|
|
155
|
-
if (keyOid === O("ecPublicKey")) {
|
|
156
|
-
var curveOid;
|
|
157
|
-
try { curveOid = asn1.read.oid(asn1.decode(alg.parameters)); }
|
|
158
|
-
catch (e) { throw _err("cms/unsupported-algorithm", "the signer EC key parameters are not a named-curve OID", e); }
|
|
159
|
-
var ec = EC_BY_CURVE_OID[curveOid];
|
|
160
|
-
if (!ec) throw _err("cms/unsupported-algorithm", "the signer key is on an unsupported EC curve");
|
|
161
|
-
var de = so.digestAlgorithm || "sha256";
|
|
162
|
-
if (!HASH[de]) throw _err("cms/unsupported-algorithm", "unsupported ECDSA digest algorithm " + JSON.stringify(de));
|
|
163
|
-
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 };
|
|
164
|
-
}
|
|
165
|
-
if (keyOid === O("Ed25519") || keyOid === O("Ed448")) {
|
|
166
|
-
var name = keyOid === O("Ed25519") ? "Ed25519" : "Ed448";
|
|
167
|
-
var dd = so.digestAlgorithm || (name === "Ed25519" ? "sha512" : "shake256");
|
|
168
|
-
if (!NODE_DIGEST[dd]) throw _err("cms/unsupported-algorithm", "unsupported " + name + " digest algorithm " + JSON.stringify(dd));
|
|
169
|
-
return { digest: dd, digestAlgId: _algId(dd, "absent"), sigAlgId: _algId(name, "absent"), imp: { name: name }, sign: { name: name }, ecdsaDer: false };
|
|
170
|
-
}
|
|
171
|
-
if (MLDSA_BY_OID[keyOid]) {
|
|
172
|
-
// ML-DSA (RFC 9882): a one-shot post-quantum signature, pure mode, empty context. The signature
|
|
173
|
-
// covers the same sec. 5.4 preimage as every other scheme; digestAlgorithm names the message-
|
|
174
|
-
// digest algorithm (SHA-512 by default -- the sec. 3.3 MUST / interop default), which must be
|
|
175
|
-
// suitable for the parameter set (Q1 -- enforced on sign, refusing a below-strength digest).
|
|
176
|
-
var mlName = MLDSA_BY_OID[keyOid];
|
|
177
|
-
// RFC 9882 sec. 3.3: without signed attributes the digestAlgorithm has no meaning, but the
|
|
178
|
-
// signer MUST still emit SHA-512 to avoid an interoperability failure -- so force it (ignoring
|
|
179
|
-
// any caller digestAlgorithm) rather than carry a value a strict peer would reject. With signed
|
|
180
|
-
// attributes the digest must be suitable for the parameter set (Q1 -- enforced on sign).
|
|
181
|
-
var md;
|
|
182
|
-
if (noSignedAttrs) {
|
|
183
|
-
md = "sha512";
|
|
184
|
-
} else {
|
|
185
|
-
md = so.digestAlgorithm || "sha512";
|
|
186
|
-
if (!NODE_DIGEST[md]) throw _err("cms/unsupported-algorithm", "unsupported ML-DSA message digest " + JSON.stringify(md));
|
|
187
|
-
if (!MLDSA_SUITABLE_DIGEST[mlName][md]) throw _err("cms/unsupported-algorithm", "the " + md + " message digest is below the security strength of " + mlName + " (RFC 9882 sec. 3.3)");
|
|
188
|
-
}
|
|
189
|
-
return { digest: md, digestAlgId: _algId(md, "absent"), sigAlgId: _algId(oid.name(keyOid), "absent"), imp: { name: mlName }, sign: { name: mlName }, ecdsaDer: false };
|
|
190
|
-
}
|
|
191
|
-
if (SLHDSA_BY_OID[keyOid]) {
|
|
192
|
-
// SLH-DSA (RFC 9814): a one-shot post-quantum signature, pure mode, empty context. The message
|
|
193
|
-
// digest is FIXED per parameter set (sec. 4) -- emit it, and reject a contradicting caller
|
|
194
|
-
// digestAlgorithm fail-closed rather than carry a non-conformant digest.
|
|
195
|
-
var slh = SLHDSA_BY_OID[keyOid];
|
|
196
|
-
if (so.digestAlgorithm && so.digestAlgorithm !== slh.digest) throw _err("cms/bad-input", "SLH-DSA " + slh.wc + " requires the " + slh.digest + " message digest (RFC 9814 sec. 4); digestAlgorithm " + JSON.stringify(so.digestAlgorithm) + " conflicts");
|
|
197
|
-
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 };
|
|
198
|
-
}
|
|
199
|
-
throw _err("cms/unsupported-algorithm", "unsupported signer key algorithm " + keyOid);
|
|
200
|
-
}
|
|
201
48
|
|
|
202
49
|
// The raw issuer Name TLV from a parsed certificate (byte-identical to the cert, so the sid the
|
|
203
50
|
// verifier canonically compares matches exactly). The issuer is the Name after the optional
|
|
@@ -215,30 +62,6 @@ function _skiValue(cert) {
|
|
|
215
62
|
catch (e) { throw _err("cms/no-ski", "the signer certificate's subjectKeyIdentifier extension value is not an OCTET STRING", e); }
|
|
216
63
|
}
|
|
217
64
|
|
|
218
|
-
// A caller-supplied CryptoKey carries its OWN algorithm (name, and for RSA the baked-in hash,
|
|
219
|
-
// for ECDSA the curve). A PKCS#8 key is imported under the resolved scheme, so it always agrees;
|
|
220
|
-
// but a pre-imported CryptoKey whose algorithm disagrees with the certificate's scheme would
|
|
221
|
-
// silently sign under a different hash than the digestAlgorithm advertises -- an inconsistent,
|
|
222
|
-
// non-verifiable CMS. Reject the mismatch fail-closed.
|
|
223
|
-
function _assertKeyMatchesScheme(key, imp) {
|
|
224
|
-
var ka = key.algorithm || {};
|
|
225
|
-
if (ka.name !== imp.name) throw _err("cms/bad-input", "the signer CryptoKey algorithm (" + ka.name + ") does not match the certificate's key algorithm (" + imp.name + ")");
|
|
226
|
-
if (imp.hash && (!ka.hash || ka.hash.name !== imp.hash)) throw _err("cms/bad-input", "the signer CryptoKey hash (" + (ka.hash && ka.hash.name) + ") does not match the signing digest (" + imp.hash + ")");
|
|
227
|
-
if (imp.namedCurve && ka.namedCurve !== imp.namedCurve) throw _err("cms/bad-input", "the signer CryptoKey curve (" + ka.namedCurve + ") does not match the certificate curve (" + imp.namedCurve + ")");
|
|
228
|
-
}
|
|
229
|
-
// Import the signer private key: a CryptoKey passed through, or a PKCS#8 DER Buffer / PEM string.
|
|
230
|
-
function _importKey(key, imp) {
|
|
231
|
-
if (key && typeof key === "object" && !Buffer.isBuffer(key) && !(key instanceof Uint8Array) && key.type === "private") {
|
|
232
|
-
_assertKeyMatchesScheme(key, imp);
|
|
233
|
-
return Promise.resolve(key);
|
|
234
|
-
}
|
|
235
|
-
var der;
|
|
236
|
-
if (Buffer.isBuffer(key)) der = key;
|
|
237
|
-
else if (key instanceof Uint8Array) der = Buffer.from(key);
|
|
238
|
-
else if (typeof key === "string") { try { der = pkcs8.pemDecode(key); } catch (e) { throw _err("cms/bad-input", "the signer PEM private key could not be decoded", e); } }
|
|
239
|
-
else throw _err("cms/bad-input", "a signer key must be a CryptoKey, a PKCS#8 DER Buffer, or a PKCS#8 PEM string");
|
|
240
|
-
return subtle.importKey("pkcs8", der, imp, false, ["sign"]);
|
|
241
|
-
}
|
|
242
65
|
|
|
243
66
|
// Build one SignerInfo (RFC 5652 sec. 5.3) and sign it. Resolves the SignerInfo (as a build
|
|
244
67
|
// node) plus its digestAlgorithm AlgorithmIdentifier and the signer certificate DER (for the
|
|
@@ -247,7 +70,7 @@ function _buildSignerInfo(signer, content, eContentType, opts) {
|
|
|
247
70
|
var so = signer || {};
|
|
248
71
|
var certDer = _normCertDer(so.cert);
|
|
249
72
|
var cert = x509.parse(certDer);
|
|
250
|
-
var scheme =
|
|
73
|
+
var scheme = signScheme.resolveSignScheme(cert, so, opts.signedAttributes === false, _signE);
|
|
251
74
|
var useSki = opts.sid === "ski";
|
|
252
75
|
var sid = useSki
|
|
253
76
|
? b.contextPrimitive(0, _skiValue(cert)) // [0] IMPLICIT SubjectKeyIdentifier
|
|
@@ -282,7 +105,7 @@ function _buildSignerInfo(signer, content, eContentType, opts) {
|
|
|
282
105
|
return { setOf: setOf, wire: wire };
|
|
283
106
|
}).then(function (toSign) {
|
|
284
107
|
var signedBytes = toSign.setOf ? toSign.setOf : toSign; // SET-OF form for signing (sec. 5.4)
|
|
285
|
-
return
|
|
108
|
+
return signScheme.signOverTbs(scheme, so.key, signedBytes, _signE).then(function (sig) {
|
|
286
109
|
var fields = [b.integer(BigInt(version)), sid, scheme.digestAlgId];
|
|
287
110
|
if (toSign.wire) fields.push(toSign.wire); // [0] IMPLICIT signedAttrs
|
|
288
111
|
fields.push(scheme.sigAlgId, b.octetString(sig));
|
|
@@ -291,38 +114,7 @@ function _buildSignerInfo(signer, content, eContentType, opts) {
|
|
|
291
114
|
});
|
|
292
115
|
}
|
|
293
116
|
|
|
294
|
-
// Sign the sec. 5.4 preimage under the resolved scheme -> the raw signature bytes for the
|
|
295
|
-
// SignerInfo signature OCTET STRING. The classical path imports the single signer key and signs
|
|
296
|
-
// (re-encoding ECDSA to canonical DER); the composite path signs BOTH component keys over the
|
|
297
|
-
// domain-separated M' and returns mldsaSig || tradSig (composite-sig.js owns the construction).
|
|
298
|
-
function _signScheme(scheme, so, signedBytes) {
|
|
299
|
-
if (scheme.composite) {
|
|
300
|
-
return compositeSig.compositeSign(scheme.composite, _normCompositeKeys(so.key, scheme.composite), signedBytes).then(function (sig) { return Buffer.from(sig); });
|
|
301
|
-
}
|
|
302
|
-
return _importKey(so.key, scheme.imp).then(function (priv) {
|
|
303
|
-
return subtle.sign(scheme.sign, priv, signedBytes).then(function (sigRaw) {
|
|
304
|
-
var sig = Buffer.from(sigRaw);
|
|
305
|
-
if (scheme.ecdsaDer) sig = validator.sig.rawToEcdsaDer(sig, scheme.coordLen);
|
|
306
|
-
return sig;
|
|
307
|
-
});
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
117
|
|
|
311
|
-
// A composite signer key is the two component private keys { mldsa, trad } as PKCS#8 (Node has no
|
|
312
|
-
// single composite key type; the CMS draft is silent on key import). A missing/wrong-typed
|
|
313
|
-
// component fails closed at config time -- never a partial single-component signature.
|
|
314
|
-
function _normCompositeKeys(key, comp) {
|
|
315
|
-
if (!key || typeof key !== "object" || Buffer.isBuffer(key) || key instanceof Uint8Array || key.mldsa == null || key.trad == null) {
|
|
316
|
-
throw _err("cms/bad-input", "a composite " + comp.name + " signer key must be { mldsa: <PKCS#8>, trad: <PKCS#8> }");
|
|
317
|
-
}
|
|
318
|
-
return { mldsa: _normPkcs8(key.mldsa, "the composite ML-DSA component key"), trad: _normPkcs8(key.trad, "the composite traditional component key") };
|
|
319
|
-
}
|
|
320
|
-
function _normPkcs8(k, label) {
|
|
321
|
-
if (Buffer.isBuffer(k)) return k;
|
|
322
|
-
if (k instanceof Uint8Array) return Buffer.from(k);
|
|
323
|
-
if (typeof k === "string") { try { return pkcs8.pemDecode(k); } catch (e) { throw _err("cms/bad-input", label + " PEM could not be decoded", e); } }
|
|
324
|
-
throw _err("cms/bad-input", label + " must be a PKCS#8 DER Buffer, Uint8Array, or PEM string");
|
|
325
|
-
}
|
|
326
118
|
|
|
327
119
|
// A signing-time Time value: UTCTime before 2050, GeneralizedTime from 2050 (RFC 5652 sec. 11.3 /
|
|
328
120
|
// RFC 5280 sec. 4.1.2.5). A caller Date overrides; false omits the attribute (handled above).
|
|
@@ -415,7 +207,4 @@ function _toBuf(v, what) {
|
|
|
415
207
|
// * `_assertKeyMatchesScheme`'s `!ka.hash` guard -- an `imp.hash` is set only for an RSA
|
|
416
208
|
// scheme, which requires `ka.name` to already equal the RSA name (else the earlier name
|
|
417
209
|
// check throws); an RSA CryptoKey always carries a `hash`, so `!ka.hash` never fires.
|
|
418
|
-
|
|
419
|
-
// parameter-set digest-strength policy has ONE home -- a digest accepted on sign is exactly the
|
|
420
|
-
// set accepted on verify, and neither side can drift from the other (RFC 9882 sec. 3.3).
|
|
421
|
-
module.exports = { sign: sign, MLDSA_SUITABLE_DIGEST: MLDSA_SUITABLE_DIGEST, SLHDSA_BY_OID: SLHDSA_BY_OID };
|
|
210
|
+
module.exports = { sign: sign };
|
package/lib/cms-verify.js
CHANGED
|
@@ -35,8 +35,9 @@ var webcrypto = require("./webcrypto");
|
|
|
35
35
|
var subtle = webcrypto.webcrypto.subtle;
|
|
36
36
|
var edwardsPoint = require("./edwards-point");
|
|
37
37
|
var cmsSign = require("./cms-sign");
|
|
38
|
-
var
|
|
39
|
-
var
|
|
38
|
+
var signScheme = require("./sign-scheme");
|
|
39
|
+
var MLDSA_SUITABLE_DIGEST = signScheme.MLDSA_SUITABLE_DIGEST; // shared sign/verify digest-strength policy (RFC 9882 sec. 3.3)
|
|
40
|
+
var SLHDSA_BY_OID = signScheme.SLHDSA_BY_OID; // shared SLH-DSA set -> { wc, digest } (RFC 9814 sec. 4 pinned digest)
|
|
40
41
|
var validator = require("./validator-all");
|
|
41
42
|
var compositeSig = require("./composite-sig");
|
|
42
43
|
var guard = require("./guard-all");
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
//
|
|
5
|
+
// @internal -- no operator-facing namespace. The documented surfaces are pki.path.ocspChecker
|
|
6
|
+
// (revocation during path validation) and pki.ocsp.verify (standalone RFC 6960 sec. 3.2 client
|
|
7
|
+
// acceptance), which BOTH compose this ONE verify core. There is deliberately no second, weaker
|
|
8
|
+
// OCSP response-verification path: a standalone verify that re-derived the responder-authorization
|
|
9
|
+
// gates would be the exact fail-open the out-of-path-signer-cert-full-validation discipline exists
|
|
10
|
+
// to prevent.
|
|
11
|
+
//
|
|
12
|
+
// The core is a factory over an INJECTED `deps` seam so it has no back-edge into path-validate:
|
|
13
|
+
// the signature engine (`verifyWithSpki`) and the RFC 5280 cert-profile gates
|
|
14
|
+
// (`decodeExt`/`findExt`/`unrecognizedCriticalExtension`/`validateCriticalExtensionStructure`/
|
|
15
|
+
// `compositeKeyUsageCheck`/`isNullOrAbsentParams`/`spliceSpkiParameters`/`dnEqual`) are supplied by
|
|
16
|
+
// the caller that owns them; everything OCSP-specific (CertID hash binding, responder
|
|
17
|
+
// authorization, currency, status) lives here.
|
|
18
|
+
//
|
|
19
|
+
// Rule set (gap-checked verbatim against RFC 6960 sec. 3.2 / 4.1.1 / 4.2.2.2 / 4.2.2.2.1):
|
|
20
|
+
// - authorizeResponder: the issuing CA directly (responderID identifies the issuer) OR a
|
|
21
|
+
// CA-issued delegate valid at `time` bearing id-kp-OCSPSigning (anyEKU / absent EKU do NOT
|
|
22
|
+
// authorize), keyUsage-permits-signing, no unknown/malformed critical extension, and
|
|
23
|
+
// id-pkix-ocsp-nocheck (a transport-free verify cannot otherwise confirm the responder is
|
|
24
|
+
// unrevoked); the delegate's own issuance signature is verified under the issuer key.
|
|
25
|
+
// - certIdMatches: serial + issuerNameHash + issuerKeyHash under the CertID's OWN hashAlgorithm
|
|
26
|
+
// (a serial-only match is a cross-CA substitution and is rejected).
|
|
27
|
+
// - currency: thisUpdate <= time and a bounded nextUpdate > time (a nextUpdate-less response is
|
|
28
|
+
// unusable per the lightweight profile).
|
|
29
|
+
|
|
30
|
+
var asn1 = require("./asn1-der");
|
|
31
|
+
var oid = require("./oid");
|
|
32
|
+
var x509 = require("./schema-x509");
|
|
33
|
+
var compositeSig = require("./composite-sig");
|
|
34
|
+
var webcrypto = require("./webcrypto");
|
|
35
|
+
var subtle = webcrypto.webcrypto.subtle;
|
|
36
|
+
|
|
37
|
+
// CertID identity-hash algorithms (RFC 6960 sec. 4.1.1). SEPARATE from the signature HASH set
|
|
38
|
+
// (which omits SHA-1, SHAttered): a CertID hash is an identity binding of an already-known issuer,
|
|
39
|
+
// not a signature, so RFC 6960's default SHA-1 CertID MUST interoperate. A hash outside this set
|
|
40
|
+
// cannot be reproduced -> no CertID match (fail closed).
|
|
41
|
+
var OCSP_CERTID_HASHES = {};
|
|
42
|
+
OCSP_CERTID_HASHES[oid.byName("sha1")] = "SHA-1";
|
|
43
|
+
OCSP_CERTID_HASHES[oid.byName("sha256")] = "SHA-256";
|
|
44
|
+
OCSP_CERTID_HASHES[oid.byName("sha384")] = "SHA-384";
|
|
45
|
+
OCSP_CERTID_HASHES[oid.byName("sha512")] = "SHA-512";
|
|
46
|
+
var OID_OCSP_SIGNING = oid.byName("ocspSigning");
|
|
47
|
+
var OID_OCSP_NOCHECK = oid.byName("ocspNoCheck");
|
|
48
|
+
var OID_EKU = oid.byName("extKeyUsage");
|
|
49
|
+
var OID_KEY_USAGE = oid.byName("keyUsage");
|
|
50
|
+
|
|
51
|
+
function ocspDigest(alg, buf) { return subtle.digest(alg, buf).then(function (h) { return Buffer.from(h); }); }
|
|
52
|
+
|
|
53
|
+
// The subjectPublicKey BIT STRING VALUE (past the unused-bits octet) of an SPKI DER -- the exact
|
|
54
|
+
// bytes an OCSP CertID issuerKeyHash / byKey KeyHash hash over (RFC 6960 sec. 4.1.1). Throws on a
|
|
55
|
+
// malformed SPKI; the caller fails closed.
|
|
56
|
+
function ocspKeyValue(spkiDer) {
|
|
57
|
+
return asn1.read.bitString(asn1.decode(spkiDer).children[1]).bytes;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// makeOcspVerify(deps) -> the bound OCSP verify core. deps = { verifyWithSpki, decodeExt, findExt,
|
|
61
|
+
// unrecognizedCriticalExtension, validateCriticalExtensionStructure, compositeKeyUsageCheck,
|
|
62
|
+
// isNullOrAbsentParams, spliceSpkiParameters, dnEqual } -- the signature engine + RFC 5280
|
|
63
|
+
// cert-profile gates owned by the caller.
|
|
64
|
+
function makeOcspVerify(deps) {
|
|
65
|
+
var verifyWithSpki = deps.verifyWithSpki;
|
|
66
|
+
var decodeExt = deps.decodeExt;
|
|
67
|
+
var findExt = deps.findExt;
|
|
68
|
+
var unrecognizedCriticalExtension = deps.unrecognizedCriticalExtension;
|
|
69
|
+
var validateCriticalExtensionStructure = deps.validateCriticalExtensionStructure;
|
|
70
|
+
var compositeKeyUsageCheck = deps.compositeKeyUsageCheck;
|
|
71
|
+
var isNullOrAbsentParams = deps.isNullOrAbsentParams;
|
|
72
|
+
var spliceSpkiParameters = deps.spliceSpkiParameters;
|
|
73
|
+
var dnEqual = deps.dnEqual;
|
|
74
|
+
|
|
75
|
+
// The delegate responder's importable SPKI, splicing the issuer's algorithm parameters in when
|
|
76
|
+
// the delegate key inherits them (an EC SPKI that omits the namedCurve, RFC 5280 sec. 4.1.2.7).
|
|
77
|
+
function ocspResponderSpki(rc, issuer) {
|
|
78
|
+
var keyAlg = rc.subjectPublicKeyInfo.algorithm;
|
|
79
|
+
if (!isNullOrAbsentParams(keyAlg.parameters)) return rc.subjectPublicKeyInfo.bytes;
|
|
80
|
+
var issuerOid, issuerParams;
|
|
81
|
+
// Coverage residual -- the catch is unreachable: this runs only after the delegate's signature
|
|
82
|
+
// verified under issuer.workingPublicKey, so that SPKI already imported and decodes cleanly.
|
|
83
|
+
try {
|
|
84
|
+
var alg = asn1.decode(issuer.workingPublicKey).children[0];
|
|
85
|
+
issuerOid = asn1.read.oid(alg.children[0]);
|
|
86
|
+
issuerParams = alg.children[1] ? alg.children[1].bytes : null;
|
|
87
|
+
} catch (_e) { return rc.subjectPublicKeyInfo.bytes; }
|
|
88
|
+
if (issuerOid === keyAlg.oid && !isNullOrAbsentParams(issuerParams)) {
|
|
89
|
+
return spliceSpkiParameters(rc.subjectPublicKeyInfo, keyAlg.oid, issuerParams);
|
|
90
|
+
}
|
|
91
|
+
return rc.subjectPublicKeyInfo.bytes;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Returns true if `extList` carries any critical extension (this code processes no OCSP
|
|
95
|
+
// response/single extension semantics, so a critical one makes the response unusable).
|
|
96
|
+
function ocspHasCriticalExtension(extList) {
|
|
97
|
+
if (!extList) return false;
|
|
98
|
+
for (var i = 0; i < extList.length; i++) if (extList[i].critical) return true;
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// A SingleResponse's CertID names the target cert IFF serial AND issuerNameHash + issuerKeyHash
|
|
103
|
+
// (under the CertID's OWN hashAlgorithm) match the issuer. `issuerNameCandidates` is every RFC
|
|
104
|
+
// 5280 sec. 7.1-equal byte encoding of the validated issuer DN to try.
|
|
105
|
+
async function ocspCertIdMatches(certID, cert, issuerNameCandidates, issuerKeyBits) {
|
|
106
|
+
if (certID.serialNumberHex !== cert.serialNumberHex) return false;
|
|
107
|
+
var hashName = OCSP_CERTID_HASHES[certID.hashAlgorithm.oid];
|
|
108
|
+
if (!hashName) return false;
|
|
109
|
+
var keyHash = await ocspDigest(hashName, issuerKeyBits);
|
|
110
|
+
if (!certID.issuerKeyHash.equals(keyHash)) return false;
|
|
111
|
+
for (var i = 0; i < issuerNameCandidates.length; i++) {
|
|
112
|
+
if (certID.issuerNameHash.equals(await ocspDigest(hashName, issuerNameCandidates[i]))) return true;
|
|
113
|
+
}
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Resolve the signer of a BasicOCSPResponse to an AUTHORIZED responder SPKI DER, or null. Two
|
|
118
|
+
// models: the issuing CA directly, or a CA-delegated responder (RFC 6960 sec. 4.2.2.2). Fails
|
|
119
|
+
// closed at every branch.
|
|
120
|
+
async function ocspAuthorizeResponder(basicResponse, cert, issuer, issuerKeyBits, time) {
|
|
121
|
+
var rid = basicResponse.responderID;
|
|
122
|
+
var matchesIssuer = false;
|
|
123
|
+
try {
|
|
124
|
+
if (rid.byName) matchesIssuer = dnEqual(rid.byName.rdns, cert.issuer.rdns);
|
|
125
|
+
else if (rid.byKey) matchesIssuer = rid.byKey.equals(await ocspDigest("SHA-1", issuerKeyBits));
|
|
126
|
+
} catch (_e) { matchesIssuer = false; }
|
|
127
|
+
if (matchesIssuer) return issuer.workingPublicKey;
|
|
128
|
+
|
|
129
|
+
for (var i = 0; i < basicResponse.certs.length; i++) {
|
|
130
|
+
var rc;
|
|
131
|
+
try { rc = x509.parse(basicResponse.certs[i]); }
|
|
132
|
+
catch (_e) { continue; }
|
|
133
|
+
var identifies = false;
|
|
134
|
+
try {
|
|
135
|
+
if (rid.byName) identifies = dnEqual(rid.byName.rdns, rc.subject.rdns);
|
|
136
|
+
else if (rid.byKey) identifies = rid.byKey.equals(await ocspDigest("SHA-1", ocspKeyValue(rc.subjectPublicKeyInfo.bytes)));
|
|
137
|
+
} catch (_e) { identifies = false; }
|
|
138
|
+
if (!identifies) continue;
|
|
139
|
+
// The delegate MUST be issued directly by the CA that issued the target.
|
|
140
|
+
var issuedByCa;
|
|
141
|
+
try { issuedByCa = dnEqual(rc.issuer.rdns, cert.issuer.rdns); }
|
|
142
|
+
catch (_e) { continue; }
|
|
143
|
+
if (!issuedByCa) continue;
|
|
144
|
+
if (!isOctetAligned(rc.signatureValue)) continue;
|
|
145
|
+
if (!(await verifyWithSpki(rc.signatureAlgorithm, rc.signatureValue.bytes, issuer.workingPublicKey, rc.tbsBytes))) continue;
|
|
146
|
+
// The delegate cert MUST itself be valid at the validation instant.
|
|
147
|
+
if (time < rc.validity.notBefore || time > rc.validity.notAfter) continue;
|
|
148
|
+
// The delegate MUST assert id-kp-OCSPSigning; anyEKU / an absent EKU do not.
|
|
149
|
+
var eku;
|
|
150
|
+
try { eku = decodeExt(rc, OID_EKU); }
|
|
151
|
+
catch (_e) { continue; }
|
|
152
|
+
if (!eku || eku.value.indexOf(OID_OCSP_SIGNING) === -1) continue;
|
|
153
|
+
// A delegate asserting keyUsage MUST permit digitalSignature; an absent keyUsage is
|
|
154
|
+
// unrestricted, an unreadable one is not authoritative.
|
|
155
|
+
var ku;
|
|
156
|
+
try { ku = decodeExt(rc, OID_KEY_USAGE); }
|
|
157
|
+
catch (_e) { continue; }
|
|
158
|
+
if (ku && ku.value.digitalSignature !== true) continue;
|
|
159
|
+
// An unknown critical extension (or a recognized-but-malformed one) makes the delegate
|
|
160
|
+
// unusable, the same fail-closed rule the path validator applies.
|
|
161
|
+
if (unrecognizedCriticalExtension(rc, false)) continue;
|
|
162
|
+
if (validateCriticalExtensionStructure(rc)) continue;
|
|
163
|
+
// The delegate MUST carry id-pkix-ocsp-nocheck (RFC 6960 sec. 4.2.2.2.1): a transport-free
|
|
164
|
+
// checker cannot otherwise confirm the responder cert is unrevoked.
|
|
165
|
+
if (!findExt(rc, OID_OCSP_NOCHECK)) continue;
|
|
166
|
+
// A composite-keyed delegate gets the same composite keyUsage gate the path certs do.
|
|
167
|
+
if (compositeSig.COMPOSITE_ALGS[rc.subjectPublicKeyInfo.algorithm.oid] && !compositeKeyUsageCheck(rc).ok) continue;
|
|
168
|
+
return ocspResponderSpki(rc, issuer);
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// A BIT STRING is octet-aligned iff its unused-bits count is 0 -- reused from the caller's
|
|
174
|
+
// guard so a non-octet-aligned responder signature is never verified.
|
|
175
|
+
function isOctetAligned(bitString) { return !!bitString && bitString.unusedBits === 0; }
|
|
176
|
+
|
|
177
|
+
// evaluateResponse(resp, cert, issuer, issuerKeyBits, issuerNameCandidates, time, historical) ->
|
|
178
|
+
// a granular per-response SUMMARY for ONE parsed OCSPResponse, aggregating over ALL of its
|
|
179
|
+
// matching SingleResponses (a revoked SingleResponse shadows a good one WITHIN the response, the
|
|
180
|
+
// same fail-closed law the multi-response aggregator applies). Never throws (fail-closed):
|
|
181
|
+
// { applicable, responderAuthorized, signatureValid, matched, revoked:{reason,revocationReason}?,
|
|
182
|
+
// sawGood, sawUnknownStatus, thisUpdate?, nextUpdate?, reason }
|
|
183
|
+
async function evaluateResponse(resp, cert, issuer, issuerKeyBits, issuerNameCandidates, time, historical) {
|
|
184
|
+
if (resp.responseStatus.code !== 0) return { applicable: false, matched: false, reason: "non-successful OCSP responseStatus (" + resp.responseStatus.code + ")" };
|
|
185
|
+
var br = resp.basicResponse;
|
|
186
|
+
// Coverage residual -- unreachable for a parsed response (schema-ocsp enforces the
|
|
187
|
+
// successful<->responseBytes biconditional); backstop for a hand-built object.
|
|
188
|
+
if (!br) return { applicable: false, matched: false, reason: "successful OCSP response carries no BasicOCSPResponse" };
|
|
189
|
+
var signerSpki = await ocspAuthorizeResponder(br, cert, issuer, issuerKeyBits, time);
|
|
190
|
+
if (!signerSpki) return { applicable: true, matched: false, responderAuthorized: false, reason: "no authorized OCSP responder signs this response (RFC 6960 sec. 4.2.2.2)" };
|
|
191
|
+
if (!(await verifyWithSpki(br.signatureAlgorithm, br.signature, signerSpki, br.tbsResponseDataBytes))) {
|
|
192
|
+
return { applicable: true, matched: false, responderAuthorized: true, signatureValid: false, reason: "the OCSP response signature does not verify over tbsResponseData" };
|
|
193
|
+
}
|
|
194
|
+
if (ocspHasCriticalExtension(br.responseExtensions)) {
|
|
195
|
+
return { applicable: true, matched: false, responderAuthorized: true, signatureValid: true, reason: "the OCSP response carries an unrecognized critical extension" };
|
|
196
|
+
}
|
|
197
|
+
var out = { applicable: true, matched: false, responderAuthorized: true, signatureValid: true, revoked: null, sawGood: false, sawUnknownStatus: false, reason: "no current SingleResponse names this certificate" };
|
|
198
|
+
for (var s = 0; s < br.responses.length; s++) {
|
|
199
|
+
var sr = br.responses[s];
|
|
200
|
+
if (!(await ocspCertIdMatches(sr.certID, cert, issuerNameCandidates, issuerKeyBits))) continue;
|
|
201
|
+
if (ocspHasCriticalExtension(sr.singleExtensions)) continue;
|
|
202
|
+
if (sr.thisUpdate > time) continue;
|
|
203
|
+
if (!sr.nextUpdate || sr.nextUpdate < time) continue;
|
|
204
|
+
out.matched = true; out.thisUpdate = sr.thisUpdate; out.nextUpdate = sr.nextUpdate;
|
|
205
|
+
var st = sr.certStatus;
|
|
206
|
+
if (st.type === "revoked") {
|
|
207
|
+
// Present-time validation revokes regardless of a future revocationTime (skew/post-dating);
|
|
208
|
+
// only explicit historical validation defers a strictly-future revocation (reports good).
|
|
209
|
+
if (historical && st.revocationTime instanceof Date && st.revocationTime.getTime() > time.getTime()) { out.sawGood = true; }
|
|
210
|
+
else if (!out.revoked) { out.revoked = { revocationReason: st.revocationReason || null, reason: "certificate reported revoked by an authorized OCSP responder" + (st.revocationReason ? " (" + st.revocationReason + ")" : "") }; }
|
|
211
|
+
} else if (st.type === "good") { out.sawGood = true; }
|
|
212
|
+
else { out.sawUnknownStatus = true; }
|
|
213
|
+
}
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
evaluateResponse: evaluateResponse,
|
|
219
|
+
authorizeResponder: ocspAuthorizeResponder,
|
|
220
|
+
certIdMatches: ocspCertIdMatches,
|
|
221
|
+
hasCriticalExtension: ocspHasCriticalExtension,
|
|
222
|
+
responderSpki: ocspResponderSpki,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
module.exports = { makeOcspVerify: makeOcspVerify, ocspKeyValue: ocspKeyValue, ocspDigest: ocspDigest, OCSP_CERTID_HASHES: OCSP_CERTID_HASHES };
|