@blamejs/pki 0.5.6 → 0.5.8
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 +419 -378
- package/MIGRATING.md +65 -0
- package/README.md +12 -12
- package/lib/acme.js +31 -31
- package/lib/asn1-der.js +10 -10
- package/lib/attrcert-sign.js +24 -20
- package/lib/byte-reader.js +6 -6
- package/lib/byte-writer.js +5 -5
- package/lib/cbor-det.js +27 -24
- package/lib/cmc-build.js +104 -104
- package/lib/cmc-verify.js +108 -32
- package/lib/cmp-build.js +31 -26
- package/lib/cmp-session.js +74 -72
- package/lib/cmp-verify.js +72 -58
- package/lib/cms-compress.js +8 -9
- package/lib/cms-decrypt.js +92 -76
- package/lib/cms-encrypt.js +33 -34
- package/lib/cms-sign.js +100 -54
- package/lib/cms-verify.js +141 -82
- package/lib/composite-sig.js +13 -13
- package/lib/constants.js +4 -4
- package/lib/crl-sign.js +31 -25
- package/lib/crl-verify.js +7 -6
- package/lib/crmf-sign.js +19 -15
- package/lib/csr-sign.js +13 -9
- package/lib/ct.js +37 -37
- package/lib/edwards-point.js +7 -7
- package/lib/est.js +103 -59
- package/lib/framework-error.js +5 -5
- package/lib/guard-all.js +3 -3
- package/lib/guard-async.js +4 -4
- package/lib/guard-bytes.js +378 -15
- package/lib/guard-compress.js +17 -17
- package/lib/guard-crypto.js +1 -1
- package/lib/guard-encoding.js +15 -15
- package/lib/guard-header.js +3 -3
- package/lib/guard-identifier.js +16 -16
- package/lib/guard-json.js +15 -15
- package/lib/guard-limits.js +7 -7
- package/lib/guard-name.js +81 -16
- package/lib/guard-parsed.js +144 -75
- package/lib/guard-range.js +19 -19
- package/lib/guard-secret.js +11 -10
- package/lib/guard-text.js +6 -6
- package/lib/guard-time.js +10 -10
- package/lib/hpke.js +18 -17
- package/lib/http-digest.js +35 -35
- package/lib/http-retry-after.js +13 -13
- package/lib/http-transport.js +20 -19
- package/lib/inspect.js +53 -53
- package/lib/ip-utils.js +2 -2
- package/lib/jose.js +13 -13
- package/lib/key.js +16 -16
- package/lib/lint.js +51 -51
- package/lib/merkle.js +51 -36
- package/lib/mime.js +18 -18
- package/lib/ocsp-verify.js +10 -10
- package/lib/ocsp.js +32 -20
- package/lib/oid.js +29 -29
- package/lib/path-validate.js +114 -113
- package/lib/pbes2.js +16 -16
- package/lib/pkcs12-build.js +71 -56
- package/lib/pki-build.js +23 -22
- package/lib/rc2.js +1 -1
- package/lib/rfc3339.js +5 -5
- package/lib/schema-all.js +31 -31
- package/lib/schema-attrcert.js +12 -12
- package/lib/schema-c509.js +144 -142
- package/lib/schema-cmc.js +58 -58
- package/lib/schema-cmp.js +43 -43
- package/lib/schema-cms.js +169 -36
- package/lib/schema-crl.js +7 -7
- package/lib/schema-crmf.js +28 -28
- package/lib/schema-csr.js +12 -12
- package/lib/schema-csrattrs.js +16 -16
- package/lib/schema-engine.js +18 -18
- package/lib/schema-ocsp.js +15 -15
- package/lib/schema-pkcs12.js +20 -20
- package/lib/schema-pkcs8.js +2 -2
- package/lib/schema-pkix.js +131 -126
- package/lib/schema-smime.js +19 -19
- package/lib/schema-tsp.js +12 -12
- package/lib/schema-x509.js +3 -3
- package/lib/shbs.js +18 -18
- package/lib/sign-scheme.js +19 -16
- package/lib/sigstore.js +10 -11
- package/lib/sleep.js +1 -1
- package/lib/smime.js +308 -96
- package/lib/tls-cert-compress.js +18 -18
- package/lib/trust.js +27 -27
- package/lib/tsp-sign.js +22 -18
- package/lib/validator-all.js +1 -1
- package/lib/validator-attcert.js +1 -1
- package/lib/validator-cose.js +43 -44
- package/lib/validator-keydesc.js +3 -3
- package/lib/validator-sig.js +13 -13
- package/lib/validator-tls.js +11 -11
- package/lib/validator-tpm.js +21 -20
- package/lib/webauthn-mds.js +67 -67
- package/lib/webauthn.js +34 -34
- package/lib/webcrypto.js +15 -15
- package/lib/x509-sign.js +24 -15
- package/package.json +3 -2
- package/sbom.cdx.json +6 -6
package/lib/guard-time.js
CHANGED
|
@@ -10,19 +10,19 @@
|
|
|
10
10
|
//
|
|
11
11
|
// Defends the NaN-Date fail-open class (CWE-20 improper input validation feeding
|
|
12
12
|
// a silent security-gate bypass). An Invalid Date is still `instanceof Date`, and
|
|
13
|
-
//
|
|
14
|
-
// false
|
|
15
|
-
// unvalidated Date silently
|
|
13
|
+
// every relational comparison against its NaN getTime() (NaN < x, NaN >= x) is
|
|
14
|
+
// false, so a validity / not-before / not-after / currency window built on an
|
|
15
|
+
// unvalidated Date silently accepts when it should reject. The class recurred at
|
|
16
16
|
// three independent boundaries (TSP genTime, OCSP producedAt/thisUpdate, CT
|
|
17
17
|
// temporal-interval) before it was centralized here; the sibling
|
|
18
18
|
// nan-date-comparison-unguarded codebase-patterns detector flags any lib function
|
|
19
|
-
// that re-inlines a `.getTime()` comparison
|
|
20
|
-
// new boundary is routed to this guard
|
|
19
|
+
// that re-inlines a `.getTime()` comparison without first rejecting a NaN, so a
|
|
20
|
+
// new boundary is routed to this guard instead of re-growing the bug.
|
|
21
21
|
//
|
|
22
|
-
// Tier split: `value instanceof Date` with a NaN time is malformed
|
|
23
|
-
// (a config-time
|
|
24
|
-
// caller `opts.time`), so assertValid throws the caller's typed error
|
|
25
|
-
// returning a silent default. within() is the fail-closed window primitive: it
|
|
22
|
+
// Tier split: `value instanceof Date` with a NaN time is malformed caller input
|
|
23
|
+
// (a config-time or entry-point Date, such as an operator-supplied distrustAfter
|
|
24
|
+
// or a caller `opts.time`), so assertValid throws the caller's typed error instead
|
|
25
|
+
// of returning a silent default. within() is the fail-closed window primitive: it
|
|
26
26
|
// THROWS on a malformed operand but RETURNS a boolean for in/out-of-window, so the
|
|
27
27
|
// OCSP / CRL currency callers that treat out-of-window as a `continue` skip keep
|
|
28
28
|
// their control flow while a NaN operand can never slip through as a false.
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
// assertValid(value, E, code, label) -> the same Date, once proven valid.
|
|
31
31
|
// value : a Date (or an alleged one) from a caller boundary.
|
|
32
32
|
// E : the (code, message[, cause]) typed-error FACTORY in scope at the call
|
|
33
|
-
// site (ns.E / the module-local _err)
|
|
33
|
+
// site (ns.E / the module-local _err), never a defineClass class, which
|
|
34
34
|
// would crash `class cannot be invoked without new` on the error path.
|
|
35
35
|
// code : the frozen domain/reason code this boundary rejects malformed time under.
|
|
36
36
|
// label : field phrase for the message.
|
package/lib/hpke.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* Appendix A known-answer vectors. DHKEM(P-384) and HKDF-SHA384 are RFC-registered
|
|
15
15
|
* but Appendix A ships no test vector for them, so they are omitted (a request
|
|
16
16
|
* fails closed) until an authoritative KAT is available. Pure composition over
|
|
17
|
-
* node:crypto
|
|
17
|
+
* node:crypto: no ASN.1, no schema engine. Post-quantum KEMs (ML-KEM, X-Wing)
|
|
18
18
|
* are a data-row extension the registry is shaped to admit once their
|
|
19
19
|
* specifications stabilize.
|
|
20
20
|
* The RFC 9180 sec. 7 registry code points live in pki.hpke.suites
|
|
@@ -44,14 +44,15 @@ function xor(a, b) { var o = Buffer.alloc(a.length); for (var i = 0; i < a.lengt
|
|
|
44
44
|
var HPKE_V1 = Buffer.from("HPKE-v1", "ascii");
|
|
45
45
|
function L(s) { return Buffer.from(s, "ascii"); }
|
|
46
46
|
|
|
47
|
-
// ---- registries
|
|
47
|
+
// ---- registries: the RFC 9180 sec. 7 data tables (Tables 2/3/5) --------------
|
|
48
48
|
|
|
49
49
|
// KDF: id -> { hash (node name), Nh }.
|
|
50
50
|
// HKDF-SHA384 (0x0002) and DHKEM(P-384) (0x0011) are registered by RFC 9180 sec.
|
|
51
51
|
// 7 but Appendix A / the cited [TestVectors] file ship no known-answer vector for
|
|
52
|
-
// them, so they are omitted here until an authoritative KAT exists
|
|
53
|
-
//
|
|
54
|
-
//
|
|
52
|
+
// them, so they are omitted here until an authoritative KAT exists. A request for
|
|
53
|
+
// either fails closed as hpke/unknown-suite, because no test vector proves the
|
|
54
|
+
// crypto it would otherwise run. They admit as a one-row addition the moment a
|
|
55
|
+
// KAT lands.
|
|
55
56
|
var KDFS = {
|
|
56
57
|
0x0001: { hash: "sha256", Nh: 32 },
|
|
57
58
|
0x0003: { hash: "sha512", Nh: 64 },
|
|
@@ -112,7 +113,7 @@ function _expand(hash, Nh, prk, info, len) {
|
|
|
112
113
|
return exact;
|
|
113
114
|
}
|
|
114
115
|
function _labeledExtract(kdf, suiteId, salt, label, ikm) {
|
|
115
|
-
// The concatenation copies the input keying material
|
|
116
|
+
// The concatenation copies the input keying material (a DH secret or a PSK) into a fresh
|
|
116
117
|
// buffer, so that copy is cleared once extract has consumed it. The caller still owns .
|
|
117
118
|
var labeled = concat([HPKE_V1, suiteId, L(label), ikm]);
|
|
118
119
|
try { return _extract(kdf.hash, kdf.Nh, salt, labeled); }
|
|
@@ -174,7 +175,7 @@ function _importPrivate(kem, rawSk, rawPk) {
|
|
|
174
175
|
// The single KEM Diffie-Hellman choke point. node:crypto throws a raw
|
|
175
176
|
// ERR_OSSL_FAILED_DURING_DERIVATION when the peer point is low-order / invalid
|
|
176
177
|
// (an all-zero X25519 shared secret, a point not on the curve). RFC 9180 sec.
|
|
177
|
-
// 4.1: Decap raises an error on DH failure
|
|
178
|
+
// 4.1: Decap raises an error on DH failure. Surface it as a typed hpke/bad-key
|
|
178
179
|
// so no raw OpenSSL error escapes any encap / decap path.
|
|
179
180
|
function _dh(privateKey, publicKey) {
|
|
180
181
|
try {
|
|
@@ -211,9 +212,9 @@ function _ephemeral(kem, eph) {
|
|
|
211
212
|
// The raw DH output is the KEM's shared secret before extraction: it is this module's allocation
|
|
212
213
|
// and is cleared once _extractAndExpand has consumed it, on every DHKEM arm. In the authenticated
|
|
213
214
|
// arms builds a THIRD copy holding both DH results, so the concatenation is cleared as
|
|
214
|
-
// well
|
|
215
|
+
// well, since clearing only the pieces would leave the joined buffer readable.
|
|
215
216
|
function _wipeDh(dh) { guard.secret.zeroize(dh, HpkeError, "hpke/bad-input", "the KEM shared secret"); }
|
|
216
|
-
//
|
|
217
|
+
// Every agreement happens inside the protected region. In the authenticated modes the second
|
|
217
218
|
// agreement can throw on an attacker-supplied low-order sender key, and a first secret computed
|
|
218
219
|
// outside the region would be left behind on exactly that failure path.
|
|
219
220
|
function _encap(kem, pkR, pkRm, eph) {
|
|
@@ -361,7 +362,7 @@ function _recipPublic(suite, pk) {
|
|
|
361
362
|
if (Buffer.isBuffer(pk)) return { key: _importPublic(suite.kem, pk), pkm: pk };
|
|
362
363
|
if (pk && pk.pkm) return { key: _importPublic(suite.kem, pk.pkm), pkm: pk.pkm };
|
|
363
364
|
// A node KeyObject: derive its wire form. A null/undefined/invalid value must
|
|
364
|
-
// fail closed here
|
|
365
|
+
// fail closed here, so no raw error from node's export escapes upward.
|
|
365
366
|
try {
|
|
366
367
|
return { key: pk, pkm: _exportPublic(suite.kem, pk) };
|
|
367
368
|
} catch (e) {
|
|
@@ -373,8 +374,8 @@ function _recipPrivate(suite, sk) {
|
|
|
373
374
|
if (sk && sk.skm) return { key: _importPrivate(suite.kem, sk.skm, sk.pkm), pkm: sk.pkm };
|
|
374
375
|
// A raw scalar alone cannot be imported (an EC private key needs its public
|
|
375
376
|
// point for the JWK), so the serialized private form is { skm, pkm }; reject a
|
|
376
|
-
// bare buffer, and fail any other bad key closed
|
|
377
|
-
//
|
|
377
|
+
// bare buffer, and fail any other bad key closed; a raw createPublicKey error
|
|
378
|
+
// must not escape the setup path.
|
|
378
379
|
if (Buffer.isBuffer(sk)) throw _err("hpke/bad-key", "a serialized private key must be provided as { skm, pkm } (raw scalar + public point), not a bare buffer");
|
|
379
380
|
try {
|
|
380
381
|
var pkm = _exportPublic(suite.kem, nodeCrypto.createPublicKey(sk));
|
|
@@ -389,9 +390,9 @@ function _recipPrivate(suite, sk) {
|
|
|
389
390
|
// psk / auth / auth-psk. An unknown mode must fail closed, never key-schedule
|
|
390
391
|
// with an out-of-registry mode byte.
|
|
391
392
|
// The sender and the recipient read the same option object from OPPOSITE ends, so each direction
|
|
392
|
-
// gets its own table
|
|
393
|
-
//
|
|
394
|
-
// `senderKey` handed to setupR, is silently ignored
|
|
393
|
+
// gets its own table. A shared union recognises every name at both ends and so accepts the one that
|
|
394
|
+
// cannot do anything there: `senderPublicKey` handed to setupS, or
|
|
395
|
+
// `senderKey` handed to setupR, is silently ignored, which is the exact silence these tables exist
|
|
395
396
|
// to remove, in a wider form. An option that means nothing where it was passed is a misunderstanding
|
|
396
397
|
// worth reporting, and usually a misdirected auth-mode setup.
|
|
397
398
|
//
|
|
@@ -491,8 +492,8 @@ function _setupS(ids, pkR, opts) {
|
|
|
491
492
|
* @related pki.hpke.setupS, pki.hpke.open
|
|
492
493
|
*
|
|
493
494
|
* Establish the recipient HPKE context from the sender's encapsulated key `enc`
|
|
494
|
-
* and the recipient KEM private key (a node KeyObject or `{ skm, pkm }`
|
|
495
|
-
* raw private scalar plus its public point),
|
|
495
|
+
* and the recipient KEM private key (a node KeyObject, or `{ skm, pkm }` holding
|
|
496
|
+
* the raw private scalar plus its public point),
|
|
496
497
|
* recovering the same shared secret and key schedule. The returned `context`
|
|
497
498
|
* `.open(aad, ct)` decrypts and `.export(ctx, L)` derives secrets. `opts` mirrors
|
|
498
499
|
* `setupS` (mode / info / psk / pskId), with `opts.senderPublicKey` for the auth
|
package/lib/http-digest.js
CHANGED
|
@@ -2,20 +2,20 @@
|
|
|
2
2
|
// Copyright (c) blamejs contributors
|
|
3
3
|
"use strict";
|
|
4
4
|
//
|
|
5
|
-
// @internal --
|
|
5
|
+
// @internal -- not on pki.*. HTTP Digest access authentication (RFC 7616): parse an untrusted
|
|
6
6
|
// WWW-Authenticate challenge (fail-closed, quoted-string-honoring) and compute the Authorization
|
|
7
7
|
// response header (byte-exact A1/A2/KD). Prefix-agnostic: every thrown code comes from the caller's
|
|
8
|
-
// E(code, msg)
|
|
8
|
+
// E(code, msg) factory, never a defineClass class (which would crash on the error path), plus its
|
|
9
9
|
// policy.codes map, so pki.est composes it now and pki.acme / pki.cmp can reuse it as config, not a fork.
|
|
10
10
|
//
|
|
11
|
-
// The challenge is attacker-shaped: a comma inside a quoted value is
|
|
12
|
-
// substring-scan bug class), a missing realm
|
|
13
|
-
// algorithm or a no-qop challenge fails closed
|
|
11
|
+
// The challenge is attacker-shaped: a comma inside a quoted value is not a delimiter (the _hasBasicChallenge
|
|
12
|
+
// substring-scan bug class), a missing realm or nonce is rejected and not defaulted, and an unsupported or
|
|
13
|
+
// weak algorithm, or a no-qop challenge, fails closed instead of downgrading.
|
|
14
14
|
|
|
15
15
|
var crypto = require("crypto");
|
|
16
16
|
var constants = require("./constants");
|
|
17
17
|
|
|
18
|
-
// Algorithm registry
|
|
18
|
+
// Algorithm registry, a data row and not a switch: rank orders the RFC 7616 sec. 3.7
|
|
19
19
|
// "most secure the client can use" selection (SHA-512-256 > SHA-256 > MD5); an unknown algorithm ranks 0.
|
|
20
20
|
var ALGS = {
|
|
21
21
|
"SHA-512-256": { hash: "sha512-256", rank: 3, sess: false }, "SHA-512-256-SESS": { hash: "sha512-256", rank: 3, sess: true },
|
|
@@ -33,7 +33,7 @@ function H(hash, s) { return crypto.createHash(hash).update(s, "latin1").digest(
|
|
|
33
33
|
function KD(hash, secret, data) { return H(hash, secret + ":" + data); }
|
|
34
34
|
function _qstr(v) { return "\"" + String(v).replace(/(["\\])/g, "\\$1") + "\""; }
|
|
35
35
|
|
|
36
|
-
// Split a header at commas
|
|
36
|
+
// Split a header at commas outside a quoted-string (a quoted comma is a literal, not a delimiter).
|
|
37
37
|
function _commaSplitOutsideQuotes(s) {
|
|
38
38
|
var out = [], buf = "", inQ = false, esc = false;
|
|
39
39
|
for (var i = 0; i < s.length; i++) {
|
|
@@ -47,7 +47,7 @@ function _commaSplitOutsideQuotes(s) {
|
|
|
47
47
|
out.push(buf);
|
|
48
48
|
return out;
|
|
49
49
|
}
|
|
50
|
-
// Index of the first '='
|
|
50
|
+
// Index of the first '=' outside a quoted-string, or -1.
|
|
51
51
|
function _firstEqOutsideQuotes(s) {
|
|
52
52
|
var inQ = false, esc = false;
|
|
53
53
|
for (var i = 0; i < s.length; i++) {
|
|
@@ -60,7 +60,7 @@ function _firstEqOutsideQuotes(s) {
|
|
|
60
60
|
return -1;
|
|
61
61
|
}
|
|
62
62
|
function _unq(s) { return s.slice(1, -1).replace(/\\(.)/g, "$1"); } // only ever called on a well-formed quoted-string (see _closedQuote)
|
|
63
|
-
// A well-formed quoted-string: opens with ", closes with an
|
|
63
|
+
// A well-formed quoted-string: opens with ", closes with an unescaped " as the last character (nothing trails
|
|
64
64
|
// it). An unterminated / trailing-garbage quoted-string is malformed (RFC 7230 sec. 3.2.6) and rejected.
|
|
65
65
|
function _closedQuote(s) {
|
|
66
66
|
if (s.length < 2 || s.charAt(0) !== "\"") return false;
|
|
@@ -74,7 +74,7 @@ function _closedQuote(s) {
|
|
|
74
74
|
return false;
|
|
75
75
|
}
|
|
76
76
|
// A control octet (RFC 7230 sec. 3.2.6 quoted-string / qdtext forbids CTL except HTAB). Scanned with charCodeAt
|
|
77
|
-
//
|
|
77
|
+
// instead of a control-char regex literal (which eslint no-control-regex correctly refuses).
|
|
78
78
|
function _hasCtl(s) {
|
|
79
79
|
for (var i = 0; i < s.length; i++) { var c = s.charCodeAt(i); if ((c < 0x20 && c !== 0x09) || c === 0x7f) return true; }
|
|
80
80
|
return false;
|
|
@@ -119,7 +119,7 @@ function _parseParams(paramText, E, code) {
|
|
|
119
119
|
}
|
|
120
120
|
return map;
|
|
121
121
|
}
|
|
122
|
-
// Structural validation of
|
|
122
|
+
// Structural validation of one Digest challenge's params (RFC 7616 sec. 3.3). Throws on any violation.
|
|
123
123
|
function _validateDigest(paramText, E, code) {
|
|
124
124
|
var p = _parseParams(paramText, E, code);
|
|
125
125
|
if (!p.realm || !p.realm.quoted || p.realm.value === "") throw E(code, "a Digest challenge requires a non-empty quoted realm (RFC 7616 sec. 3.3)");
|
|
@@ -129,38 +129,38 @@ function _validateDigest(paramText, E, code) {
|
|
|
129
129
|
if (p.qop) {
|
|
130
130
|
if (!p.qop.quoted) throw E(code, "the Digest qop must be a quoted list (RFC 7616 sec. 3.3)");
|
|
131
131
|
qop = p.qop.value.split(",").map(function (x) { return x.replace(/^\s+|\s+$/g, "").toLowerCase(); }).filter(Boolean);
|
|
132
|
-
// A qop directive that is
|
|
133
|
-
// absent-qop (RFC 2069) case. Reject it
|
|
134
|
-
// cannot accept (RFC 7616 sec. 3.3). Only an
|
|
132
|
+
// A qop directive that is present but lists no value (qop="" / qop=", ,") is malformed, and is not the
|
|
133
|
+
// absent-qop (RFC 2069) case. Reject it instead of silently collapsing to a no-qop response the server
|
|
134
|
+
// cannot accept (RFC 7616 sec. 3.3). Only an omitted qop directive selects the legacy no-qop path.
|
|
135
135
|
if (qop.length === 0) throw E(code, "a present Digest qop directive must list at least one value (RFC 7616 sec. 3.3)");
|
|
136
136
|
}
|
|
137
|
-
// domain (RFC 7616 sec. 3.3): a
|
|
138
|
-
//
|
|
139
|
-
// widened
|
|
140
|
-
// server intended. Surfaced as an array of the listed URIs, or null when omitted
|
|
137
|
+
// domain (RFC 7616 sec. 3.3): a quoted, space-separated list of URIs defining the protection space. An
|
|
138
|
+
// unquoted domain is malformed and rejected (fail closed like realm / nonce / qop) instead of silently
|
|
139
|
+
// widened: widening a mis-encoded scope to "the whole server" would send credentials more broadly than the
|
|
140
|
+
// server intended. Surfaced as an array of the listed URIs, or null when omitted or quoted-empty, which per
|
|
141
141
|
// the RFC means "all URIs on the responding server" (the answer may be reused for any same-origin URI, sec. 3.5).
|
|
142
142
|
if (p.domain && !p.domain.quoted) throw E(code, "the Digest domain must be a quoted-string (RFC 7616 sec. 3.3)");
|
|
143
143
|
var domain = (p.domain && p.domain.value.replace(/^\s+|\s+$/g, "") !== "")
|
|
144
144
|
? p.domain.value.replace(/^\s+|\s+$/g, "").split(/\s+/) : null;
|
|
145
|
-
// charset (RFC 7616 sec. 3.3): the
|
|
146
|
-
// other value, is malformed
|
|
145
|
+
// charset (RFC 7616 sec. 3.3): the one permitted value is the unquoted token "UTF-8". A quoted charset, or any
|
|
146
|
+
// other value, is malformed, since answering it would hash the credentials in the wrong encoding, so it is
|
|
147
147
|
// rejected (and thus skipped during multi-offer selection in favour of a conforming offer).
|
|
148
148
|
if (p.charset && (p.charset.quoted || String(p.charset.value).toUpperCase() !== "UTF-8")) throw E(code, "the Digest charset must be the unquoted token UTF-8 (RFC 7616 sec. 3.3)");
|
|
149
149
|
// stale (RFC 7616 sec. 3.3) is the unquoted token "true" or "false". A quoted or otherwise invalid value
|
|
150
|
-
// (stale=maybe) is malformed and rejected (thus skipped in selection)
|
|
150
|
+
// (stale=maybe) is malformed and rejected (thus skipped in selection), never parsed as a false flag that
|
|
151
151
|
// could shadow a conforming offer.
|
|
152
152
|
if (p.stale) {
|
|
153
153
|
var sv = String(p.stale.value).toLowerCase();
|
|
154
154
|
if (p.stale.quoted || (sv !== "true" && sv !== "false")) throw E(code, "the Digest stale directive must be the unquoted token true or false (RFC 7616 sec. 3.3)");
|
|
155
155
|
}
|
|
156
156
|
// userhash (RFC 7616 sec. 3.3) is likewise the unquoted token "true" or "false". A quoted or otherwise invalid
|
|
157
|
-
// value is malformed and rejected (skipped in selection)
|
|
157
|
+
// value is malformed and rejected (skipped in selection), never silently changing how the username is sent.
|
|
158
158
|
if (p.userhash) {
|
|
159
159
|
var uhv = String(p.userhash.value).toLowerCase();
|
|
160
160
|
if (p.userhash.quoted || (uhv !== "true" && uhv !== "false")) throw E(code, "the Digest userhash directive must be the unquoted token true or false (RFC 7616 sec. 3.3)");
|
|
161
161
|
}
|
|
162
|
-
// opaque (RFC 7616 sec. 3.3) is a quoted-string echoed back verbatim. An
|
|
163
|
-
// rejected (skipped in selection)
|
|
162
|
+
// opaque (RFC 7616 sec. 3.3) is a quoted-string echoed back verbatim. An unquoted opaque is malformed and
|
|
163
|
+
// rejected (skipped in selection), never accepted, where it could shadow a conforming offer.
|
|
164
164
|
if (p.opaque && !p.opaque.quoted) throw E(code, "the Digest opaque directive must be a quoted-string (RFC 7616 sec. 3.3)");
|
|
165
165
|
return {
|
|
166
166
|
scheme: "Digest", realm: p.realm.value, nonce: p.nonce.value, qop: qop, domain: domain,
|
|
@@ -177,9 +177,9 @@ function _validateDigest(paramText, E, code) {
|
|
|
177
177
|
// Selection is policy-aware: a challenge answer() would refuse under the active policy (an unsupported /
|
|
178
178
|
// MD5-when-disallowed algorithm, or a qop this client cannot satisfy) is ranked BELOW every usable one, so a
|
|
179
179
|
// higher-algorithm-rank but unusable offer (e.g. a SHA-512-256 no-qop challenge under the default policy)
|
|
180
|
-
// never shadows a lower-ranked usable one (e.g. SHA-256 with qop="auth")
|
|
180
|
+
// never shadows a lower-ranked usable one (e.g. SHA-256 with qop="auth"), per RFC 7616 sec. 3.3. When no
|
|
181
181
|
// challenge is usable the highest-algorithm-rank one is still returned, so answer() reports the specific
|
|
182
|
-
// policy reason (which opt to set)
|
|
182
|
+
// policy reason (which opt to set) in place of a generic "no challenge".
|
|
183
183
|
function parseChallenge(www, E, code, policy) {
|
|
184
184
|
var pol = policy || {};
|
|
185
185
|
var codes = pol.codes || DEFAULT_CODES;
|
|
@@ -210,9 +210,9 @@ function parseChallenge(www, E, code, policy) {
|
|
|
210
210
|
// so it must not shadow a weaker offer that does apply. Only checked when the caller supplies the request
|
|
211
211
|
// context; otherwise every offer is treated as applicable.
|
|
212
212
|
var applicable = pol.requestTarget === undefined ? true : inProtectionSpace(parsed, pol.requestOrigin, pol.requestTarget);
|
|
213
|
-
// A
|
|
214
|
-
// for the
|
|
215
|
-
// A stale offer for a
|
|
213
|
+
// A retryable offer (only meaningful in the rejection context) is stale=true, carries a fresh nonce, and is
|
|
214
|
+
// for the same realm whose credential was just rejected: the conditions under which the caller re-answers.
|
|
215
|
+
// A stale offer for a different realm is a new protection space (not a retry), and a stale offer repeating
|
|
216
216
|
// the prior nonce cannot re-answer, so neither must out-rank a weaker genuinely retryable offer (RFC 7616 sec. 3.3).
|
|
217
217
|
var retryable = preferStale && !!parsed.stale && parsed.nonce !== pol.priorNonce && parsed.realm === pol.priorRealm;
|
|
218
218
|
// Usability dominates; then an offer that APPLIES to this request; then (in a rejection) a retryable offer;
|
|
@@ -296,7 +296,7 @@ function answer(challenge, params, E) {
|
|
|
296
296
|
var realm = challenge.realm, nonce = challenge.nonce;
|
|
297
297
|
// Normalize UTF-8 credentials to NFC before hashing (RFC 7616 sec. 4): a server hashes the normalized form,
|
|
298
298
|
// so a canonically-decomposed username / password (e + combining accent vs the precomposed char) must be
|
|
299
|
-
// composed first or HA1 disagrees.
|
|
299
|
+
// composed first or HA1 disagrees. That one normalized username feeds A1, userhash, and username*.
|
|
300
300
|
var isUtf8 = String(challenge.charset || "").toUpperCase() === "UTF-8";
|
|
301
301
|
var pUser = params.username == null ? "" : String(params.username);
|
|
302
302
|
var pPass = params.password == null ? "" : String(params.password);
|
|
@@ -317,8 +317,8 @@ function answer(challenge, params, E) {
|
|
|
317
317
|
var response = useQop
|
|
318
318
|
? KD(alg.hash, HA1, nonce + ":" + nc + ":" + cnonce + ":" + useQop + ":" + HA2)
|
|
319
319
|
: KD(alg.hash, HA1, nonce + ":" + HA2);
|
|
320
|
-
// The username field (RFC 7616 sec. 3.4). userhash=true sends H(username:realm) for privacy (sec. 3.4.4)
|
|
321
|
-
// an ASCII hex value; A1 above still used the
|
|
320
|
+
// The username field (RFC 7616 sec. 3.4). userhash=true sends H(username:realm) for privacy (sec. 3.4.4),
|
|
321
|
+
// an ASCII hex value; A1 above still used the actual username. Otherwise a charset=UTF-8 username containing
|
|
322
322
|
// non-ASCII characters MUST be carried in the extended `username*` form (RFC 5987 / RFC 8187 percent-encoded
|
|
323
323
|
// UTF-8), because a server reads the legacy quoted `username` as ISO-8859-1 and could not resolve the account;
|
|
324
324
|
// `username` and `username*` MUST NOT both appear. An ASCII (or userhash) username uses the legacy quoted form.
|
|
@@ -341,9 +341,9 @@ function answer(challenge, params, E) {
|
|
|
341
341
|
}
|
|
342
342
|
|
|
343
343
|
// Parse one domain URI entry (RFC 7616 sec. 3.3) into { origin, full }: an abs_path ("/a", "/a?x=1") is
|
|
344
|
-
// relative to the responding server -> origin null (any same-origin request); an
|
|
345
|
-
// authority -> origin "scheme://authority" (lowercased, default port normalized) and matches
|
|
346
|
-
// that
|
|
344
|
+
// relative to the responding server -> origin null (any same-origin request); an absolute URI carries its own
|
|
345
|
+
// authority -> origin "scheme://authority" (lowercased, default port normalized) and matches only a request to
|
|
346
|
+
// that same origin, since an entry to a different host names a protection space on a different server. `full` is
|
|
347
347
|
// the pathname + query the request URI is prefix-compared against. A relative / unparseable absolute entry -> null.
|
|
348
348
|
function _domainEntry(d) {
|
|
349
349
|
d = String(d == null ? "" : d);
|
package/lib/http-retry-after.js
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
//
|
|
5
5
|
// @internal -- the shared HTTP Retry-After parser (RFC 7231 sec. 7.1.3): a delay-seconds count OR an
|
|
6
6
|
// HTTP-date (sec. 7.1.1.1). The enrollment protocol clients (pki.est now, pki.acme next) all surface a
|
|
7
|
-
// server's Retry-After as a bounded delay
|
|
8
|
-
// delay-seconds | HTTP-date grammar and the one-year ceiling live in
|
|
7
|
+
// server's Retry-After as a bounded delay, never sleeping on it in a single-step call, so the
|
|
8
|
+
// delay-seconds | HTTP-date grammar and the one-year ceiling live in one place both compose, error-
|
|
9
9
|
// factory-parameterized so each domain keeps its own typed verdict. A malformed / overflowing value
|
|
10
10
|
// fails closed (the caller's E(code, ...)); the value is never returned unparsed.
|
|
11
11
|
|
|
@@ -26,7 +26,7 @@ var HTTP_DATE_FORMS = [
|
|
|
26
26
|
new RegExp("^(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) " + MONTH + " [ \\d]\\d \\d{2}:\\d{2}:\\d{2} \\d{4}$"),
|
|
27
27
|
];
|
|
28
28
|
|
|
29
|
-
// Expand an obsolete RFC 850 two-digit year: HTTP interprets it
|
|
29
|
+
// Expand an obsolete RFC 850 two-digit year: HTTP interprets it relative to the receipt year, as the full
|
|
30
30
|
// year with these two digits within [refYear-50, refYear+49] (moved to the past only when it would
|
|
31
31
|
// otherwise be more than 50 years ahead). A fixed cutoff would misdate a year near the century boundary.
|
|
32
32
|
// With no receipt time, fall back to the RFC 6265 fixed rule (< 70 -> 2000s, else 1900s).
|
|
@@ -34,15 +34,15 @@ function _rfc850Year(yy, refMs) {
|
|
|
34
34
|
if (typeof refMs !== "number" || !isFinite(refMs)) return yy + (yy < 70 ? 2000 : 1900);
|
|
35
35
|
var refYear = new Date(refMs).getUTCFullYear();
|
|
36
36
|
var full = Math.floor(refYear / 100) * 100 + yy;
|
|
37
|
-
// Move to the
|
|
37
|
+
// Move to the previous century only when the same-century year would be more than 50 years ahead; never
|
|
38
38
|
// advance a past year to the future (an old two-digit year stays in the past).
|
|
39
39
|
if (full - refYear > 50) full -= 100;
|
|
40
40
|
return full;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
// Parse an HTTP-date to epoch ms, or NaN when its grammar, calendar, or time is invalid. Built from the
|
|
44
|
-
// extracted fields via Date.UTC (never Date.parse): every HTTP-date is UTC
|
|
45
|
-
// no GMT token
|
|
44
|
+
// extracted fields via Date.UTC (never Date.parse): every HTTP-date is UTC, the asctime form carries
|
|
45
|
+
// no GMT token so delegating would parse it in local time, and the round-trip check rejects an
|
|
46
46
|
// impossible date / time (Date.UTC normalizes Feb 31 -> Mar 2 just as Date.parse would).
|
|
47
47
|
function httpDateMs(s, refMs) {
|
|
48
48
|
if (!HTTP_DATE_FORMS.some(function (re) { return re.test(s); })) return NaN;
|
|
@@ -71,20 +71,20 @@ function httpDateMs(s, refMs) {
|
|
|
71
71
|
// retryAfterDate (epoch ms) plus, when opts.now (epoch ms) is given, a bounded retryAfterSeconds. Either
|
|
72
72
|
// form beyond the one-year ceiling, or a value that is neither, fails closed via opts.E(opts.code, ...).
|
|
73
73
|
// opts.cap (seconds) clamps a value above the cap to the cap instead of rejecting it; opts.lenient surfaces
|
|
74
|
-
// an otherwise-rejected value as a null retryAfterSeconds
|
|
75
|
-
// for whom the value is advisory and must not discard the response.
|
|
76
|
-
//
|
|
74
|
+
// an otherwise-rejected value as a null retryAfterSeconds. Both exist for a caller (e.g. an ARI poll
|
|
75
|
+
// cadence) for whom the value is advisory and must not discard the response. Nothing sleeps here; the
|
|
76
|
+
// value is surfaced for the caller to decide.
|
|
77
77
|
function parse(value, opts) {
|
|
78
78
|
opts = opts || {};
|
|
79
79
|
var raStr = String(value).trim();
|
|
80
80
|
var out = { retryAfterSeconds: null, retryAfterDate: null };
|
|
81
81
|
if (/^\d+$/.test(raStr)) {
|
|
82
82
|
var n = parseInt(raStr, 10);
|
|
83
|
-
// opts.cap (seconds): a caller that will clamp anyway (e.g. an ARI poll cadence) wants a delay
|
|
84
|
-
// ceiling reduced to the ceiling, not rejected
|
|
83
|
+
// opts.cap (seconds): a caller that will clamp anyway (e.g. an ARI poll cadence) wants a delay above its
|
|
84
|
+
// ceiling reduced to the ceiling, not rejected, so a valid-but-huge value never discards the response.
|
|
85
85
|
if (typeof opts.cap === "number" && n > opts.cap) { out.retryAfterSeconds = opts.cap; return out; }
|
|
86
|
-
// opts.lenient: a caller for whom the value is purely advisory wants an
|
|
87
|
-
// null (retryAfterSeconds stays null)
|
|
86
|
+
// opts.lenient: a caller for whom the value is purely advisory wants an unparseable one surfaced as
|
|
87
|
+
// null (retryAfterSeconds stays null) instead of a hard reject that would discard the whole response.
|
|
88
88
|
if (!Number.isSafeInteger(n) || n > MAX_RETRY_AFTER_SECONDS) { if (opts.lenient) return out; throw _fail(opts, "the Retry-After delay is out of the supported range (0.." + MAX_RETRY_AFTER_SECONDS + " seconds)"); }
|
|
89
89
|
out.retryAfterSeconds = n;
|
|
90
90
|
return out;
|
package/lib/http-transport.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* @intro
|
|
12
12
|
* The shared, fail-closed `node:https` transport the enrollment protocol clients
|
|
13
|
-
* drive
|
|
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
16
|
* `transport(request) -> Promise<{ status, headers, body, tls }>`. The first three are
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* channel -- `{ protocol, cipher, peerCertificate }` -- which is the one fact a caller
|
|
22
22
|
* cannot recover from the response bytes. An INJECTED substitute should return it too:
|
|
23
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
|
|
24
|
+
* is about to accept, and a transport that reports no cipher is trusted instead of
|
|
25
25
|
* refused (so a loopback test channel works), which means omitting the field silently
|
|
26
26
|
* skips that assertion.
|
|
27
27
|
*
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* transport. Trust is EXPLICIT and fail-closed: a request is refused unless it
|
|
30
30
|
* carries an https URL and either a `tls.anchors` set (an Explicit trust-anchor
|
|
31
31
|
* database, mapped to the node `ca` option) or an explicit `tls.useSystemStore`
|
|
32
|
-
* opt-in to node's bundled roots. `rejectUnauthorized` is
|
|
32
|
+
* opt-in to node's bundled roots. `rejectUnauthorized` is always on: there is no
|
|
33
33
|
* code path that disables server-certificate verification. The response body is
|
|
34
34
|
* bounded WHILE it streams: the accumulator aborts the socket the instant the running
|
|
35
35
|
* total crosses `maxResponseBytes`, before a byte reaches a decoder. A protocol
|
|
@@ -64,8 +64,8 @@ function _budget(value, key, dflt, max, E, code) {
|
|
|
64
64
|
return guard.limits.cap(value, key, dflt, { E: E, code: code, min: 1, max: max, label: key });
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
// Node's `ca` option expects PEM
|
|
68
|
-
// form (e.g. what pki.est.cacerts / parseCertsOnly return)
|
|
67
|
+
// Node's `ca` option expects PEM. A raw DER certificate Buffer, the toolkit's native trust-anchor
|
|
68
|
+
// form (e.g. what pki.est.cacerts / parseCertsOnly return), is silently ignored there, so the
|
|
69
69
|
// connection would fail server authentication despite a valid anchor. Wrap a DER anchor as PEM
|
|
70
70
|
// (a Buffer already carrying PEM text, or a PEM string, passes through unchanged).
|
|
71
71
|
function _pemifyAnchor(a) {
|
|
@@ -86,7 +86,7 @@ function _pemifyAnchors(anchors) {
|
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
// The OS system certificate store plus node's bundled roots (cached; the read is not free), for a
|
|
89
|
-
// tls.useSystemStore opt-in. Node's default (no `ca`) is
|
|
89
|
+
// tls.useSystemStore opt-in. Node's default (no `ca`) is just the bundled Mozilla set, so a server
|
|
90
90
|
// chaining to an OS-installed enterprise CA would fail without this. On a node too old for
|
|
91
91
|
// tls.getCACertificates the cache stays empty and the caller falls back to node's default store.
|
|
92
92
|
var _systemCaCache = null;
|
|
@@ -96,7 +96,7 @@ function _systemCa() {
|
|
|
96
96
|
if (typeof nodeTls.getCACertificates === "function") {
|
|
97
97
|
["system", "bundled"].forEach(function (t) {
|
|
98
98
|
// allow:swallow-unverified a store TYPE unsupported on this node is skipped; the other type
|
|
99
|
-
// (and, if both fail, node's default-store fallback) still applies
|
|
99
|
+
// (and, if both fail, node's default-store fallback) still applies. Best-effort store load.
|
|
100
100
|
try { var c = nodeTls.getCACertificates(t); if (Array.isArray(c)) out = out.concat(c); } catch (_e) { /* unsupported type */ }
|
|
101
101
|
});
|
|
102
102
|
}
|
|
@@ -107,14 +107,14 @@ function _systemCa() {
|
|
|
107
107
|
// Classify a bare IP string (no brackets) as a private / loopback / link-local / reserved destination an
|
|
108
108
|
// untrusted URL must not reach: RFC 1918 + loopback + this-network + multicast/reserved + 169.254 (cloud
|
|
109
109
|
// metadata) + CGNAT for IPv4, and loopback / unspecified / IPv4-mapped / ULA (fc00::/7) + link-local (fe80::/10)
|
|
110
|
-
// for IPv6. This is the
|
|
111
|
-
// here at DNS-
|
|
112
|
-
//
|
|
110
|
+
// for IPv6. This is the same range set the AIA literal pre-check applies (pki.path.build reuses it), enforced
|
|
111
|
+
// here at DNS-resolution time so a hostname pointing at an internal address is caught too. A malformed IP fails
|
|
112
|
+
// closed (net.isIP === 0 -> not a v4/v6 arm -> the caller treats a non-IP as un-judgeable, never as public).
|
|
113
113
|
function _isBlockedIp(ip) {
|
|
114
114
|
var fam = nodeNet.isIP(ip);
|
|
115
115
|
if (fam === 4) {
|
|
116
|
-
// net.isIP === 4 guarantees exactly four octets 0..255. Block the
|
|
117
|
-
// non-global set (RFC 6890) so an untrusted destination can reach
|
|
116
|
+
// net.isIP === 4 guarantees exactly four octets 0..255. Block the complete IANA special-purpose /
|
|
117
|
+
// non-global set (RFC 6890) so an untrusted destination can reach only globally-routable public space.
|
|
118
118
|
var o = ip.split("."), a = +o[0], b = +o[1], c = +o[2];
|
|
119
119
|
return a === 0 || a === 10 || a === 127 || a >= 224 || // this-network / RFC1918 10/8 / loopback / multicast 224/4 + reserved 240/4 + broadcast
|
|
120
120
|
(a === 100 && b >= 64 && b <= 127) || // 100.64/10 CGNAT
|
|
@@ -146,17 +146,18 @@ function _isBlockedIp(ip) {
|
|
|
146
146
|
return false;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
-
// A DNS-rebinding-safe SSRF filter, installed as node's `lookup`
|
|
150
|
-
// blockPrivateAddresses (the AIA fetch of an untrusted-cert URL). node connects to
|
|
151
|
-
// returns
|
|
152
|
-
// TOCTOU. Any private
|
|
149
|
+
// A DNS-rebinding-safe SSRF filter, installed as node's `lookup` only when a request opts into
|
|
150
|
+
// blockPrivateAddresses (the AIA fetch of an untrusted-cert URL). node connects to exactly the address this
|
|
151
|
+
// returns, with no second resolution, so checking the resolved address here pins it and closes the
|
|
152
|
+
// resolve/connect TOCTOU. Any private, loopback or link-local result fails the lookup, which surfaces as
|
|
153
|
+
// transport/blocked-address.
|
|
153
154
|
function _blockedAddrErr(hostname, address) {
|
|
154
155
|
var e = new Error("refusing to connect to " + hostname + " -> " + address + " (private / loopback / link-local address blocked)");
|
|
155
156
|
e.pkiBlockedAddress = true;
|
|
156
157
|
return e;
|
|
157
158
|
}
|
|
158
|
-
// Built over an injectable resolver (defaults to nodeDns.lookup) so every branch
|
|
159
|
-
// options.all array shape, a blocked result, a permitted result
|
|
159
|
+
// Built over an injectable resolver (defaults to nodeDns.lookup) so every branch (a resolve error, the
|
|
160
|
+
// options.all array shape, a blocked result, a permitted result) is unit-testable without a live DNS.
|
|
160
161
|
function _makeGuardedLookup(lookupFn) {
|
|
161
162
|
return function guardedLookup(hostname, options, callback) {
|
|
162
163
|
lookupFn(hostname, options || {}, function (err, address, family) {
|
|
@@ -211,7 +212,7 @@ function _classifyError(e, C) {
|
|
|
211
212
|
*
|
|
212
213
|
* @opts
|
|
213
214
|
* - `tls.anchors` -- Explicit trust anchor(s): a DER/PEM Buffer, an array, or PEM string(s) (node `ca`).
|
|
214
|
-
* - `tls.useSystemStore` -- boolean; the
|
|
215
|
+
* - `tls.useSystemStore` -- boolean; the one opt-in to node's bundled CA store (default false).
|
|
215
216
|
* - `tls.cert` / `tls.key` -- client certificate + key for mutual-TLS re-enrollment.
|
|
216
217
|
* - `tls.minVersion` -- 'TLSv1.2' (default) or 'TLSv1.3'; never below the floor.
|
|
217
218
|
* - `tls.servername` / `tls.checkServerIdentity` -- SNI + RFC 6125 identity; may tighten, never disable.
|