@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/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
|
+
};
|