@blamejs/pki 0.5.7 → 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 +405 -386
- package/MIGRATING.md +43 -0
- package/README.md +12 -12
- package/lib/acme.js +31 -31
- package/lib/asn1-der.js +10 -10
- package/lib/attrcert-sign.js +19 -19
- 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 +97 -92
- package/lib/cmc-verify.js +106 -30
- package/lib/cmp-build.js +25 -25
- package/lib/cmp-session.js +70 -70
- package/lib/cmp-verify.js +71 -57
- package/lib/cms-compress.js +7 -7
- package/lib/cms-decrypt.js +90 -72
- package/lib/cms-encrypt.js +32 -32
- package/lib/cms-sign.js +74 -55
- package/lib/cms-verify.js +97 -75
- package/lib/composite-sig.js +13 -13
- package/lib/constants.js +4 -4
- package/lib/crl-sign.js +22 -22
- package/lib/crl-verify.js +7 -6
- package/lib/crmf-sign.js +14 -14
- package/lib/csr-sign.js +8 -8
- package/lib/ct.js +37 -37
- package/lib/edwards-point.js +7 -7
- package/lib/est.js +98 -55
- 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 +79 -79
- 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 +80 -80
- 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 +13 -13
- package/lib/oid.js +29 -29
- package/lib/path-validate.js +114 -113
- package/lib/pbes2.js +16 -16
- package/lib/pkcs12-build.js +53 -53
- package/lib/pki-build.js +21 -19
- 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 +45 -45
- 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 +13 -13
- 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 +17 -17
- 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 +20 -19
- package/lib/webauthn-mds.js +66 -66
- package/lib/webauthn.js +33 -33
- package/lib/webcrypto.js +15 -15
- package/lib/x509-sign.js +13 -13
- package/package.json +3 -2
- package/sbom.cdx.json +6 -6
package/lib/guard-identifier.js
CHANGED
|
@@ -21,11 +21,11 @@
|
|
|
21
21
|
// silent false-reject). Every string-form identifier check routes through here so
|
|
22
22
|
// the string and DER forms cannot diverge.
|
|
23
23
|
|
|
24
|
-
// The dotted-decimal grammar, walked
|
|
24
|
+
// The dotted-decimal grammar, walked instead of matched. `(0|[1-9]\d*)(\.(0|[1-9]\d*))+`
|
|
25
25
|
// nests a quantified group inside a quantified group with an alternation in each,
|
|
26
|
-
// which is the shape whose cost on a
|
|
27
|
-
//
|
|
28
|
-
//
|
|
26
|
+
// which is the shape whose cost on a rejecting string is a property of the pattern
|
|
27
|
+
// and not of the length, and this guard's whole job is to be handed strings that
|
|
28
|
+
// reject. Walking the string is one pass, one comparison per character, and it
|
|
29
29
|
// states the two rules plainly: an arc is one or more digits, and an arc longer than
|
|
30
30
|
// one digit does not start with zero (the leading-zero form round-trips to a
|
|
31
31
|
// DIFFERENT OID, which is the divergence this guard exists to stop).
|
|
@@ -49,14 +49,14 @@ function _isDottedDecimal(str) {
|
|
|
49
49
|
|
|
50
50
|
// assertCanonicalOid(str, E, code, label, boundsCode) -> str | throws
|
|
51
51
|
// A canonical dotted-decimal object identifier string: two or more arcs, each a
|
|
52
|
-
// non-negative decimal integer with no leading zero (the
|
|
53
|
-
// bounds are waived
|
|
54
|
-
// and 1 (the X.660
|
|
52
|
+
// non-negative decimal integer with no leading zero (the syntax), and, unless
|
|
53
|
+
// bounds are waived, the root arc 0..2 with the second arc 0..39 under roots 0
|
|
54
|
+
// and 1 (the X.660 encodability bounds: the first two arcs pack into a single
|
|
55
55
|
// sub-identifier as 40*root+second). E is the (code, message) typed-error factory:
|
|
56
56
|
// - a syntax fault always throws E(code, ...).
|
|
57
|
-
// - boundsCode === null waives the arc-bound check
|
|
57
|
+
// - boundsCode === null waives the arc-bound check, for a lookup key (oid.name
|
|
58
58
|
// / oid.has), where a well-formed but non-encodable OID is simply not
|
|
59
|
-
// registered (a miss)
|
|
59
|
+
// registered (a miss) and not an error.
|
|
60
60
|
// - otherwise an out-of-range arc throws E(boundsCode, ...) (boundsCode defaults
|
|
61
61
|
// to code), so a caller distinguishing the two reasons (oid.js: oid/bad-input
|
|
62
62
|
// syntax vs oid/bad-arc bounds) keeps both codes.
|
|
@@ -79,27 +79,27 @@ function assertCanonicalOid(str, E, code, label, boundsCode) {
|
|
|
79
79
|
return str;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
// assertKnownKeys(obj, known, E, code, label)
|
|
82
|
+
// assertKnownKeys(obj, known, E, code, label): every own key of `obj` must appear in `known`.
|
|
83
83
|
//
|
|
84
84
|
// The shape this replaces was hand-written in a dozen callers, and it is worth one home because
|
|
85
|
-
// getting it wrong fails
|
|
85
|
+
// getting it wrong fails open in the quietest possible way: an options object whose key is
|
|
86
86
|
// misspelled silently carries the default, so a caller who asked for a stricter check gets the
|
|
87
87
|
// looser behaviour and no error. Two details are easy to lose in a re-inline and are fixed here.
|
|
88
|
-
// `known` is consulted with hasOwnProperty
|
|
88
|
+
// `known` is consulted with hasOwnProperty and not a truthiness test, so an inherited Object
|
|
89
89
|
// member ("constructor", "toString") cannot read as a recognised key; and the walk is over own
|
|
90
|
-
// enumerable keys, so a `__proto__` arriving from JSON is inspected
|
|
90
|
+
// enumerable keys, so a `__proto__` arriving from JSON is inspected instead of skipped.
|
|
91
91
|
//
|
|
92
92
|
// @enforced-by behavioral -- an options-shape walk has no rename-proof code shape; the RED vectors
|
|
93
93
|
// (an unknown key, an inherited name, a JSON-borne __proto__) are the guard.
|
|
94
94
|
//
|
|
95
|
-
// `message` is the caller's
|
|
95
|
+
// `message` is the caller's own wording, so routing an existing check through this guard changes
|
|
96
96
|
// what is checked, never what the operator reads: a string is the prefix the quoted key is appended
|
|
97
97
|
// to, and a function (key) -> string builds the whole sentence, for the many callers that name the
|
|
98
98
|
// key mid-sentence and follow it with the hint that says what to pass instead.
|
|
99
99
|
//
|
|
100
100
|
// @enforced-by guard-shape-reinlined
|
|
101
|
-
// The shape requires the
|
|
102
|
-
// recognised keys onward
|
|
101
|
+
// The shape requires the throw: an identical walk whose body filters on the same table (copying the
|
|
102
|
+
// recognised keys onward instead of rejecting the unrecognised ones) is a different operation with
|
|
103
103
|
// no fail-open risk, and must not be dragged through a guard that only knows how to reject.
|
|
104
104
|
// @guard-shape Object\.keys\(\w+(?:\.\w+)*\)\.forEach\(function \((\w+)\) \{\s*if \(![\w.]+\[\1\]\) (?:\{\s*)?throw
|
|
105
105
|
function assertKnownKeys(obj, known, E, code, message) {
|
package/lib/guard-json.js
CHANGED
|
@@ -7,17 +7,17 @@
|
|
|
7
7
|
// pki.acme resource objects, pki.webcrypto JWK unwrap).
|
|
8
8
|
//
|
|
9
9
|
// guard-json -- strict, bounded parse of an untrusted JSON document. JSON.parse
|
|
10
|
-
// silently takes the
|
|
10
|
+
// silently takes the last value of a duplicate member, so a signed or wrapped
|
|
11
11
|
// object carrying two members of the same name resolves differently for a
|
|
12
|
-
// verifier than for a consumer
|
|
13
|
-
// (CWE-20 / CWE-436). It also caps neither size nor nesting (CWE-770 /
|
|
14
|
-
// and over a Buffer substitutes U+FFFD for invalid UTF-8
|
|
12
|
+
// verifier than for a consumer. That is the JSON smuggling / parser-differential
|
|
13
|
+
// class (CWE-20 / CWE-436). It also caps neither size nor nesting (CWE-770 /
|
|
14
|
+
// CWE-400), and over a Buffer substitutes U+FFFD for invalid UTF-8 instead of failing.
|
|
15
15
|
//
|
|
16
16
|
// This is a single hand-written recursive-descent reader that composes
|
|
17
|
-
// guard.text.decode (the byte cap runs
|
|
17
|
+
// guard.text.decode (the byte cap runs before the strict/fatal UTF-8 decode, so
|
|
18
18
|
// an oversized document is rejected before it is materialized), then rejects a
|
|
19
|
-
// duplicate member at
|
|
20
|
-
// an
|
|
19
|
+
// duplicate member at every nesting depth, caps nesting, assigns each member as
|
|
20
|
+
// an own data property (a "__proto__" key becomes a normal member: it cannot
|
|
21
21
|
// mutate the prototype nor, being a non-own assignment for a primitive, silently
|
|
22
22
|
// defeat the duplicate-member gate), and enforces the RFC 8259 number/string
|
|
23
23
|
// grammar (no leading zero, a fraction/exponent needs a digit, no bare "-").
|
|
@@ -35,11 +35,11 @@ function _hexVal(c) {
|
|
|
35
35
|
|
|
36
36
|
// parse(input, ErrorClass, spec) -> value. `input` is a Buffer or a string.
|
|
37
37
|
// spec = { maxBytes, maxDepth, badJson, tooDeep, duplicateMember, tooLarge,
|
|
38
|
-
// badInput, label }
|
|
39
|
-
// Both caps are
|
|
38
|
+
// badInput, label }, the caller's caps and frozen domain/reason codes.
|
|
39
|
+
// Both caps are required authoring inputs: an omitted, NaN or fractional cap
|
|
40
40
|
// would silently disable the bound it configures (a depth-uncapped recursive
|
|
41
41
|
// descent escapes as a raw stack-overflow RangeError), so they are validated
|
|
42
|
-
// through the shared cap guards at entry
|
|
42
|
+
// through the shared cap guards at entry, maxDepth additionally against the
|
|
43
43
|
// stack-safe recursion ceiling, so no caller cap can exceed frame safety.
|
|
44
44
|
// @enforced-by json-parse-not-via-guard
|
|
45
45
|
function parse(input, ErrorClass, spec) {
|
|
@@ -49,7 +49,7 @@ function parse(input, ErrorClass, spec) {
|
|
|
49
49
|
}
|
|
50
50
|
var maxBytes = limits.cap(spec.maxBytes, "guard.json.parse spec.maxBytes", undefined);
|
|
51
51
|
var maxDepth = limits.depthCap(spec.maxDepth, "guard.json.parse spec.maxDepth", undefined);
|
|
52
|
-
// Byte cap
|
|
52
|
+
// Byte cap before the fatal UTF-8 decode (an oversized/ill-encoded document is
|
|
53
53
|
// rejected before it is turned into a string).
|
|
54
54
|
var str = text.decode(input, maxBytes, ErrorClass, {
|
|
55
55
|
charset: "utf-8", fatal: true, tooLarge: spec.tooLarge, badDecode: spec.badJson, badInput: spec.badInput, label: spec.label,
|
|
@@ -126,7 +126,7 @@ function parse(input, ErrorClass, spec) {
|
|
|
126
126
|
else if (e === "r") s += "\r";
|
|
127
127
|
else if (e === "t") s += "\t";
|
|
128
128
|
else if (e === "u") {
|
|
129
|
-
// Four hex digits, read as digits
|
|
129
|
+
// Four hex digits, read as digits instead of matched as a pattern: the
|
|
130
130
|
// scanner is already walking this string one character at a time, and a
|
|
131
131
|
// parser handed hostile input should not hand any of it to a second engine.
|
|
132
132
|
var cp = 0;
|
|
@@ -144,10 +144,10 @@ function parse(input, ErrorClass, spec) {
|
|
|
144
144
|
} else s += c;
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
|
-
// RFC 8259 sec. 6, enforced
|
|
147
|
+
// RFC 8259 sec. 6, enforced by the walk itself and not by re-matching the token
|
|
148
148
|
// afterwards. The scan already knows where each part starts and ends, so the
|
|
149
|
-
// grammar's three rules
|
|
150
|
-
// fraction with at least one digit, an exponent with at least one digit
|
|
149
|
+
// grammar's three rules (an integer part that is "0" or has no leading zero, a
|
|
150
|
+
// fraction with at least one digit, an exponent with at least one digit) are
|
|
151
151
|
// checked as it goes. Re-matching what the scanner just read meant maintaining
|
|
152
152
|
// the same grammar twice, in two notations, and the pattern was the copy whose
|
|
153
153
|
// cost on a rejecting token could not be bounded from outside.
|
package/lib/guard-limits.js
CHANGED
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
// raw RangeError, not a typed verdict) once the cap is raised past the frame
|
|
20
20
|
// limit; depthCap refuses a maxDepth above the stack-safe ceiling. A bad limit
|
|
21
21
|
// is an authoring bug, so this tier throws a TypeError (not a PkiError). NIST
|
|
22
|
-
// 800-53 SI-10 / SC-5 do not require these bounds (framework lag)
|
|
23
|
-
//
|
|
22
|
+
// 800-53 SI-10 / SC-5 do not require these bounds (framework lag). The control
|
|
23
|
+
// that actually holds is the bound at the parser.
|
|
24
24
|
|
|
25
25
|
var constants = require("./constants");
|
|
26
26
|
|
|
@@ -31,7 +31,7 @@ var constants = require("./constants");
|
|
|
31
31
|
// With no opts it is the config-time codec default: a bare TypeError with the
|
|
32
32
|
// "decode:" prefix and a >= 0 floor (the asn1-der / cbor-det callers are
|
|
33
33
|
// untouched). opts lets a caller in another currency reuse the same integer
|
|
34
|
-
// floor with its
|
|
34
|
+
// floor with its own typed verdict and bounds, so a fractional/negative/NaN
|
|
35
35
|
// bound cannot be spelled a fifth way with a dropped Number.isInteger:
|
|
36
36
|
// E - a (code, message) -> Error factory; when given, an out-of-range
|
|
37
37
|
// value throws E(code, ...) instead of the bare TypeError.
|
|
@@ -89,7 +89,7 @@ function depthCap(value, key, dflt) {
|
|
|
89
89
|
// dense TLV run rejects at the cap) driving the composing decoders are the guard.
|
|
90
90
|
function counter(max, E, code, label) {
|
|
91
91
|
// The ceiling is an authoring input: an undefined / NaN / fractional max
|
|
92
|
-
// builds a counter whose `n > max`
|
|
92
|
+
// builds a counter whose `n > max` never fires, leaving a silently dead fanout
|
|
93
93
|
// defence. Reject at construction (config-time TypeError).
|
|
94
94
|
if (!Number.isInteger(max) || max < 0) {
|
|
95
95
|
throw new TypeError("guard.limits.counter: max must be a non-negative integer");
|
|
@@ -104,7 +104,7 @@ function counter(max, E, code, label) {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
// byteCap(buf, max, E, code, label) -> the same buffer, once proven within `max`
|
|
107
|
-
// bytes. Bounds a caller-supplied buffer
|
|
107
|
+
// bytes. Bounds a caller-supplied buffer before an expensive hash / verify / parse
|
|
108
108
|
// materializes over it (CWE-400 uncontrolled resource consumption / CWE-770
|
|
109
109
|
// allocation without limits -- the class guard.text.decode closes for a decoded
|
|
110
110
|
// string and counter closes for element fanout, here for a raw byte input). Tier-2:
|
|
@@ -117,8 +117,8 @@ function counter(max, E, code, label) {
|
|
|
117
117
|
// over-size RED vectors driving the composing verifiers are the guard.
|
|
118
118
|
function byteCap(buf, max, E, code, label) {
|
|
119
119
|
// The ceiling is an authoring input: an undefined / NaN / fractional / negative
|
|
120
|
-
// max makes `length > max` never fire
|
|
121
|
-
// config time (TypeError), regardless of the value currency E selects.
|
|
120
|
+
// max makes `length > max` never fire, and the size defence is then silently
|
|
121
|
+
// dead. Reject at config time (TypeError), regardless of the value currency E selects.
|
|
122
122
|
if (!Number.isInteger(max) || max < 0) {
|
|
123
123
|
throw new TypeError("guard.limits.byteCap: max must be a non-negative integer");
|
|
124
124
|
}
|
package/lib/guard-name.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
//
|
|
9
9
|
// guard-name -- fail-closed name integrity: reject an embedded control byte in a
|
|
10
10
|
// distinguished-name / SAN string, and compare two distinguished names by their
|
|
11
|
-
// RFC 5280 sec. 7.1 canonical identity
|
|
11
|
+
// RFC 5280 sec. 7.1 canonical identity, never their raw bytes.
|
|
12
12
|
//
|
|
13
13
|
// Defends the name-truncation / display-confusion class (CVE-2009-2408): a NUL
|
|
14
14
|
// or control byte embedded in a decoded name lets an attacker make two different
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
//
|
|
20
20
|
// Defends the DN identity-vs-bytes class (CWE-706): a distinguished name has many
|
|
21
21
|
// RFC 5280 sec. 7.1-equal DER encodings (case, whitespace, PrintableString vs
|
|
22
|
-
// UTF8String). Binding identity to raw bytes
|
|
23
|
-
// name.bytes as a lookup key
|
|
22
|
+
// UTF8String). Binding identity to raw bytes (a byte compare, or hashing
|
|
23
|
+
// name.bytes as a lookup key) silently treats two equal names as different, so
|
|
24
24
|
// certificate chaining breaks, a revocation issuer / OCSP responder fails to
|
|
25
25
|
// match, or (the mirror risk) a name constraint is escaped. Every DN identity
|
|
26
26
|
// decision routes through the one canonical comparison here.
|
|
@@ -45,7 +45,7 @@ function assertNoControlBytes(str, E, code, label) {
|
|
|
45
45
|
|
|
46
46
|
// assertPrintableIa5(buf, E, code, label) -> buf | throws E(code, ...)
|
|
47
47
|
// IA5String policy (a dNSName / rfc822Name / URI GeneralName): every byte must be
|
|
48
|
-
// printable 7-bit ASCII [0x20, 0x7e]
|
|
48
|
+
// printable 7-bit ASCII [0x20, 0x7e], since an embedded NUL or control byte enables the
|
|
49
49
|
// same name-truncation bypass downstream. `buf` is the raw GeneralName content.
|
|
50
50
|
// @enforced-by behavioral -- the printable-IA5 byte-range reject has no rename-proof
|
|
51
51
|
// code shape distinct from the ASN.1 IA5 reader; the CVE-2009-2408 RED vectors
|
|
@@ -65,9 +65,9 @@ function assertPrintableIa5(buf, E, code, label) {
|
|
|
65
65
|
// matches OpenSSL's X509_NAME_cmp, so a chain OpenSSL accepts is not rejected. This
|
|
66
66
|
// canonicalization is the shape the guard-shape-reinlined detector keys on
|
|
67
67
|
// (declared on dnEqual): a boundary hand-rolling it is re-implementing DN identity.
|
|
68
|
-
// The collapse is a single walk
|
|
69
|
-
// attribute value of every name the toolkit compares
|
|
70
|
-
// supplies included
|
|
68
|
+
// The collapse is a single walk, not a pattern replace. This runs on every
|
|
69
|
+
// attribute value of every name the toolkit compares, a certificate an attacker
|
|
70
|
+
// supplies included, and one pass with a running "was the last character a space"
|
|
71
71
|
// flag costs exactly the length. It also spells out which characters count as
|
|
72
72
|
// whitespace: RFC 5280 sec. 7.1 defers to X.520's caseIgnoreMatch, whose SPACE is
|
|
73
73
|
// the ASCII space, and a pattern's \s silently also folds VT, FF, NBSP and every
|
|
@@ -95,9 +95,9 @@ function _canonAttrValue(v, E, code, label) {
|
|
|
95
95
|
// not a style check: a parsed Name has no `length`, so `a.length !== b.length` compares undefined
|
|
96
96
|
// with undefined, the loop over `i < undefined` never runs, and the comparison returns TRUE for two
|
|
97
97
|
// unrelated names. An identity comparison that answers "equal" because it was handed the wrong
|
|
98
|
-
// shape is the worst possible failure of this guard
|
|
99
|
-
// decided, and every caller reads a true from it as proof. So a comparison that
|
|
100
|
-
// performed refuses, and the caller learns at the call site
|
|
98
|
+
// shape is the worst possible failure of this guard: it is the one place a DN identity is
|
|
99
|
+
// decided, and every caller reads a true from it as proof. So a comparison that cannot be
|
|
100
|
+
// performed refuses, and the caller learns at the call site instead of trusting a fabricated
|
|
101
101
|
// match. (A caller holding a parsed Name passes its `.rdns`.)
|
|
102
102
|
function _assertSequence(a, b, E, code, label, what) {
|
|
103
103
|
if (!Array.isArray(a) || !Array.isArray(b)) {
|
|
@@ -122,10 +122,10 @@ function rdnEqual(a, b, E, code, label) {
|
|
|
122
122
|
return true;
|
|
123
123
|
}
|
|
124
124
|
// dnEqual(rdnsA, rdnsB, E, code, label) -> boolean. RFC 5280 sec. 7.1 canonical
|
|
125
|
-
// distinguished-name comparison over the RDN sequence
|
|
125
|
+
// distinguished-name comparison over the RDN sequence, the single place a DN identity
|
|
126
126
|
// is decided, so no caller binds identity to raw DER (a byte compare, or hashing
|
|
127
|
-
// name.bytes, would treat two RFC 5280-equal names as different, breaking a chain
|
|
128
|
-
// a revocation match, or
|
|
127
|
+
// name.bytes, would treat two RFC 5280-equal names as different, breaking a chain or
|
|
128
|
+
// a revocation match, or, the mirror risk, escaping a name constraint). The
|
|
129
129
|
// per-RDN canonicalization + control-byte reject is in rdnEqual / _canonAttrValue.
|
|
130
130
|
// @enforced-by guard-shape-reinlined
|
|
131
131
|
// @guard-shape replace\(/\\s\+/g,
|
|
@@ -140,7 +140,7 @@ function dnEqual(rdnsA, rdnsB, E, code, label) {
|
|
|
140
140
|
|
|
141
141
|
// escapeControlBytes(str) -> str. The render-side sibling of assertNoControlBytes:
|
|
142
142
|
// where a name string must still be DISPLAYED best-effort (a human-readable report,
|
|
143
|
-
// a log line)
|
|
143
|
+
// a log line) instead of rejected, neutralize every C0 control byte and DEL by
|
|
144
144
|
// rendering it as \xHH. A bare CR / LF / NUL in a decoded dNSName or DN value would
|
|
145
145
|
// otherwise forge or overwrite report lines in a terminal or log (CWE-117 output-log
|
|
146
146
|
// injection / CWE-116 improper output encoding). Non-control bytes pass through.
|
|
@@ -170,7 +170,7 @@ var DN_SPECIAL = { 0x2c: 1, 0x2b: 1, 0x22: 1, 0x5c: 1, 0x3c: 1, 0x3e: 1, 0x3b: 1
|
|
|
170
170
|
// RFC 4514 sec. 2.4's special set is the TABLE above and the walk is a single pass.
|
|
171
171
|
// The separator escape used to be a pattern replace feeding a second loop, which
|
|
172
172
|
// meant two passes over an attacker-supplied value and a character class holding a
|
|
173
|
-
// quote and a backslash inside a regex literal
|
|
173
|
+
// quote and a backslash inside a regex literal, the form most easily misread by a
|
|
174
174
|
// human and, as it happened, by the codebase-patterns literal-stripper.
|
|
175
175
|
//
|
|
176
176
|
// @enforced-by behavioral -- the escaping has no rename-proof code shape distinct
|
|
@@ -191,8 +191,73 @@ function escapeDnValue(v) {
|
|
|
191
191
|
return out;
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
// emailEqual(a, b) -> "match" | "no-match" | "not-comparable". The RFC 5280 sec. 7.5
|
|
195
|
+
// sibling of dnEqual: two email addresses match iff their local-parts are an exact match
|
|
196
|
+
// AND their host-parts match under a case-insensitive ASCII comparison. The local-part is
|
|
197
|
+
// NEVER case-folded -- RFC 8398 sec. 5 states it "MUST NOT be transformed in any way, such
|
|
198
|
+
// as by doing case folding or normalization of any kind", because Alice@ and alice@ are
|
|
199
|
+
// permitted to be different mailboxes.
|
|
200
|
+
//
|
|
201
|
+
// The result is three-valued on purpose. An internationalized host-part (an `xn--` A-label
|
|
202
|
+
// or any non-ASCII octet) needs an IDNA2008 A-label/U-label transform this toolkit does not
|
|
203
|
+
// carry, so the rule cannot be applied. Returning "no-match" there would answer a question
|
|
204
|
+
// that was never asked, and a caller acting on it would reject a legitimate sender; the
|
|
205
|
+
// honest verdict is that the comparison was unavailable. A guard that cannot decide says so
|
|
206
|
+
// instead of guessing (the value is fail-CLOSED at the call site: no caller may read
|
|
207
|
+
// "not-comparable" as a pass).
|
|
208
|
+
//
|
|
209
|
+
// RFC 8398 sec. 5 also forbids wildcards outright -- "implementations MUST NOT interpret
|
|
210
|
+
// any characters as wildcards" -- so no character here is ever special. A certificate
|
|
211
|
+
// carrying `*@example.com` matches that literal string and nothing else. Name-CONSTRAINT
|
|
212
|
+
// processing (RFC 5280 sec. 4.2.1.10 as extended by RFC 8398 sec. 6), where a bare domain
|
|
213
|
+
// legitimately covers a whole subtree, is a different rule and stays in path-validate.
|
|
214
|
+
//
|
|
215
|
+
// @enforced-by guard-shape-reinlined
|
|
216
|
+
// @guard-shape lastIndexOf\("@"\)
|
|
217
|
+
function emailEqual(a, b) {
|
|
218
|
+
if (typeof a !== "string" || typeof b !== "string") return "not-comparable";
|
|
219
|
+
// Split on the LAST "@": a quoted local-part may legally contain one, and taking the
|
|
220
|
+
// first would move part of the mailbox into the domain and compare the wrong halves.
|
|
221
|
+
var ai = a.lastIndexOf("@"), bi = b.lastIndexOf("@");
|
|
222
|
+
if (ai <= 0 || bi <= 0 || ai === a.length - 1 || bi === b.length - 1) return "not-comparable";
|
|
223
|
+
var aLocal = a.slice(0, ai), bLocal = b.slice(0, bi);
|
|
224
|
+
var aHost = a.slice(ai + 1), bHost = b.slice(bi + 1);
|
|
225
|
+
if (!_asciiHost(aHost) || !_asciiHost(bHost)) return "not-comparable";
|
|
226
|
+
if (aLocal !== bLocal) return "no-match";
|
|
227
|
+
return _lowerAscii(aHost) === _lowerAscii(bHost) ? "match" : "no-match";
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// A host-part this comparison can decide: ASCII, and carrying no A-label. Anything else
|
|
231
|
+
// needs the IDNA transform named above, so it is reported as undecidable rather than
|
|
232
|
+
// compared as raw bytes -- `xn--e1afmkfd.example` and its Unicode form are one host, and
|
|
233
|
+
// treating them as different would be a wrong answer rather than an absent one.
|
|
234
|
+
function _asciiHost(h) {
|
|
235
|
+
if (h.length === 0) return false;
|
|
236
|
+
// ASCII is the whole test, and an A-label is ASCII. Two hosts already in A-label form
|
|
237
|
+
// need no transformation to be compared -- they are the same encoding -- so rejecting
|
|
238
|
+
// them for carrying `xn--` would make sender binding unusable for every certificate with
|
|
239
|
+
// an internationalized domain, including one whose address matches exactly. The case an
|
|
240
|
+
// IDNA transform WOULD be needed for is an A-label against a U-label, and the U-label
|
|
241
|
+
// side is non-ASCII, so it is refused here anyway.
|
|
242
|
+
for (var i = 0; i < h.length; i++) if (h.charCodeAt(i) > 0x7f) return false;
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ASCII-only lowercase. String.toLowerCase() is Unicode-aware and would fold characters
|
|
247
|
+
// outside ASCII (the Kelvin sign lowercases to "k"), which is precisely the mapping the
|
|
248
|
+
// host-part rule does not authorize; _asciiHost has already excluded those inputs, so this
|
|
249
|
+
// stays a plain A-Z shift rather than inheriting a locale-sensitive transform.
|
|
250
|
+
function _lowerAscii(s) {
|
|
251
|
+
var out = "";
|
|
252
|
+
for (var i = 0; i < s.length; i++) {
|
|
253
|
+
var c = s.charCodeAt(i);
|
|
254
|
+
out += (c >= 0x41 && c <= 0x5a) ? String.fromCharCode(c + 32) : s.charAt(i);
|
|
255
|
+
}
|
|
256
|
+
return out;
|
|
257
|
+
}
|
|
258
|
+
|
|
194
259
|
module.exports = {
|
|
195
260
|
assertNoControlBytes: assertNoControlBytes, assertPrintableIa5: assertPrintableIa5,
|
|
196
|
-
dnEqual: dnEqual, rdnEqual: rdnEqual,
|
|
261
|
+
dnEqual: dnEqual, rdnEqual: rdnEqual, emailEqual: emailEqual,
|
|
197
262
|
escapeControlBytes: escapeControlBytes, escapeDnValue: escapeDnValue,
|
|
198
263
|
};
|