@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.
@@ -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 };
package/lib/ocsp.js ADDED
@@ -0,0 +1,378 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ /**
5
+ * @module pki.ocsp
6
+ * @nav Revocation
7
+ * @title OCSP
8
+ * @intro The producing + client-facing half of RFC 6960 Online Certificate Status Protocol:
9
+ * build a status request over a certificate (`pki.ocsp.buildRequest`), build and sign a status
10
+ * response as an authorized responder (`pki.ocsp.sign`), emit an unsigned error response
11
+ * (`pki.ocsp.buildErrorResponse`), and verify a returned response as a relying party
12
+ * (`pki.ocsp.verify`). Parsing lives in `pki.schema.ocsp`; revocation during path validation is
13
+ * `pki.path.ocspChecker`. Signing rides the shared sign-scheme registry (the same classical +
14
+ * post-quantum set `pki.cms.sign` uses), so a response is signed under RSA / ECDSA / EdDSA /
15
+ * ML-DSA / SLH-DSA per the responder key. Verification composes the SAME hardened responder-
16
+ * authorization + signature + currency gates `pki.path.ocspChecker` runs -- there is no weaker
17
+ * second verify path. Fail-closed: `verify` returns a `"unknown"` verdict (never a silent accept)
18
+ * for any unmet gate; malformed input throws a typed `OcspError`.
19
+ * @spec RFC 6960, RFC 9654, RFC 5019
20
+ * @card Build, sign, and verify RFC 6960 OCSP requests + responses (a responder + a relying party).
21
+ */
22
+
23
+ var nodeCrypto = require("crypto");
24
+ var asn1 = require("./asn1-der");
25
+ var oid = require("./oid");
26
+ var x509 = require("./schema-x509");
27
+ var ocspSchema = require("./schema-ocsp");
28
+ var pathValidate = require("./path-validate");
29
+ var signScheme = require("./sign-scheme");
30
+ var ocspVerify = require("./ocsp-verify");
31
+ var webcrypto = require("./webcrypto");
32
+ var subtle = webcrypto.webcrypto.subtle;
33
+ var guard = require("./guard-all");
34
+ var constants = require("./constants");
35
+ var frameworkError = require("./framework-error");
36
+
37
+ var OcspError = frameworkError.OcspError;
38
+ var b = asn1.build;
39
+ function O(name) { return oid.byName(name); }
40
+ function _err(code, message, cause) { return new OcspError(code, message, cause); }
41
+ // The domain error factory the shared sign-scheme resolver/signer throws through (kind ->
42
+ // ocsp/<kind>), so its faults keep the ocsp/* codes.
43
+ function _signE(kind, message, cause) { return new OcspError("ocsp/" + kind, message, cause); }
44
+
45
+ // CertID / responder-ID hash algorithm name -> WebCrypto digest name. SHA-1 is the RFC 5019 interop
46
+ // default for the CertID IDENTITY hash (not a signature; collision resistance is irrelevant to the
47
+ // lookup, so SHAttered does not bar it -- the deliberate split path-validate makes too).
48
+ var HASH_WC = { sha1: "SHA-1", sha256: "SHA-256", sha384: "SHA-384", sha512: "SHA-512" };
49
+ // A CRLReason name -> its enumerated value (RFC 5280 sec. 5.3.1), reverse of pki.C.NAMES.CRL_REASON.
50
+ var REASON_CODE = {};
51
+ Object.keys(constants.NAMES.CRL_REASON).forEach(function (k) { REASON_CODE[constants.NAMES.CRL_REASON[k]] = parseInt(k, 10); });
52
+ var OID_OCSP_NONCE = O("ocspNonce");
53
+ var OID_OCSP_BASIC = O("ocspBasic");
54
+ var OID_EXTENDED_REVOKE = O("ocspExtendedRevoke");
55
+
56
+ function _digest(wcHash, buf) { return subtle.digest(wcHash, buf).then(function (h) { return Buffer.from(h); }); }
57
+ function _certOf(arg, what) {
58
+ if (arg && arg.subjectPublicKeyInfo && arg.tbsBytes) return arg; // already parsed
59
+ var der;
60
+ if (Buffer.isBuffer(arg)) der = arg;
61
+ else if (arg instanceof Uint8Array) der = Buffer.from(arg);
62
+ else if (typeof arg === "string") { try { der = x509.pemDecode(arg); } catch (e) { throw _err("ocsp/bad-input", (what || "a certificate") + " PEM could not be decoded", e); } }
63
+ else throw _err("ocsp/bad-input", (what || "a certificate") + " must be a parsed certificate, a DER Buffer, or a PEM string");
64
+ try { return x509.parse(der); } catch (e) { throw _err("ocsp/bad-input", (what || "a certificate") + " is not a well-formed X.509 certificate", e); }
65
+ }
66
+ function _toDer(input, what) {
67
+ if (Buffer.isBuffer(input)) return input;
68
+ if (input instanceof Uint8Array) return Buffer.from(input);
69
+ if (typeof input === "string") { try { return ocspSchema.pemDecode(input); } catch (e) { throw _err("ocsp/bad-input", (what || "input") + " PEM could not be decoded", e); } }
70
+ throw _err("ocsp/bad-input", (what || "input") + " must be a DER Buffer, Uint8Array, or PEM string");
71
+ }
72
+ // The subjectPublicKey BIT STRING value an issuerKeyHash / byKey KeyHash hashes over.
73
+ function _keyValue(spkiDer) { return ocspVerify.ocspKeyValue(spkiDer); }
74
+ // A non-critical id-pkix-ocsp-nonce Extension carrying the raw nonce (RFC 9654): the extnValue
75
+ // OCTET STRING wraps the Nonce ::= OCTET STRING.
76
+ function _nonceExt(nonceBytes) { return b.sequence([b.oid(OID_OCSP_NONCE), b.octetString(b.octetString(nonceBytes))]); }
77
+ function _assertNonce(nonceBytes) {
78
+ if (!Buffer.isBuffer(nonceBytes) || nonceBytes.length < 1 || nonceBytes.length > 128) {
79
+ throw _err("ocsp/bad-input", "a nonce must be a Buffer of 1..128 octets (RFC 9654 sec. 2.1)");
80
+ }
81
+ }
82
+ // The CertID SEQUENCE for target `cert` under `issuer`, hashing under `hashName` (RFC 6960 sec. 4.1.1).
83
+ function _buildCertID(cert, issuer, hashName) {
84
+ var wc = HASH_WC[hashName];
85
+ if (!wc) throw _err("ocsp/bad-input", "unsupported CertID hashAlgorithm " + JSON.stringify(hashName) + " (sha1 / sha256 / sha384 / sha512)");
86
+ return Promise.all([
87
+ _digest(wc, issuer.subject.bytes),
88
+ _digest(wc, _keyValue(issuer.subjectPublicKeyInfo.bytes)),
89
+ ]).then(function (hashes) {
90
+ // SHA-1 carries NULL parameters (RFC 3279); a SHA-2 digest AlgorithmIdentifier omits them
91
+ // (RFC 5754 sec. 2). The CertID matcher keys on the OID and ignores parameters either way.
92
+ var hashAlgId = hashName === "sha1" ? b.sequence([b.oid(O(hashName)), b.nullValue()]) : b.sequence([b.oid(O(hashName))]);
93
+ return b.sequence([
94
+ hashAlgId,
95
+ b.octetString(hashes[0]),
96
+ b.octetString(hashes[1]),
97
+ b.integer(cert.serialNumber),
98
+ ]);
99
+ });
100
+ }
101
+
102
+ /**
103
+ * @primitive pki.ocsp.buildRequest
104
+ * @signature pki.ocsp.buildRequest(query, opts?) -> Buffer | string
105
+ * @since 0.2.22
106
+ * @status experimental
107
+ * @spec RFC 6960, RFC 9654, RFC 5019
108
+ * @related pki.ocsp.verify, pki.schema.ocsp.parseRequest
109
+ *
110
+ * Build an OCSPRequest for the status of one or more certificates. `query` is a `{ cert, issuer }`
111
+ * pair (or an array of them), each certificate given parsed or as DER/PEM; the CertID is derived by
112
+ * hashing the issuer name and key under `opts.hashAlgorithm` (SHA-1 by default, the RFC 5019 interop
113
+ * choice). The version DEFAULT (v1) is omitted from the DER. Returns the request DER, or PEM when
114
+ * `opts.pem` is set.
115
+ *
116
+ * @opts
117
+ * hashAlgorithm `"sha1"` (default) / `"sha256"` / `"sha384"` / `"sha512"` -- the CertID identity hash.
118
+ * nonce `true` for a fresh 32-octet CSPRNG nonce (RFC 9654), or a caller Buffer (1..128 octets).
119
+ * requestorName a Name (RDN array) placed in the [1] requestorName as a directoryName.
120
+ * signer `{ cert, key }` to sign the request (requires requestorName).
121
+ * profile `"lightweight"` -- one Request, SHA-1 CertID, nonce-only extensions (RFC 5019).
122
+ * pem emit a PEM `OCSP REQUEST` string instead of DER.
123
+ * @example
124
+ * var der = await pki.ocsp.buildRequest({ cert: leafDer, issuer: caDer }, { nonce: true });
125
+ */
126
+ function buildRequest(query, opts) {
127
+ opts = opts || {};
128
+ var lightweight = opts.profile === "lightweight";
129
+ var hashName = opts.hashAlgorithm || "sha1";
130
+ if (lightweight && hashName !== "sha1") throw _err("ocsp/bad-input", "the lightweight profile requires a SHA-1 CertID (RFC 5019 sec. 2.1.1)");
131
+ var queries = Array.isArray(query) ? query : [query];
132
+ if (!queries.length) throw _err("ocsp/bad-input", "buildRequest needs at least one { cert, issuer } query");
133
+ if (lightweight && queries.length !== 1) throw _err("ocsp/bad-input", "the lightweight profile permits exactly one Request (RFC 5019 sec. 2.1.1)");
134
+ if (opts.signer && !opts.requestorName) throw _err("ocsp/bad-input", "a signed OCSP request MUST carry requestorName (RFC 6960 sec. 4.1.2)");
135
+
136
+ return Promise.all(queries.map(function (q) {
137
+ if (!q || q.cert == null || q.issuer == null) throw _err("ocsp/bad-input", "each query must be { cert, issuer }");
138
+ var cert = _certOf(q.cert, "the query certificate");
139
+ var issuer = _certOf(q.issuer, "the issuer certificate");
140
+ return _buildCertID(cert, issuer, hashName).then(function (certID) {
141
+ var reqChildren = [certID];
142
+ if (q.singleRequestExtensions && q.singleRequestExtensions.length) {
143
+ if (lightweight) throw _err("ocsp/bad-input", "the lightweight profile permits no singleRequestExtensions (RFC 5019 sec. 2.1.1)");
144
+ reqChildren.push(b.explicit(0, b.sequence(q.singleRequestExtensions)));
145
+ }
146
+ return b.sequence(reqChildren);
147
+ });
148
+ })).then(function (requests) {
149
+ var tbsChildren = [];
150
+ if (opts.requestorName) {
151
+ // requestorName [1] EXPLICIT GeneralName; a Name is carried as directoryName [4].
152
+ var nameDer = _nameDer(opts.requestorName);
153
+ tbsChildren.push(b.explicit(1, b.contextConstructed(4, nameDer)));
154
+ }
155
+ tbsChildren.push(b.sequence(requests));
156
+ var reqExts = [];
157
+ if (opts.nonce) {
158
+ var nonceBytes = opts.nonce === true ? nodeCrypto.randomBytes(32) : opts.nonce; // >= 32 octets, CSPRNG (RFC 9654 sec. 2.1)
159
+ _assertNonce(nonceBytes);
160
+ reqExts.push(_nonceExt(nonceBytes));
161
+ }
162
+ if (reqExts.length) tbsChildren.push(b.explicit(2, b.sequence(reqExts)));
163
+ var tbsRequest = b.sequence(tbsChildren);
164
+ if (!opts.signer) return _emitReq(b.sequence([tbsRequest]), opts);
165
+ var signerCertDer = _normCertDer(opts.signer.cert, "the request signer certificate");
166
+ var signerCert = _certOf(signerCertDer, "the request signer certificate");
167
+ var scheme = signScheme.resolveSignScheme(signerCert, { combinedRsaSig: true }, true, _signE);
168
+ return signScheme.signOverTbs(scheme, opts.signer.key, tbsRequest, _signE).then(function (sig) {
169
+ var optionalSignature = b.sequence([scheme.sigAlgId, b.bitString(sig, 0), b.explicit(0, b.sequence([b.raw(signerCertDer)]))]);
170
+ return _emitReq(b.sequence([tbsRequest, b.explicit(0, optionalSignature)]), opts);
171
+ });
172
+ });
173
+ }
174
+ function _emitReq(der, opts) { return opts.pem ? ocspSchema.pemEncode(der, "OCSP REQUEST") : der; }
175
+ function _nameDer(name) {
176
+ if (Buffer.isBuffer(name)) return name;
177
+ if (name && name.bytes) return name.bytes; // a parsed Name
178
+ throw _err("ocsp/bad-input", "requestorName must be a DER Name Buffer or a parsed Name");
179
+ }
180
+ // The RAW DER of a certificate that will be embedded verbatim (the responder cert in a
181
+ // BasicOCSPResponse, the signer cert in a signed OCSPRequest). It must be supplied as bytes:
182
+ // a parsed certificate does not retain its full DER encoding, and re-encoding it would risk
183
+ // byte drift from the signed original, so a parsed cert is rejected rather than reconstructed.
184
+ function _normCertDer(cert, what) {
185
+ if (Buffer.isBuffer(cert)) return cert;
186
+ if (cert instanceof Uint8Array) return Buffer.from(cert);
187
+ if (typeof cert === "string") { try { return x509.pemDecode(cert); } catch (e) { throw _err("ocsp/bad-input", (what || "a certificate") + " PEM could not be decoded", e); } }
188
+ if (cert && cert.tbsBytes && cert.subjectPublicKeyInfo) {
189
+ throw _err("ocsp/bad-input", (what || "a certificate") + " to embed must be supplied as DER bytes or a PEM string, not a parsed certificate (the parser does not retain the full DER encoding needed to embed it verbatim)");
190
+ }
191
+ throw _err("ocsp/bad-input", (what || "a certificate") + " must be a DER Buffer, Uint8Array, or PEM string");
192
+ }
193
+
194
+ /**
195
+ * @primitive pki.ocsp.sign
196
+ * @signature pki.ocsp.sign(responseData, responder, opts?) -> Promise<Buffer | string>
197
+ * @since 0.2.22
198
+ * @status experimental
199
+ * @spec RFC 6960, RFC 9654
200
+ * @related pki.ocsp.verify, pki.ocsp.buildErrorResponse
201
+ *
202
+ * Build and sign a `successful` OCSPResponse wrapping a BasicOCSPResponse. `responseData` names the
203
+ * responderID, an optional producedAt, and one or more per-certificate responses; `responder` is the
204
+ * `{ cert, key }` signing the response (the issuing CA directly, or a CA-issued delegate bearing
205
+ * id-kp-OCSPSigning + id-pkix-ocsp-nocheck). The signature is computed over the exact ResponseData
206
+ * DER (RFC 6960 sec. 4.2.1 -- no CMS wrapper, no signed attributes). The responder certificate is
207
+ * embedded in `certs [0]` so a relying party can find it. Returns the response DER, or PEM.
208
+ *
209
+ * @opts
210
+ * nonce a request nonce Buffer to echo back in responseExtensions (RFC 9654).
211
+ * extendedRevoke emit the id-pkix-ocsp-extended-revoke extension (RFC 6960 sec. 4.4.8).
212
+ * embedCert `false` to omit certs [0] (a direct-CA response the client already trusts).
213
+ * pem emit a PEM `OCSP RESPONSE` string instead of DER.
214
+ * @example
215
+ * var resp = await pki.ocsp.sign(
216
+ * { responderID: "byName", responses: [{ cert: leafDer, issuer: caDer, status: "good" }] },
217
+ * { cert: responderCertDer, key: responderPkcs8 });
218
+ */
219
+ function sign(responseData, responder, opts) {
220
+ opts = opts || {};
221
+ responseData = responseData || {};
222
+ if (!responder || responder.cert == null || responder.key == null) throw _err("ocsp/bad-input", "a responder must be { cert, key }");
223
+ var respCertDer = _normCertDer(responder.cert, "the responder certificate");
224
+ var respCert = _certOf(respCertDer, "the responder certificate");
225
+ var responses = responseData.responses || [];
226
+ if (!responses.length) throw _err("ocsp/bad-input", "a response MUST include at least one SingleResponse (RFC 6960 sec. 4.2.1)");
227
+
228
+ return _responderID(responseData.responderID, respCert).then(function (ridNode) {
229
+ return Promise.all(responses.map(function (r) { return _buildSingleResponse(r, opts); })).then(function (srNodes) {
230
+ var rdChildren = [ridNode, b.generalizedTime(_asDate(responseData.producedAt) || new Date()), b.sequence(srNodes)];
231
+ var respExts = [];
232
+ if (opts.nonce != null) { _assertNonce(opts.nonce); respExts.push(_nonceExt(opts.nonce)); }
233
+ if (opts.extendedRevoke) respExts.push(b.sequence([b.oid(OID_EXTENDED_REVOKE), b.octetString(b.nullValue())]));
234
+ if (respExts.length) rdChildren.push(b.explicit(1, b.sequence(respExts)));
235
+ var responseDataDer = b.sequence(rdChildren);
236
+ var scheme = signScheme.resolveSignScheme(respCert, { combinedRsaSig: true }, true, _signE);
237
+ return signScheme.signOverTbs(scheme, responder.key, responseDataDer, _signE).then(function (sig) {
238
+ var basicChildren = [responseDataDer, scheme.sigAlgId, b.bitString(sig, 0)];
239
+ if (opts.embedCert !== false) basicChildren.push(b.explicit(0, b.sequence([b.raw(respCertDer)])));
240
+ var responseBytes = b.sequence([b.oid(OID_OCSP_BASIC), b.octetString(b.sequence(basicChildren))]);
241
+ var der = b.sequence([b.enumerated(0n), b.explicit(0, responseBytes)]);
242
+ return opts.pem ? ocspSchema.pemEncode(der, "OCSP RESPONSE") : der;
243
+ });
244
+ });
245
+ });
246
+ }
247
+ // A date value -> Date, or null when absent (the caller defaults). An unparseable value is a
248
+ // config-time error, not a silently-emitted Invalid Date: a NaN date would encode as a malformed
249
+ // GeneralizedTime in a SIGNED response, and compare false against every currency bound on verify.
250
+ function _asDate(d) {
251
+ if (d == null) return null;
252
+ var dt = d instanceof Date ? d : new Date(d);
253
+ if (isNaN(dt.getTime())) throw _err("ocsp/bad-input", "an invalid date value " + JSON.stringify(d));
254
+ return dt;
255
+ }
256
+ function _responderID(rid, respCert) {
257
+ if (rid == null || rid === "byName") return Promise.resolve(b.explicit(1, b.raw(respCert.subject.bytes))); // byName [1] EXPLICIT Name
258
+ if (rid === "byKey") return _digest("SHA-1", _keyValue(respCert.subjectPublicKeyInfo.bytes)).then(function (kh) { return b.explicit(2, b.octetString(kh)); });
259
+ throw _err("ocsp/bad-input", "responderID must be \"byName\" or \"byKey\"");
260
+ }
261
+ function _buildSingleResponse(r, opts) {
262
+ r = r || {};
263
+ var certIdP;
264
+ if (r.certID != null) certIdP = Promise.resolve(b.raw(Buffer.isBuffer(r.certID) ? r.certID : Buffer.from(r.certID)));
265
+ else if (r.cert != null && r.issuer != null) certIdP = _buildCertID(_certOf(r.cert, "a response certificate"), _certOf(r.issuer, "a response issuer"), r.hashAlgorithm || "sha1");
266
+ else return Promise.reject(_err("ocsp/bad-input", "each response entry needs { certID } or { cert, issuer }"));
267
+ return certIdP.then(function (certID) {
268
+ var statusNode = _certStatusNode(r.status, opts);
269
+ var srChildren = [certID, statusNode, b.generalizedTime(_asDate(r.thisUpdate) || new Date())];
270
+ if (r.nextUpdate !== null) srChildren.push(b.explicit(0, b.generalizedTime(_asDate(r.nextUpdate) || _defaultNextUpdate())));
271
+ if (r.singleExtensions && r.singleExtensions.length) srChildren.push(b.explicit(1, b.sequence(r.singleExtensions)));
272
+ return b.sequence(srChildren);
273
+ });
274
+ }
275
+ function _defaultNextUpdate() { return new Date(Date.now() + constants.TIME.days(7)); }
276
+ function _certStatusNode(status, opts) {
277
+ if (status == null || status === "good") return b.contextPrimitive(0, Buffer.alloc(0)); // good [0] IMPLICIT NULL
278
+ if (status === "unknown") return b.contextPrimitive(2, Buffer.alloc(0)); // unknown [2] IMPLICIT NULL
279
+ if (typeof status === "object" && status.revoked != null) {
280
+ var ri = [b.generalizedTime(_asDate(status.revoked))]; // revoked != null is guaranteed above
281
+ if (status.revocationReason != null) {
282
+ var code = typeof status.revocationReason === "number" ? status.revocationReason : REASON_CODE[status.revocationReason];
283
+ if (code == null) throw _err("ocsp/bad-input", "unknown revocationReason " + JSON.stringify(status.revocationReason));
284
+ ri.push(b.explicit(0, b.enumerated(BigInt(code)))); // revocationReason [0] EXPLICIT CRLReason
285
+ }
286
+ return b.contextConstructed(1, Buffer.concat(ri)); // revoked [1] IMPLICIT RevokedInfo
287
+ }
288
+ throw _err("ocsp/bad-input", "a response status must be \"good\", \"unknown\", or { revoked: <Date>, revocationReason? }");
289
+ }
290
+
291
+ /**
292
+ * @primitive pki.ocsp.buildErrorResponse
293
+ * @signature pki.ocsp.buildErrorResponse(status) -> Buffer | string
294
+ * @since 0.2.22
295
+ * @status experimental
296
+ * @spec RFC 6960
297
+ * @related pki.ocsp.sign
298
+ *
299
+ * Build an UNSIGNED error OCSPResponse -- `malformedRequest` / `internalError` / `tryLater` /
300
+ * `sigRequired` / `unauthorized` -- carrying only the responseStatus and no responseBytes (RFC 6960
301
+ * sec. 2.3: an error message conveys no certificate status and is not signed).
302
+ *
303
+ * @example
304
+ * var der = pki.ocsp.buildErrorResponse("tryLater");
305
+ */
306
+ var ERROR_STATUS = { malformedRequest: 1, internalError: 2, tryLater: 3, sigRequired: 5, unauthorized: 6 };
307
+ function buildErrorResponse(status) {
308
+ var code = ERROR_STATUS[status];
309
+ if (code == null) throw _err("ocsp/bad-input", "an error responseStatus must be one of " + Object.keys(ERROR_STATUS).join(" / "));
310
+ return b.sequence([b.enumerated(BigInt(code))]);
311
+ }
312
+
313
+ /**
314
+ * @primitive pki.ocsp.verify
315
+ * @signature pki.ocsp.verify(response, opts) -> Promise<{ status, responderAuthorized, signatureValid, thisUpdate, nextUpdate, revocationReason?, nonceMatched?, reason }>
316
+ * @since 0.2.22
317
+ * @status experimental
318
+ * @spec RFC 6960, RFC 9654, RFC 5019
319
+ * @related pki.path.ocspChecker, pki.ocsp.buildRequest
320
+ *
321
+ * Verify a returned OCSP response as a relying party (RFC 6960 sec. 3.2 client acceptance). Resolves
322
+ * an AUTHORIZED responder (the issuing CA directly, or a CA-issued delegate bearing id-kp-OCSPSigning
323
+ * + id-pkix-ocsp-nocheck), verifies the signature over `tbsResponseData`, matches the CertID triple
324
+ * to the target certificate under the CertID's own hashAlgorithm, checks currency
325
+ * (`thisUpdate`/`nextUpdate`), and -- when `opts.requestNonce` is supplied -- confirms the response
326
+ * nonce echoes it. This runs the SAME hardened gates `pki.path.ocspChecker` does. Fail-closed: an
327
+ * unauthorized, stale, mismatched, or nonce-mismatched response is a `"unknown"` verdict (never a
328
+ * silent accept); a malformed response's parse fault surfaces as the parser's `ocsp/*` / `asn1/*`.
329
+ *
330
+ * @opts
331
+ * cert the target certificate (parsed, DER, or PEM) -- REQUIRED.
332
+ * issuer its issuer certificate (parsed, DER, or PEM) -- REQUIRED.
333
+ * time the validation instant (default: now).
334
+ * requestNonce the nonce the client sent; when given, the response MUST echo it (constant-time).
335
+ * historicalMode defer a strictly-future revocation (report good) instead of revoking on skew.
336
+ * @example
337
+ * var res = await pki.ocsp.verify(responseDer, { cert: leafDer, issuer: caDer });
338
+ * res.status; // "good" | "revoked" | "unknown"
339
+ */
340
+ function verify(response, opts) {
341
+ opts = opts || {};
342
+ if (opts.cert == null || opts.issuer == null) return Promise.reject(_err("ocsp/bad-input", "verify requires opts.cert and opts.issuer"));
343
+ var parsed, cert, issuerCert, time;
344
+ try {
345
+ parsed = (response && response.responseStatus) ? response : ocspSchema.parseResponse(_toDer(response, "the OCSP response"));
346
+ cert = _certOf(opts.cert, "the target certificate");
347
+ issuerCert = _certOf(opts.issuer, "the issuer certificate");
348
+ // The time drives the currency + responder-cert validity windows; an invalid Date fails closed
349
+ // via _asDate (a NaN compares false against every bound, silently disabling both), never defaults.
350
+ time = opts.time == null ? new Date() : _asDate(opts.time);
351
+ } catch (e) { return Promise.reject(e); }
352
+ return pathValidate.verifyOcspResponse(parsed, cert, issuerCert, time, { historicalMode: opts.historicalMode === true }).then(function (verdict) {
353
+ if (opts.requestNonce == null) return verdict;
354
+ // A client that sent a nonce binds it (RFC 9654 / RFC 5019 sec. 4): a missing or mismatched
355
+ // response nonce fails the verdict closed, even if the status/signature were otherwise good.
356
+ var respNonce = _responseNonce(parsed);
357
+ var reqNonce = Buffer.isBuffer(opts.requestNonce) ? opts.requestNonce : (opts.requestNonce instanceof Uint8Array ? Buffer.from(opts.requestNonce) : null);
358
+ var matched = respNonce != null && reqNonce != null && guard.crypto.constantTimeEqual(respNonce, reqNonce);
359
+ var out = Object.assign({}, verdict, { nonceMatched: matched });
360
+ if (!matched && verdict.status !== "unknown") {
361
+ return Object.assign(out, { status: "unknown", reason: "the OCSP response nonce does not echo the request nonce (RFC 9654)" });
362
+ }
363
+ return out;
364
+ });
365
+ }
366
+ function _responseNonce(parsed) {
367
+ var br = parsed && parsed.basicResponse;
368
+ var exts = (br && br.responseExtensions) || [];
369
+ for (var i = 0; i < exts.length; i++) if (exts[i].oid === OID_OCSP_NONCE) return exts[i].nonce || null;
370
+ return null;
371
+ }
372
+
373
+ module.exports = {
374
+ buildRequest: buildRequest,
375
+ sign: sign,
376
+ buildErrorResponse: buildErrorResponse,
377
+ verify: verify,
378
+ };
package/lib/oid.js CHANGED
@@ -553,7 +553,7 @@ var _PARAMS_ABSENT = new Set();
553
553
  "id-hash-slh-dsa-shake-192s-with-shake256", "id-hash-slh-dsa-shake-192f-with-shake256",
554
554
  "id-hash-slh-dsa-shake-256s-with-shake256", "id-hash-slh-dsa-shake-256f-with-shake256",
555
555
  "Ed25519", "Ed448", "X25519", "X448",
556
- // FIPS 203 ML-KEM (RFC 9936 sec. 3: PARAMS ARE absent).
556
+ // FIPS 203 ML-KEM (RFC 9935 sec. 3 certificates + RFC 9936 sec. 3 CMS: PARAMS ARE absent).
557
557
  "id-ml-kem-512", "id-ml-kem-768", "id-ml-kem-1024",
558
558
  // HKDF key-derivation identifiers (RFC 8619 sec. 2: when any of these appear
559
559
  // within AlgorithmIdentifier, the parameters component SHALL be absent).