@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.
Files changed (104) hide show
  1. package/CHANGELOG.md +405 -386
  2. package/MIGRATING.md +43 -0
  3. package/README.md +12 -12
  4. package/lib/acme.js +31 -31
  5. package/lib/asn1-der.js +10 -10
  6. package/lib/attrcert-sign.js +19 -19
  7. package/lib/byte-reader.js +6 -6
  8. package/lib/byte-writer.js +5 -5
  9. package/lib/cbor-det.js +27 -24
  10. package/lib/cmc-build.js +97 -92
  11. package/lib/cmc-verify.js +106 -30
  12. package/lib/cmp-build.js +25 -25
  13. package/lib/cmp-session.js +70 -70
  14. package/lib/cmp-verify.js +71 -57
  15. package/lib/cms-compress.js +7 -7
  16. package/lib/cms-decrypt.js +90 -72
  17. package/lib/cms-encrypt.js +32 -32
  18. package/lib/cms-sign.js +74 -55
  19. package/lib/cms-verify.js +97 -75
  20. package/lib/composite-sig.js +13 -13
  21. package/lib/constants.js +4 -4
  22. package/lib/crl-sign.js +22 -22
  23. package/lib/crl-verify.js +7 -6
  24. package/lib/crmf-sign.js +14 -14
  25. package/lib/csr-sign.js +8 -8
  26. package/lib/ct.js +37 -37
  27. package/lib/edwards-point.js +7 -7
  28. package/lib/est.js +98 -55
  29. package/lib/framework-error.js +5 -5
  30. package/lib/guard-all.js +3 -3
  31. package/lib/guard-async.js +4 -4
  32. package/lib/guard-bytes.js +79 -79
  33. package/lib/guard-compress.js +17 -17
  34. package/lib/guard-crypto.js +1 -1
  35. package/lib/guard-encoding.js +15 -15
  36. package/lib/guard-header.js +3 -3
  37. package/lib/guard-identifier.js +16 -16
  38. package/lib/guard-json.js +15 -15
  39. package/lib/guard-limits.js +7 -7
  40. package/lib/guard-name.js +81 -16
  41. package/lib/guard-parsed.js +80 -80
  42. package/lib/guard-range.js +19 -19
  43. package/lib/guard-secret.js +11 -10
  44. package/lib/guard-text.js +6 -6
  45. package/lib/guard-time.js +10 -10
  46. package/lib/hpke.js +18 -17
  47. package/lib/http-digest.js +35 -35
  48. package/lib/http-retry-after.js +13 -13
  49. package/lib/http-transport.js +20 -19
  50. package/lib/inspect.js +53 -53
  51. package/lib/ip-utils.js +2 -2
  52. package/lib/jose.js +13 -13
  53. package/lib/key.js +16 -16
  54. package/lib/lint.js +51 -51
  55. package/lib/merkle.js +51 -36
  56. package/lib/mime.js +18 -18
  57. package/lib/ocsp-verify.js +10 -10
  58. package/lib/ocsp.js +13 -13
  59. package/lib/oid.js +29 -29
  60. package/lib/path-validate.js +114 -113
  61. package/lib/pbes2.js +16 -16
  62. package/lib/pkcs12-build.js +53 -53
  63. package/lib/pki-build.js +21 -19
  64. package/lib/rc2.js +1 -1
  65. package/lib/rfc3339.js +5 -5
  66. package/lib/schema-all.js +31 -31
  67. package/lib/schema-attrcert.js +12 -12
  68. package/lib/schema-c509.js +144 -142
  69. package/lib/schema-cmc.js +58 -58
  70. package/lib/schema-cmp.js +43 -43
  71. package/lib/schema-cms.js +45 -45
  72. package/lib/schema-crl.js +7 -7
  73. package/lib/schema-crmf.js +28 -28
  74. package/lib/schema-csr.js +12 -12
  75. package/lib/schema-csrattrs.js +16 -16
  76. package/lib/schema-engine.js +18 -18
  77. package/lib/schema-ocsp.js +15 -15
  78. package/lib/schema-pkcs12.js +20 -20
  79. package/lib/schema-pkcs8.js +2 -2
  80. package/lib/schema-pkix.js +131 -126
  81. package/lib/schema-smime.js +19 -19
  82. package/lib/schema-tsp.js +12 -12
  83. package/lib/schema-x509.js +3 -3
  84. package/lib/shbs.js +18 -18
  85. package/lib/sign-scheme.js +13 -13
  86. package/lib/sigstore.js +10 -11
  87. package/lib/sleep.js +1 -1
  88. package/lib/smime.js +308 -96
  89. package/lib/tls-cert-compress.js +18 -18
  90. package/lib/trust.js +27 -27
  91. package/lib/tsp-sign.js +17 -17
  92. package/lib/validator-all.js +1 -1
  93. package/lib/validator-attcert.js +1 -1
  94. package/lib/validator-cose.js +43 -44
  95. package/lib/validator-keydesc.js +3 -3
  96. package/lib/validator-sig.js +13 -13
  97. package/lib/validator-tls.js +11 -11
  98. package/lib/validator-tpm.js +20 -19
  99. package/lib/webauthn-mds.js +66 -66
  100. package/lib/webauthn.js +33 -33
  101. package/lib/webcrypto.js +15 -15
  102. package/lib/x509-sign.js +13 -13
  103. package/package.json +3 -2
  104. package/sbom.cdx.json +6 -6
@@ -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 rather than matched. `(0|[1-9]\d*)(\.(0|[1-9]\d*))+`
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 REJECTING string is a property of the pattern
27
- // rather than of the length -- and this guard's whole job is to be handed strings
28
- // that reject. Walking the string is one pass, one comparison per character, and it
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 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
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 -- for a LOOKUP key (oid.name
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), not an error.
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) -- every own key of `obj` must appear in `known`.
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 OPEN in the quietest possible way: an options object whose key is
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 rather than a truthiness test, so an inherited Object
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 rather than skipped.
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 OWN wording, so routing an existing check through this guard changes
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 THROW: an identical walk whose body FILTERS on the same table (copying the
102
- // recognised keys onward rather than rejecting the unrecognised ones) is a different operation with
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 LAST value of a DUPLICATE member, so a signed / wrapped
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 -- the JSON smuggling / parser-differential class
13
- // (CWE-20 / CWE-436). It also caps neither size nor nesting (CWE-770 / CWE-400),
14
- // and over a Buffer substitutes U+FFFD for invalid UTF-8 rather than failing.
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 BEFORE the strict/fatal UTF-8 decode, so
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 EVERY nesting depth, caps nesting, assigns each member as
20
- // an OWN data property (a "__proto__" key becomes a normal member -- it cannot
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 } -- the caller's caps + frozen domain/reason codes.
39
- // Both caps are REQUIRED authoring inputs: an omitted / NaN / fractional cap
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 -- maxDepth additionally against the
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 BEFORE the fatal UTF-8 decode (an oversized/ill-encoded document is
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 rather than matched as a pattern: the
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 BY the walk rather than by re-matching the token
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 -- 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
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.
@@ -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) -- the bound
23
- // at the parser is the actual control.
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 OWN typed verdict and bounds -- so a fractional/negative/NaN
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` NEVER fires -- a silently dead fanout
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 BEFORE an expensive hash / verify / parse
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 -- a silently dead size defence. Reject at
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 rather than their raw bytes.
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 -- a byte compare, or hashing
23
- // name.bytes as a lookup key -- silently treats two equal names as different, so
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] -- an embedded NUL / control byte enables the
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 rather than 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"
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 -- 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 rather than trusting a fabricated
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 -- the ONE place a DN identity
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 -- the mirror risk -- escaping a name constraint). The
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) rather than rejected, neutralize every C0 control byte and DEL by
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 -- the form most easily misread by a
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
  };