@blamejs/pki 0.4.15 → 0.5.0
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 +37 -1
- package/MIGRATING.md +2 -2
- package/README.md +142 -137
- package/index.js +4 -0
- package/lib/acme.js +73 -1
- package/lib/asn1-der.js +2 -0
- package/lib/attrcert-sign.js +4 -0
- package/lib/cbor-det.js +32 -16
- package/lib/cmc-build.js +880 -0
- package/lib/cmc-verify.js +657 -0
- package/lib/cmp-build.js +8 -7
- package/lib/cmp-verify.js +11 -1
- package/lib/cms-sign.js +170 -8
- package/lib/cms-verify.js +80 -14
- package/lib/crl-sign.js +22 -0
- package/lib/crmf-sign.js +5 -2
- package/lib/csr-sign.js +3 -0
- package/lib/ct.js +72 -0
- package/lib/est.js +828 -32
- package/lib/framework-error.js +13 -0
- package/lib/guard-bytes.js +37 -1
- package/lib/guard-range.js +23 -1
- package/lib/http-transport.js +9 -3
- package/lib/inspect.js +28 -5
- package/lib/jose.js +15 -0
- package/lib/lint.js +4 -0
- package/lib/merkle.js +5 -5
- package/lib/ocsp.js +139 -11
- package/lib/oid.js +69 -1
- package/lib/path-validate.js +27 -4
- package/lib/pkcs12-build.js +12 -0
- package/lib/schema-all.js +19 -1
- package/lib/schema-attrcert.js +27 -0
- package/lib/schema-c509.js +6 -0
- package/lib/schema-cmc.js +791 -0
- package/lib/schema-cmp.js +25 -0
- package/lib/schema-cms.js +17 -1
- package/lib/schema-crl.js +23 -1
- package/lib/schema-crmf.js +13 -0
- package/lib/schema-csr.js +11 -0
- package/lib/schema-csrattrs.js +6 -0
- package/lib/schema-engine.js +6 -2
- package/lib/schema-ocsp.js +41 -0
- package/lib/schema-pkcs12.js +16 -0
- package/lib/schema-pkcs8.js +8 -0
- package/lib/schema-smime.js +4 -4
- package/lib/schema-tsp.js +32 -1
- package/lib/schema-x509.js +14 -1
- package/lib/shbs.js +12 -4
- package/lib/sigstore.js +4 -0
- package/lib/smime.js +28 -7
- package/lib/tls-cert-compress.js +15 -3
- package/lib/trust.js +27 -4
- package/lib/tsp-sign.js +41 -6
- package/lib/vendor/README.md +19 -19
- package/lib/webauthn.js +895 -26
- package/lib/x509-sign.js +3 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/framework-error.js
CHANGED
|
@@ -47,6 +47,7 @@ var CODE_SHAPE = /^[a-z0-9-]+\/[a-z0-9-]+$/;
|
|
|
47
47
|
* site instead of shipping prose into a code-switching consumer.
|
|
48
48
|
*
|
|
49
49
|
* @example
|
|
50
|
+
* var bytes = Buffer.from([0x30, 0x80]); // indefinite length -- not valid DER
|
|
50
51
|
* try { pki.asn1.decode(bytes); }
|
|
51
52
|
* catch (e) {
|
|
52
53
|
* if (e instanceof pki.errors.PkiError) console.error(e.code);
|
|
@@ -88,6 +89,7 @@ class PkiError extends Error {
|
|
|
88
89
|
* withCause: boolean, // default: false -- constructor becomes (code, message, cause)
|
|
89
90
|
*
|
|
90
91
|
* @example
|
|
92
|
+
* // throws: my/bad-input -- raising the new error type IS what this shows
|
|
91
93
|
* var MyError = pki.errors.defineClass("MyError");
|
|
92
94
|
* throw new MyError("my/bad-input", "explanation");
|
|
93
95
|
*/
|
|
@@ -273,6 +275,16 @@ var CsrattrsError = defineClass("CsrattrsError", { withCause: true });
|
|
|
273
275
|
// `.cause`.
|
|
274
276
|
var EstError = defineClass("EstError", { withCause: true });
|
|
275
277
|
|
|
278
|
+
// CmcError -- an RFC 5272 / 6402 Certificate Management over CMS message-layer
|
|
279
|
+
// fault: a PKIData / PKIResponse whose structure, body-part identity, control
|
|
280
|
+
// placement or status encoding is malformed, a control this side must act on
|
|
281
|
+
// that cannot be decoded, or an exchange binding (transactionId / recipientNonce)
|
|
282
|
+
// that does not match the request it answers. Distinct from EstError because the
|
|
283
|
+
// CMC layer is the MESSAGE and EST is the TRANSPORT that carried it: an operator
|
|
284
|
+
// needs to know whether the CA's answer was unreadable or the HTTP hop failed.
|
|
285
|
+
// Carries the underlying cms/* or asn1/* leaf fault as `.cause`.
|
|
286
|
+
var CmcError = defineClass("CmcError", { withCause: true });
|
|
287
|
+
|
|
276
288
|
// TransportError -- a fault from the shared node:https transport (pki.transport):
|
|
277
289
|
// a non-https / unparseable request URL, a missing trust anchor, a TLS handshake /
|
|
278
290
|
// server-authentication failure, a negotiated protocol below the floor, a response
|
|
@@ -380,6 +392,7 @@ module.exports = {
|
|
|
380
392
|
SmimeError: SmimeError,
|
|
381
393
|
CsrattrsError: CsrattrsError,
|
|
382
394
|
EstError: EstError,
|
|
395
|
+
CmcError: CmcError,
|
|
383
396
|
TransportError: TransportError,
|
|
384
397
|
JoseError: JoseError,
|
|
385
398
|
AcmeError: AcmeError,
|
package/lib/guard-bytes.js
CHANGED
|
@@ -57,4 +57,40 @@ function source(input, ErrorClass, code, label) {
|
|
|
57
57
|
throw new ErrorClass(code, label + ": expected a BufferSource (ArrayBuffer / TypedArray / Buffer)");
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
|
|
60
|
+
// snapshot(input, ErrorClass, code, label) -> private Buffer copy | throws ErrorClass
|
|
61
|
+
//
|
|
62
|
+
// The parse-then-verify time-of-check/time-of-use defence. A verification entry
|
|
63
|
+
// point that PARSES its input synchronously and then VERIFIES a signature over
|
|
64
|
+
// the same bytes in a later promise turn is reading the caller's memory twice
|
|
65
|
+
// with an await in between. Every byte range the parse surfaced -- the signed
|
|
66
|
+
// content, the signer set, the values the verdict is built from -- is a VIEW into
|
|
67
|
+
// that memory, so anything that rewrites the buffer in the gap makes the verdict
|
|
68
|
+
// describe bytes other than the ones the signature was checked against
|
|
69
|
+
// (CWE-367 TOCTOU reaching a CWE-347 wrong-verdict). The window is real without
|
|
70
|
+
// an attacker in the process: a caller recycling a pooled read buffer across
|
|
71
|
+
// concurrent verifies hits it by accident.
|
|
72
|
+
//
|
|
73
|
+
// So take one private copy at the boundary and read EVERYTHING from it. `view`
|
|
74
|
+
// re-views and is the right guard where the input is consumed in one synchronous
|
|
75
|
+
// pass; this is its sibling for the boundary that spans an await.
|
|
76
|
+
// @enforced-by behavioral -- a copy has no rename-proof code shape to detect (any
|
|
77
|
+
// `Buffer.from(x)` is one, and most are legitimate). The guard is the RED vector
|
|
78
|
+
// that mutates the caller's buffer between parse and verify and asserts the
|
|
79
|
+
// verdict still describes the bytes that were verified.
|
|
80
|
+
function snapshot(input, ErrorClass, code, label) {
|
|
81
|
+
return Buffer.from(view(input, ErrorClass, code, label));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// snapshotSource(input, ErrorClass, code, label) -> private Buffer copy | throws
|
|
85
|
+
// The same parse-then-verify defence as `snapshot`, over the FULL W3C BufferSource
|
|
86
|
+
// (raw ArrayBuffer / DataView / any typed-array view) rather than only the
|
|
87
|
+
// Buffer / Uint8Array contract. A parser that accepts a BufferSource must snapshot
|
|
88
|
+
// the same set: leaving an ArrayBuffer or a DataView aliased reopens the window for
|
|
89
|
+
// exactly the inputs that took the wider path in.
|
|
90
|
+
// @enforced-by behavioral -- a copy has no rename-proof shape to detect; the guard
|
|
91
|
+
// is the RED vector that mutates the caller's backing buffer across the await.
|
|
92
|
+
function snapshotSource(input, ErrorClass, code, label) {
|
|
93
|
+
return Buffer.from(source(input, ErrorClass, code, label));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = { view: view, source: source, snapshot: snapshot, snapshotSource: snapshotSource };
|
package/lib/guard-range.js
CHANGED
|
@@ -99,4 +99,26 @@ function uint64(value, E, code, label) {
|
|
|
99
99
|
return v;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
|
|
102
|
+
// authoredInteger(value, E, code, label) -> BigInt. The AUTHORING counterpart to
|
|
103
|
+
// int(): a value a CALLER supplies for an ASN.1 INTEGER with no upper bound (a CMC
|
|
104
|
+
// Transaction Identifier, a CRMF certReqId), normalized to the BigInt the builders
|
|
105
|
+
// take. Accepts a bigint, or a Number that is a SAFE integer -- isSafeInteger, not
|
|
106
|
+
// isInteger, because a Number above 2^53 has already lost precision by the time it
|
|
107
|
+
// arrives, so a large identifier MUST come as a bigint rather than be silently
|
|
108
|
+
// rounded to a neighbour. Everything else is a config-time reject.
|
|
109
|
+
//
|
|
110
|
+
// Distinct from int() on purpose: int() BOUNDS a value the wire decoded and narrows
|
|
111
|
+
// it to Number; this one is unbounded and returns BigInt, because the field has no
|
|
112
|
+
// ceiling to check against and the encoder wants the wide type.
|
|
113
|
+
// @enforced-by guard-shape-reinlined
|
|
114
|
+
// @guard-shape Number\.isSafeInteger\(\s*[A-Za-z_$][\w$]*\s*\)\s*\)\s*return BigInt
|
|
115
|
+
function authoredInteger(value, E, code, label) {
|
|
116
|
+
if (typeof value === "bigint") return value;
|
|
117
|
+
if (typeof value === "number" && Number.isSafeInteger(value)) return BigInt(value);
|
|
118
|
+
throw E(code, label + " must be an integer (a safe-integer number, or a bigint for a large value)");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = {
|
|
122
|
+
int: int, uint31: uint31, positiveInt31: positiveInt31, uint64: uint64,
|
|
123
|
+
authoredInteger: authoredInteger,
|
|
124
|
+
};
|
package/lib/http-transport.js
CHANGED
|
@@ -13,11 +13,17 @@
|
|
|
13
13
|
* drive -- `pki.est` now, `pki.acme` and `pki.cmp` next. This is the ONLY module in
|
|
14
14
|
* the toolkit that opens a socket; every protocol layer stays transport-agnostic and
|
|
15
15
|
* composes it (or an injected substitute) through one contract:
|
|
16
|
-
* `transport(request) -> Promise<{ status, headers, body }>`. The
|
|
16
|
+
* `transport(request) -> Promise<{ status, headers, body, tls }>`. The first three are
|
|
17
17
|
* exactly what a message layer's classifier consumes, so no protocol semantics leak
|
|
18
18
|
* into the socket layer -- the transport owns socket lifecycle, the TLS trust policy,
|
|
19
19
|
* the streaming size cap, and the timeout budget; the caller owns HTTP status,
|
|
20
|
-
* content-type, redirect, and authentication decisions.
|
|
20
|
+
* content-type, redirect, and authentication decisions. `tls` reports the negotiated
|
|
21
|
+
* channel -- `{ protocol, cipher, peerCertificate }` -- which is the one fact a caller
|
|
22
|
+
* cannot recover from the response bytes. An INJECTED substitute should return it too:
|
|
23
|
+
* `pki.est.serverkeygen` asserts the negotiated cipher can protect the private key it
|
|
24
|
+
* is about to accept, and a transport that reports no cipher is trusted rather than
|
|
25
|
+
* refused (so a loopback test channel works), which means omitting the field silently
|
|
26
|
+
* skips that assertion.
|
|
21
27
|
*
|
|
22
28
|
* `pki.transport.https(defaults?)` binds TLS + budget defaults and returns a
|
|
23
29
|
* transport. Trust is EXPLICIT and fail-closed: a request is refused unless it
|
|
@@ -33,7 +39,7 @@
|
|
|
33
39
|
* @card
|
|
34
40
|
* The shared fail-closed node:https transport (est / acme / cmp): explicit trust
|
|
35
41
|
* anchors, rejectUnauthorized always on, a TLS floor, a streaming response-size cap,
|
|
36
|
-
* and a timeout -- behind one `transport(request) -> {status, headers, body}` seam.
|
|
42
|
+
* and a timeout -- behind one `transport(request) -> {status, headers, body, tls}` seam.
|
|
37
43
|
*/
|
|
38
44
|
|
|
39
45
|
var nodeHttps = require("node:https");
|
package/lib/inspect.js
CHANGED
|
@@ -607,7 +607,7 @@ function _parse(input) {
|
|
|
607
607
|
* @primitive pki.inspect.certificate
|
|
608
608
|
* @signature pki.inspect.certificate(input) -> string
|
|
609
609
|
* @since 0.2.4
|
|
610
|
-
* @status
|
|
610
|
+
* @status stable
|
|
611
611
|
* @spec RFC 5280
|
|
612
612
|
* @related pki.schema.x509.parse
|
|
613
613
|
*
|
|
@@ -618,6 +618,10 @@ function _parse(input) {
|
|
|
618
618
|
* hex dump rather than failing the whole report. Pure -- no OpenSSL dependency.
|
|
619
619
|
*
|
|
620
620
|
* @example
|
|
621
|
+
* var pair = await pki.key.generate("Ed25519");
|
|
622
|
+
* var der = await pki.x509.sign({ subject: "example.com", subjectPublicKey: await pki.key.export(pair.publicKey),
|
|
623
|
+
* notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") },
|
|
624
|
+
* { key: await pki.key.export(pair.privateKey) });
|
|
621
625
|
* var cert = pki.schema.x509.parse(der);
|
|
622
626
|
* pki.inspect.certificate(cert).split("\n")[0]; // "Certificate:"
|
|
623
627
|
*/
|
|
@@ -753,7 +757,7 @@ function _crlExtension(ext, pad) {
|
|
|
753
757
|
* @primitive pki.inspect.crl
|
|
754
758
|
* @signature pki.inspect.crl(input) -> string
|
|
755
759
|
* @since 0.3.8
|
|
756
|
-
* @status
|
|
760
|
+
* @status stable
|
|
757
761
|
* @spec RFC 5280
|
|
758
762
|
* @related pki.schema.crl.parse, pki.inspect.certificate
|
|
759
763
|
*
|
|
@@ -765,6 +769,13 @@ function _crlExtension(ext, pad) {
|
|
|
765
769
|
* extension renders as hex rather than failing the report.
|
|
766
770
|
*
|
|
767
771
|
* @example
|
|
772
|
+
* var pair = await pki.key.generate("Ed25519");
|
|
773
|
+
* var key = await pki.key.export(pair.privateKey);
|
|
774
|
+
* var caCert = await pki.x509.sign({ subject: "Issuing CA", subjectPublicKey: await pki.key.export(pair.publicKey),
|
|
775
|
+
* notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
|
|
776
|
+
* extensions: { basicConstraints: { cA: true }, keyUsage: ["cRLSign"] } }, { key: key });
|
|
777
|
+
* var crlDer = await pki.crl.sign({ thisUpdate: new Date("2026-01-01T00:00:00Z"), crlNumber: 1n, revoked: [] },
|
|
778
|
+
* { cert: caCert, key: key });
|
|
768
779
|
* pki.inspect.crl(crlDer).split("\n")[0]; // "Certificate Revocation List (CRL):"
|
|
769
780
|
*/
|
|
770
781
|
function crlReport(input) {
|
|
@@ -818,7 +829,7 @@ function _attribute(attr, pad) {
|
|
|
818
829
|
* @primitive pki.inspect.csr
|
|
819
830
|
* @signature pki.inspect.csr(input) -> string
|
|
820
831
|
* @since 0.3.8
|
|
821
|
-
* @status
|
|
832
|
+
* @status stable
|
|
822
833
|
* @spec RFC 2986
|
|
823
834
|
* @related pki.schema.csr.parse, pki.inspect.certificate
|
|
824
835
|
*
|
|
@@ -829,6 +840,9 @@ function _attribute(attr, pad) {
|
|
|
829
840
|
* input `inspect/bad-input`. Best-effort like `certificate`.
|
|
830
841
|
*
|
|
831
842
|
* @example
|
|
843
|
+
* var pair = await pki.key.generate("Ed25519");
|
|
844
|
+
* var csrDer = await pki.csr.sign({ subject: "req.example", subjectPublicKey: await pki.key.export(pair.publicKey) },
|
|
845
|
+
* { key: await pki.key.export(pair.privateKey) });
|
|
832
846
|
* pki.inspect.csr(csrDer).split("\n")[0]; // "Certificate Request:"
|
|
833
847
|
*/
|
|
834
848
|
function csrReport(input) {
|
|
@@ -927,7 +941,7 @@ function _signerInfo(si, pad) {
|
|
|
927
941
|
* @primitive pki.inspect.cms
|
|
928
942
|
* @signature pki.inspect.cms(input) -> string
|
|
929
943
|
* @since 0.3.8
|
|
930
|
-
* @status
|
|
944
|
+
* @status stable
|
|
931
945
|
* @spec RFC 5652
|
|
932
946
|
* @related pki.schema.cms.parse, pki.inspect.certificate
|
|
933
947
|
*
|
|
@@ -939,6 +953,11 @@ function _signerInfo(si, pad) {
|
|
|
939
953
|
* `pki.schema.cms.parse` result; a non-CMS throws `inspect/bad-cms`. Best-effort.
|
|
940
954
|
*
|
|
941
955
|
* @example
|
|
956
|
+
* var pair = await pki.key.generate("Ed25519");
|
|
957
|
+
* var key = await pki.key.export(pair.privateKey);
|
|
958
|
+
* var cert = await pki.x509.sign({ subject: "Signer", subjectPublicKey: await pki.key.export(pair.publicKey),
|
|
959
|
+
* notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") }, { key: key });
|
|
960
|
+
* var cmsDer = await pki.cms.sign(Buffer.from("hello"), { cert: cert, key: key });
|
|
942
961
|
* pki.inspect.cms(cmsDer).split("\n")[0]; // "CMS ContentInfo:"
|
|
943
962
|
*/
|
|
944
963
|
// A ContentInfo whose content type pki.schema.cms.parse does not dispatch (id-data,
|
|
@@ -1003,7 +1022,7 @@ var _INSPECT_BY_FORMAT = { x509: certificate, crl: crlReport, csr: csrReport, cm
|
|
|
1003
1022
|
* @primitive pki.inspect.any
|
|
1004
1023
|
* @signature pki.inspect.any(input) -> string
|
|
1005
1024
|
* @since 0.3.8
|
|
1006
|
-
* @status
|
|
1025
|
+
* @status stable
|
|
1007
1026
|
* @spec RFC 5280
|
|
1008
1027
|
* @related pki.schema.detectFormat, pki.inspect.certificate
|
|
1009
1028
|
*
|
|
@@ -1015,6 +1034,10 @@ var _INSPECT_BY_FORMAT = { x509: certificate, crl: crlReport, csr: csrReport, cm
|
|
|
1015
1034
|
* `inspect/bad-input`.
|
|
1016
1035
|
*
|
|
1017
1036
|
* @example
|
|
1037
|
+
* var pair = await pki.key.generate("Ed25519");
|
|
1038
|
+
* var der = await pki.x509.sign({ subject: "example.com", subjectPublicKey: await pki.key.export(pair.publicKey),
|
|
1039
|
+
* notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") },
|
|
1040
|
+
* { key: await pki.key.export(pair.privateKey) });
|
|
1018
1041
|
* pki.inspect.any(der); // routes to the right report by detected format
|
|
1019
1042
|
*/
|
|
1020
1043
|
function any(input) {
|
package/lib/jose.js
CHANGED
|
@@ -347,6 +347,11 @@ function assertPublicJwk(jwk) {
|
|
|
347
347
|
* key: object // a public JWK, required unless the profile embeds jwk
|
|
348
348
|
*
|
|
349
349
|
* @example
|
|
350
|
+
* var ec = await pki.key.generate({ name: "ECDSA", namedCurve: "P-256" });
|
|
351
|
+
* var accountJwk = await pki.webcrypto.subtle.exportKey("jwk", ec.publicKey);
|
|
352
|
+
* var jws = await pki.jose.sign({ protected: { alg: "ES256", jwk: accountJwk,
|
|
353
|
+
* nonce: "oFvnlFP1wIhRlYS2jTaXbA", url: "https://ca.example/acme/new-acct" },
|
|
354
|
+
* payload: Buffer.from("{}"), key: ec.privateKey });
|
|
350
355
|
* var v = await pki.jose.verify(jws, { profile: "acme-outer", key: accountJwk });
|
|
351
356
|
* v.header.alg; // -> "ES256"
|
|
352
357
|
*/
|
|
@@ -405,6 +410,10 @@ async function verify(jws, opts) {
|
|
|
405
410
|
* jwk: object // the public JWK, when the header embeds jwk
|
|
406
411
|
*
|
|
407
412
|
* @example
|
|
413
|
+
* var ec = await pki.key.generate({ name: "ECDSA", namedCurve: "P-256" });
|
|
414
|
+
* var priv = ec.privateKey;
|
|
415
|
+
* var hdr = { alg: "ES256", jwk: await pki.webcrypto.subtle.exportKey("jwk", ec.publicKey),
|
|
416
|
+
* nonce: "oFvnlFP1wIhRlYS2jTaXbA", url: "https://ca.example/acme/new-acct" };
|
|
408
417
|
* var jws = await pki.jose.sign({ protected: hdr, payload: Buffer.from("{}"), key: priv });
|
|
409
418
|
*/
|
|
410
419
|
async function sign(opts) {
|
|
@@ -491,6 +500,12 @@ var THUMBPRINT_MEMBERS = {
|
|
|
491
500
|
* always yields the same thumbprint (the ACME key-authorization anchor).
|
|
492
501
|
*
|
|
493
502
|
* @example
|
|
503
|
+
* // the RFC 7638 sec. 3.1 worked example, so the thumbprint below is the spec's own
|
|
504
|
+
* var accountJwk = { kty: "RSA", e: "AQAB", n: "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78L" +
|
|
505
|
+
* "hWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXA" +
|
|
506
|
+
* "rwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajr" +
|
|
507
|
+
* "n1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-" +
|
|
508
|
+
* "kEgU8awapJzKnqDKgw" };
|
|
494
509
|
* await pki.jose.thumbprint(accountJwk); // -> "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs"
|
|
495
510
|
*/
|
|
496
511
|
async function thumbprint(jwk) {
|
package/lib/lint.js
CHANGED
|
@@ -715,6 +715,10 @@ function _applyThreshold(report, severity) {
|
|
|
715
715
|
* @opts severity Suppress findings below this floor (default `"notice"`). `counts` and
|
|
716
716
|
* `worst` always reflect the complete, unfiltered result.
|
|
717
717
|
* @example
|
|
718
|
+
* var pair = await pki.key.generate("Ed25519");
|
|
719
|
+
* var pemString = await pki.x509.sign({ subject: "example.com", subjectPublicKey: await pki.key.export(pair.publicKey),
|
|
720
|
+
* notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") },
|
|
721
|
+
* { key: await pki.key.export(pair.privateKey) }, { pem: true });
|
|
718
722
|
* var report = pki.lint.certificate(pemString);
|
|
719
723
|
* report.worst; // "notice" | "error" | ...
|
|
720
724
|
* report.findings.map(function (f) { return f.id; });
|
package/lib/merkle.js
CHANGED
|
@@ -102,7 +102,7 @@ function _ctEq(a, b) {
|
|
|
102
102
|
* @primitive pki.merkle.leafHash
|
|
103
103
|
* @signature pki.merkle.leafHash(entry) -> Buffer
|
|
104
104
|
* @since 0.1.28
|
|
105
|
-
* @status
|
|
105
|
+
* @status stable
|
|
106
106
|
* @spec RFC 6962, RFC 9162
|
|
107
107
|
* @related pki.merkle.nodeHash, pki.merkle.verifyInclusion
|
|
108
108
|
*
|
|
@@ -122,7 +122,7 @@ function leafHash(entry) {
|
|
|
122
122
|
* @primitive pki.merkle.nodeHash
|
|
123
123
|
* @signature pki.merkle.nodeHash(left, right) -> Buffer
|
|
124
124
|
* @since 0.1.28
|
|
125
|
-
* @status
|
|
125
|
+
* @status stable
|
|
126
126
|
* @spec RFC 6962, RFC 9162
|
|
127
127
|
* @related pki.merkle.leafHash
|
|
128
128
|
*
|
|
@@ -146,7 +146,7 @@ function nodeHash(left, right) {
|
|
|
146
146
|
* @primitive pki.merkle.emptyRootHash
|
|
147
147
|
* @signature pki.merkle.emptyRootHash() -> Buffer
|
|
148
148
|
* @since 0.1.28
|
|
149
|
-
* @status
|
|
149
|
+
* @status stable
|
|
150
150
|
* @spec RFC 6962, RFC 9162
|
|
151
151
|
* @related pki.merkle.verifyConsistency
|
|
152
152
|
*
|
|
@@ -164,7 +164,7 @@ function emptyRootHash() {
|
|
|
164
164
|
* @primitive pki.merkle.verifyInclusion
|
|
165
165
|
* @signature pki.merkle.verifyInclusion(opts) -> boolean
|
|
166
166
|
* @since 0.1.28
|
|
167
|
-
* @status
|
|
167
|
+
* @status stable
|
|
168
168
|
* @spec RFC 6962, RFC 9162
|
|
169
169
|
* @related pki.merkle.leafHash, pki.merkle.verifyConsistency
|
|
170
170
|
*
|
|
@@ -223,7 +223,7 @@ function verifyInclusion(opts) {
|
|
|
223
223
|
* @primitive pki.merkle.verifyConsistency
|
|
224
224
|
* @signature pki.merkle.verifyConsistency(opts) -> boolean
|
|
225
225
|
* @since 0.1.28
|
|
226
|
-
* @status
|
|
226
|
+
* @status stable
|
|
227
227
|
* @spec RFC 6962, RFC 9162
|
|
228
228
|
* @related pki.merkle.verifyInclusion, pki.merkle.emptyRootHash
|
|
229
229
|
*
|
package/lib/ocsp.js
CHANGED
|
@@ -15,7 +15,9 @@
|
|
|
15
15
|
* ML-DSA / SLH-DSA per the responder key. Verification composes the SAME hardened responder-
|
|
16
16
|
* authorization + signature + currency gates `pki.path.ocspChecker` runs -- there is no weaker
|
|
17
17
|
* second verify path. Fail-closed: `verify` returns a `"unknown"` verdict (never a silent accept)
|
|
18
|
-
* for any unmet gate
|
|
18
|
+
* for any unmet gate, with ONE scoped exception -- a request-nonce mismatch downgrades only a
|
|
19
|
+
* `good` to `"unknown"`, leaving a signed, current, authorized `revoked` reported as `revoked`
|
|
20
|
+
* with `nonceMatched: false` (see `verify`). Malformed input throws a typed `OcspError`.
|
|
19
21
|
* @spec RFC 6960, RFC 9654, RFC 5019
|
|
20
22
|
* @card Build, sign, and verify RFC 6960 OCSP requests + responses (a responder + a relying party).
|
|
21
23
|
*/
|
|
@@ -103,7 +105,7 @@ function _buildCertID(cert, issuer, hashName) {
|
|
|
103
105
|
* @primitive pki.ocsp.buildRequest
|
|
104
106
|
* @signature pki.ocsp.buildRequest(query, opts?) -> Buffer | string
|
|
105
107
|
* @since 0.2.22
|
|
106
|
-
* @status
|
|
108
|
+
* @status stable
|
|
107
109
|
* @spec RFC 6960, RFC 9654, RFC 5019
|
|
108
110
|
* @related pki.ocsp.verify, pki.schema.ocsp.parseRequest
|
|
109
111
|
*
|
|
@@ -121,6 +123,15 @@ function _buildCertID(cert, issuer, hashName) {
|
|
|
121
123
|
* profile `"lightweight"` -- one Request, SHA-1 CertID, nonce-only extensions (RFC 5019).
|
|
122
124
|
* pem emit a PEM `OCSP REQUEST` string instead of DER.
|
|
123
125
|
* @example
|
|
126
|
+
* var ca = await pki.key.generate("Ed25519");
|
|
127
|
+
* var caKey = await pki.key.export(ca.privateKey);
|
|
128
|
+
* var caDer = await pki.x509.sign({ subject: "Example CA", subjectPublicKey: await pki.key.export(ca.publicKey),
|
|
129
|
+
* notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
|
|
130
|
+
* extensions: { basicConstraints: { cA: true }, keyUsage: ["keyCertSign"], subjectKeyIdentifier: true } }, { key: caKey });
|
|
131
|
+
* var leaf = await pki.key.generate("Ed25519");
|
|
132
|
+
* var leafDer = await pki.x509.sign({ subject: "leaf.example", subjectPublicKey: await pki.key.export(leaf.publicKey),
|
|
133
|
+
* notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") },
|
|
134
|
+
* { cert: caDer, key: caKey });
|
|
124
135
|
* var der = await pki.ocsp.buildRequest({ cert: leafDer, issuer: caDer }, { nonce: true });
|
|
125
136
|
*/
|
|
126
137
|
function buildRequest(query, opts) {
|
|
@@ -195,7 +206,7 @@ function _normCertDer(cert, what) {
|
|
|
195
206
|
* @primitive pki.ocsp.sign
|
|
196
207
|
* @signature pki.ocsp.sign(responseData, responder, opts?) -> Promise<Buffer | string>
|
|
197
208
|
* @since 0.2.22
|
|
198
|
-
* @status
|
|
209
|
+
* @status stable
|
|
199
210
|
* @spec RFC 6960, RFC 9654
|
|
200
211
|
* @related pki.ocsp.verify, pki.ocsp.buildErrorResponse
|
|
201
212
|
*
|
|
@@ -212,6 +223,18 @@ function _normCertDer(cert, what) {
|
|
|
212
223
|
* embedCert `false` to omit certs [0] (a direct-CA response the client already trusts).
|
|
213
224
|
* pem emit a PEM `OCSP RESPONSE` string instead of DER.
|
|
214
225
|
* @example
|
|
226
|
+
* var ca = await pki.key.generate("Ed25519");
|
|
227
|
+
* var responderPkcs8 = await pki.key.export(ca.privateKey);
|
|
228
|
+
* // the issuing CA responds directly here, so its own certificate is the responder's
|
|
229
|
+
* var caDer = await pki.x509.sign({ subject: "Example CA", subjectPublicKey: await pki.key.export(ca.publicKey),
|
|
230
|
+
* notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
|
|
231
|
+
* extensions: { basicConstraints: { cA: true }, keyUsage: ["keyCertSign"], subjectKeyIdentifier: true } },
|
|
232
|
+
* { key: responderPkcs8 });
|
|
233
|
+
* var responderCertDer = caDer;
|
|
234
|
+
* var leaf = await pki.key.generate("Ed25519");
|
|
235
|
+
* var leafDer = await pki.x509.sign({ subject: "leaf.example", subjectPublicKey: await pki.key.export(leaf.publicKey),
|
|
236
|
+
* notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") },
|
|
237
|
+
* { cert: caDer, key: responderPkcs8 });
|
|
215
238
|
* var resp = await pki.ocsp.sign(
|
|
216
239
|
* { responderID: "byName", responses: [{ cert: leafDer, issuer: caDer, status: "good" }] },
|
|
217
240
|
* { cert: responderCertDer, key: responderPkcs8 });
|
|
@@ -222,6 +245,43 @@ function sign(responseData, responder, opts) {
|
|
|
222
245
|
if (!responder || responder.cert == null || responder.key == null) throw _err("ocsp/bad-input", "a responder must be { cert, key }");
|
|
223
246
|
var respCertDer = _normCertDer(responder.cert, "the responder certificate");
|
|
224
247
|
var respCert = _certOf(respCertDer, "the responder certificate");
|
|
248
|
+
// Read with the certificate, not after the awaits below. The ResponderID and the
|
|
249
|
+
// embedded certificate are both fixed from `respCert` here; the signature is made
|
|
250
|
+
// several promise turns later, so a `responder` whose key was replaced in between
|
|
251
|
+
// would produce a response naming one responder and signed by another key.
|
|
252
|
+
//
|
|
253
|
+
// Capturing the REFERENCE only closes half of that: it stops `responder.key = other`,
|
|
254
|
+
// but PKCS#8 arrives as caller-owned BYTES and a composite key as a caller-owned
|
|
255
|
+
// { mldsa, trad } object, either of which can be rewritten in place across the same
|
|
256
|
+
// gap with the same result -- a response carrying this responder's ID and certificate
|
|
257
|
+
// over a signature made by a different key, which no relying party can verify. So the
|
|
258
|
+
// material itself is snapshotted here, not just the binding to it. A CryptoKey is
|
|
259
|
+
// opaque and a PEM string is immutable, so both are already safe by reference.
|
|
260
|
+
var ownedKeyBytes = [];
|
|
261
|
+
var responderKey = _snapshotSignerKey(responder.key, ownedKeyBytes);
|
|
262
|
+
// The copy is cleared on the failing path as well as the succeeding one -- a wrong or
|
|
263
|
+
// malformed key is the case an attacker can force, so a success-only wipe would keep the
|
|
264
|
+
// secret exactly when it matters. The wipe covers the WHOLE window the copy exists, not
|
|
265
|
+
// just the signing call: the copy is taken before the response list, the responder ID, the
|
|
266
|
+
// SingleResponses, the dates, the nonce and the signature scheme are validated, and every
|
|
267
|
+
// one of those can fail. Attaching cleanup to signing alone leaves the secret in the heap
|
|
268
|
+
// on each of those earlier exits, which are the easiest ones for a caller to reach.
|
|
269
|
+
function _wipeOwnedKey() {
|
|
270
|
+
if (ownedKeyBytes.length) guard.secret.zeroizeAll(ownedKeyBytes, OcspError, "ocsp/bad-input", "the responder key copy");
|
|
271
|
+
ownedKeyBytes.length = 0;
|
|
272
|
+
}
|
|
273
|
+
var pending;
|
|
274
|
+
try {
|
|
275
|
+
pending = _signResponse(responseData, responder, respCert, respCertDer, responderKey, opts);
|
|
276
|
+
} catch (e) { _wipeOwnedKey(); throw e; }
|
|
277
|
+
return pending.then(function (out) { _wipeOwnedKey(); return out; },
|
|
278
|
+
function (e) { _wipeOwnedKey(); throw e; });
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Everything from the key snapshot onward, so a single cleanup in the caller covers every
|
|
282
|
+
// exit -- sync throw and async rejection alike -- rather than each fallible step needing to
|
|
283
|
+
// remember. `responderKey` is the private copy; it is never the caller's object.
|
|
284
|
+
function _signResponse(responseData, responder, respCert, respCertDer, responderKey, opts) {
|
|
225
285
|
var responses = responseData.responses || [];
|
|
226
286
|
if (!responses.length) throw _err("ocsp/bad-input", "a response MUST include at least one SingleResponse (RFC 6960 sec. 4.2.1)");
|
|
227
287
|
|
|
@@ -234,7 +294,7 @@ function sign(responseData, responder, opts) {
|
|
|
234
294
|
if (respExts.length) rdChildren.push(b.explicit(1, b.sequence(respExts)));
|
|
235
295
|
var responseDataDer = b.sequence(rdChildren);
|
|
236
296
|
var scheme = signScheme.resolveSignScheme(respCert, { combinedRsaSig: true }, true, _signE);
|
|
237
|
-
return signScheme.signOverTbs(scheme,
|
|
297
|
+
return signScheme.signOverTbs(scheme, responderKey, responseDataDer, _signE).then(function (sig) {
|
|
238
298
|
var basicChildren = [responseDataDer, scheme.sigAlgId, b.bitString(sig, 0)];
|
|
239
299
|
if (opts.embedCert !== false) basicChildren.push(b.explicit(0, b.sequence([b.raw(respCertDer)])));
|
|
240
300
|
var responseBytes = b.sequence([b.oid(OID_OCSP_BASIC), b.octetString(b.sequence(basicChildren))]);
|
|
@@ -253,6 +313,36 @@ function _asDate(d) {
|
|
|
253
313
|
if (isNaN(dt.getTime())) throw _err("ocsp/bad-input", "an invalid date value " + JSON.stringify(d));
|
|
254
314
|
return dt;
|
|
255
315
|
}
|
|
316
|
+
// A private copy of whatever signer-key material the caller owns, taken at the entry
|
|
317
|
+
// point because signing happens several promise turns later (see `sign`). Bytes are
|
|
318
|
+
// copied; a composite { mldsa, trad } is rebuilt with each component copied, since
|
|
319
|
+
// rewriting a component is the same attack one level down. Anything else -- a CryptoKey,
|
|
320
|
+
// a PEM string -- is not caller-mutable in a way that changes the key, so it rides as-is
|
|
321
|
+
// rather than being coerced into a shape this function does not understand.
|
|
322
|
+
// `owned` collects every buffer THIS function allocated, so the caller can clear them once
|
|
323
|
+
// signing is done. A copy of a private key is a second copy of a secret, which is precisely
|
|
324
|
+
// what this project's secret-lifetime discipline exists to avoid -- so the copy that closes
|
|
325
|
+
// the aliasing window is wiped rather than left to the garbage collector. Only our own
|
|
326
|
+
// allocations are listed: the caller's key is never written to.
|
|
327
|
+
function _snapshotSignerKey(key, owned) {
|
|
328
|
+
if (Buffer.isBuffer(key) || key instanceof Uint8Array) {
|
|
329
|
+
var copy = guard.bytes.snapshot(key, OcspError, "ocsp/bad-input", "the responder key");
|
|
330
|
+
owned.push(copy);
|
|
331
|
+
return copy;
|
|
332
|
+
}
|
|
333
|
+
// The test is "is this a composite DESCRIPTOR", not "does it currently hold bytes". A
|
|
334
|
+
// composite whose components are both PEM strings carries nothing mutable inside it, but
|
|
335
|
+
// the OBJECT is still the caller's: reassigning `key.mldsa` after the call reaches the
|
|
336
|
+
// deferred signing operation just as rewriting a buffer would, and yields a response whose
|
|
337
|
+
// responder ID and embedded certificate describe one responder over a signature made by
|
|
338
|
+
// another key. So the container is always rebuilt, and each component is snapshotted by
|
|
339
|
+
// its own type -- bytes copied, an immutable PEM string passed through.
|
|
340
|
+
if (key && typeof key === "object" && (key.mldsa != null || key.trad != null)) {
|
|
341
|
+
return Object.assign({}, key, { mldsa: _snapshotSignerKey(key.mldsa, owned), trad: _snapshotSignerKey(key.trad, owned) });
|
|
342
|
+
}
|
|
343
|
+
return key;
|
|
344
|
+
}
|
|
345
|
+
|
|
256
346
|
function _responderID(rid, respCert) {
|
|
257
347
|
if (rid == null || rid === "byName") return Promise.resolve(b.explicit(1, b.raw(respCert.subject.bytes))); // byName [1] EXPLICIT Name
|
|
258
348
|
if (rid === "byKey") return _digest("SHA-1", _keyValue(respCert.subjectPublicKeyInfo.bytes)).then(function (kh) { return b.explicit(2, b.octetString(kh)); });
|
|
@@ -261,7 +351,10 @@ function _responderID(rid, respCert) {
|
|
|
261
351
|
function _buildSingleResponse(r, opts) {
|
|
262
352
|
r = r || {};
|
|
263
353
|
var certIdP;
|
|
264
|
-
|
|
354
|
+
// A pre-encoded CertID is spliced in RAW, so its type is checked rather than coerced:
|
|
355
|
+
// Buffer.from(20) would allocate twenty zero octets and emit a structurally broken
|
|
356
|
+
// CertID inside a response this responder then SIGNS.
|
|
357
|
+
if (r.certID != null) certIdP = Promise.resolve(b.raw(guard.bytes.view(r.certID, OcspError, "ocsp/bad-input", "a response entry's certID")));
|
|
265
358
|
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
359
|
else return Promise.reject(_err("ocsp/bad-input", "each response entry needs { certID } or { cert, issuer }"));
|
|
267
360
|
return certIdP.then(function (certID) {
|
|
@@ -292,7 +385,7 @@ function _certStatusNode(status, opts) {
|
|
|
292
385
|
* @primitive pki.ocsp.buildErrorResponse
|
|
293
386
|
* @signature pki.ocsp.buildErrorResponse(status) -> Buffer | string
|
|
294
387
|
* @since 0.2.22
|
|
295
|
-
* @status
|
|
388
|
+
* @status stable
|
|
296
389
|
* @spec RFC 6960
|
|
297
390
|
* @related pki.ocsp.sign
|
|
298
391
|
*
|
|
@@ -314,7 +407,7 @@ function buildErrorResponse(status) {
|
|
|
314
407
|
* @primitive pki.ocsp.verify
|
|
315
408
|
* @signature pki.ocsp.verify(response, opts) -> Promise<{ status, responderAuthorized, signatureValid, thisUpdate, nextUpdate, revocationReason?, nonceMatched?, reason }>
|
|
316
409
|
* @since 0.2.22
|
|
317
|
-
* @status
|
|
410
|
+
* @status stable
|
|
318
411
|
* @spec RFC 6960, RFC 9654, RFC 5019
|
|
319
412
|
* @related pki.path.ocspChecker, pki.ocsp.buildRequest
|
|
320
413
|
*
|
|
@@ -324,8 +417,16 @@ function buildErrorResponse(status) {
|
|
|
324
417
|
* to the target certificate under the CertID's own hashAlgorithm, checks currency
|
|
325
418
|
* (`thisUpdate`/`nextUpdate`), and -- when `opts.requestNonce` is supplied -- confirms the response
|
|
326
419
|
* nonce echoes it. This runs the SAME hardened gates `pki.path.ocspChecker` does. Fail-closed: an
|
|
327
|
-
* unauthorized, stale,
|
|
328
|
-
*
|
|
420
|
+
* unauthorized, stale, or CertID-mismatched response is a `"unknown"` verdict (never a silent
|
|
421
|
+
* accept); a malformed response's parse fault surfaces as the parser's `ocsp/*` / `asn1/*`.
|
|
422
|
+
*
|
|
423
|
+
* The request-nonce check is reported, and downgrades `good` ONLY. Every verdict carries
|
|
424
|
+
* `nonceMatched` (true / false / null when the client sent no nonce). An unmatched nonce turns a
|
|
425
|
+
* `good` into `"unknown"`, because a response that is not an answer to this request cannot be relied
|
|
426
|
+
* on to say the certificate is still fine. It does NOT touch `revoked`: revocation does not go stale
|
|
427
|
+
* the way non-revocation does, so discarding a signed, current, authorized `revoked` because it was
|
|
428
|
+
* replayed would hand a soft-failing caller the very certificate the responder refused. A replayed
|
|
429
|
+
* `revoked` is therefore reported as `revoked` with `nonceMatched: false`.
|
|
329
430
|
*
|
|
330
431
|
* @opts
|
|
331
432
|
* cert the target certificate (parsed, DER, or PEM) -- REQUIRED.
|
|
@@ -334,6 +435,19 @@ function buildErrorResponse(status) {
|
|
|
334
435
|
* requestNonce the nonce the client sent; when given, the response MUST echo it (constant-time).
|
|
335
436
|
* historicalMode defer a strictly-future revocation (report good) instead of revoking on skew.
|
|
336
437
|
* @example
|
|
438
|
+
* var ca = await pki.key.generate("Ed25519");
|
|
439
|
+
* var caKey = await pki.key.export(ca.privateKey);
|
|
440
|
+
* var caDer = await pki.x509.sign({ subject: "Example CA", subjectPublicKey: await pki.key.export(ca.publicKey),
|
|
441
|
+
* notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
|
|
442
|
+
* extensions: { basicConstraints: { cA: true }, keyUsage: ["keyCertSign"], subjectKeyIdentifier: true } },
|
|
443
|
+
* { key: caKey });
|
|
444
|
+
* var leaf = await pki.key.generate("Ed25519");
|
|
445
|
+
* var leafDer = await pki.x509.sign({ subject: "leaf.example", subjectPublicKey: await pki.key.export(leaf.publicKey),
|
|
446
|
+
* notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z") },
|
|
447
|
+
* { cert: caDer, key: caKey });
|
|
448
|
+
* var responseDer = await pki.ocsp.sign(
|
|
449
|
+
* { responderID: "byName", responses: [{ cert: leafDer, issuer: caDer, status: "good" }] },
|
|
450
|
+
* { cert: caDer, key: caKey });
|
|
337
451
|
* var res = await pki.ocsp.verify(responseDer, { cert: leafDer, issuer: caDer });
|
|
338
452
|
* res.status; // "good" | "revoked" | "unknown"
|
|
339
453
|
*/
|
|
@@ -350,14 +464,28 @@ function verify(response, opts) {
|
|
|
350
464
|
time = opts.time == null ? new Date() : _asDate(opts.time);
|
|
351
465
|
} catch (e) { return Promise.reject(e); }
|
|
352
466
|
return pathValidate.verifyOcspResponse(parsed, cert, issuerCert, time, { historicalMode: opts.historicalMode === true }).then(function (verdict) {
|
|
353
|
-
|
|
467
|
+
// A client that sent no nonce still gets the field, as null. Leaving it absent would make
|
|
468
|
+
// "not requested" indistinguishable from "the field is not there yet" for a consumer reading
|
|
469
|
+
// res.nonceMatched, and the three-state contract is the whole point: true bound, false not
|
|
470
|
+
// bound, null never asked. The verdict is copied rather than mutated, since it is the lower
|
|
471
|
+
// primitive's object and this layer does not own it.
|
|
472
|
+
if (opts.requestNonce == null) return Object.assign({}, verdict, { nonceMatched: null });
|
|
354
473
|
// A client that sent a nonce binds it (RFC 9654 / RFC 5019 sec. 4): a missing or mismatched
|
|
355
474
|
// response nonce fails the verdict closed, even if the status/signature were otherwise good.
|
|
356
475
|
var respNonce = _responseNonce(parsed);
|
|
357
476
|
var reqNonce = Buffer.isBuffer(opts.requestNonce) ? opts.requestNonce : (opts.requestNonce instanceof Uint8Array ? Buffer.from(opts.requestNonce) : null);
|
|
358
477
|
var matched = respNonce != null && reqNonce != null && guard.crypto.constantTimeEqual(respNonce, reqNonce);
|
|
359
478
|
var out = Object.assign({}, verdict, { nonceMatched: matched });
|
|
360
|
-
|
|
479
|
+
// The downgrade applies to `good` ONLY. `unknown` is the closed direction for a
|
|
480
|
+
// response claiming the certificate is fine, because an unmatched nonce means
|
|
481
|
+
// this is not an answer to this request and the "fine" may be stale. It is NOT
|
|
482
|
+
// the closed direction for `revoked`: revocation does not expire the way
|
|
483
|
+
// non-revocation does, so discarding a signed, current, authorized revoked
|
|
484
|
+
// verdict because it was replayed would hand a soft-fail caller the certificate
|
|
485
|
+
// the responder just refused -- turning the anti-replay defence into the thing
|
|
486
|
+
// that accepts a revoked certificate. `nonceMatched: false` still reports that
|
|
487
|
+
// this response was not bound to this request.
|
|
488
|
+
if (!matched && verdict.status === "good") {
|
|
361
489
|
return Object.assign(out, { status: "unknown", reason: "the OCSP response nonce does not echo the request nonce (RFC 9654)" });
|
|
362
490
|
}
|
|
363
491
|
return out;
|