@blamejs/pki 0.5.7 → 0.5.9

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 (105) hide show
  1. package/CHANGELOG.md +418 -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 +5 -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-der.js +152 -0
  36. package/lib/guard-encoding.js +15 -15
  37. package/lib/guard-header.js +3 -3
  38. package/lib/guard-identifier.js +16 -16
  39. package/lib/guard-json.js +15 -15
  40. package/lib/guard-limits.js +7 -7
  41. package/lib/guard-name.js +81 -16
  42. package/lib/guard-parsed.js +80 -80
  43. package/lib/guard-range.js +19 -19
  44. package/lib/guard-secret.js +11 -10
  45. package/lib/guard-text.js +6 -6
  46. package/lib/guard-time.js +10 -10
  47. package/lib/hpke.js +18 -17
  48. package/lib/http-digest.js +35 -35
  49. package/lib/http-retry-after.js +13 -13
  50. package/lib/http-transport.js +20 -19
  51. package/lib/inspect.js +53 -53
  52. package/lib/ip-utils.js +2 -2
  53. package/lib/jose.js +13 -13
  54. package/lib/key.js +16 -16
  55. package/lib/lint.js +51 -51
  56. package/lib/merkle.js +51 -36
  57. package/lib/mime.js +18 -18
  58. package/lib/ocsp-verify.js +10 -10
  59. package/lib/ocsp.js +13 -13
  60. package/lib/oid.js +29 -29
  61. package/lib/path-validate.js +114 -113
  62. package/lib/pbes2.js +16 -16
  63. package/lib/pkcs12-build.js +53 -53
  64. package/lib/pki-build.js +48 -20
  65. package/lib/rc2.js +1 -1
  66. package/lib/rfc3339.js +5 -5
  67. package/lib/schema-all.js +31 -31
  68. package/lib/schema-attrcert.js +12 -12
  69. package/lib/schema-c509.js +149 -197
  70. package/lib/schema-cmc.js +58 -58
  71. package/lib/schema-cmp.js +43 -43
  72. package/lib/schema-cms.js +45 -45
  73. package/lib/schema-crl.js +7 -7
  74. package/lib/schema-crmf.js +28 -28
  75. package/lib/schema-csr.js +12 -12
  76. package/lib/schema-csrattrs.js +16 -16
  77. package/lib/schema-engine.js +18 -18
  78. package/lib/schema-ocsp.js +15 -15
  79. package/lib/schema-pkcs12.js +20 -20
  80. package/lib/schema-pkcs8.js +2 -2
  81. package/lib/schema-pkix.js +131 -126
  82. package/lib/schema-smime.js +19 -19
  83. package/lib/schema-tsp.js +12 -12
  84. package/lib/schema-x509.js +3 -3
  85. package/lib/shbs.js +18 -18
  86. package/lib/sign-scheme.js +13 -13
  87. package/lib/sigstore.js +10 -11
  88. package/lib/sleep.js +1 -1
  89. package/lib/smime.js +308 -96
  90. package/lib/tls-cert-compress.js +18 -18
  91. package/lib/trust.js +27 -27
  92. package/lib/tsp-sign.js +17 -17
  93. package/lib/validator-all.js +1 -1
  94. package/lib/validator-attcert.js +1 -1
  95. package/lib/validator-cose.js +43 -44
  96. package/lib/validator-keydesc.js +3 -3
  97. package/lib/validator-sig.js +13 -13
  98. package/lib/validator-tls.js +11 -11
  99. package/lib/validator-tpm.js +20 -19
  100. package/lib/webauthn-mds.js +66 -66
  101. package/lib/webauthn.js +33 -33
  102. package/lib/webcrypto.js +15 -15
  103. package/lib/x509-sign.js +13 -13
  104. package/package.json +3 -2
  105. package/sbom.cdx.json +6 -6
@@ -0,0 +1,152 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ //
5
+ // @internal -- no operator-facing namespace. The documented surface is the consumers
6
+ // whose encoding integrity composes this guard (pki.schema.c509 reconstruct sites,
7
+ // pki.x509.sign and its sibling builders on any subjectAltName otherName value).
8
+ //
9
+ // guard-der: the single choke point for "these raw bytes are exactly one strictly-valid
10
+ // DER element", for every site that splices caller-supplied ANY bytes verbatim into
11
+ // something it then signs or reconstructs.
12
+ //
13
+ // The bug class is framing mistaken for validation. asn1.decode checks TLV framing and
14
+ // rejects trailing data, and stopping there feels sufficient. It is not: framing accepts
15
+ // a BOOLEAN whose content octet is 0x01 where DER requires 0xFF, a NumericString holding
16
+ // "@", and a SET whose members sit in no canonical order. None of those is DER. Spliced
17
+ // into a certificate and signed, the issuer emits a structure that strict relying parties
18
+ // reject, under a real signature, while the producing verb reported success (CWE-20).
19
+ //
20
+ // The rule is validate-or-refuse. A universal primitive runs through its strict content
21
+ // reader, and a universal type with no reader here is REFUSED, so the table is exhaustive
22
+ // by refusal and adding a type is a deliberate act. A non-universal element (a
23
+ // legitimately context- or application-tagged ANY) passes on its framing because no
24
+ // content rule is knowable for it, and its constructed children are still walked.
25
+ //
26
+ // Every entry point takes the CALLER's error factory and code, so a c509 splice reports in
27
+ // the c509 domain and a certificate builder in the x509 domain.
28
+ // asn1-der composes this guard family, so requiring the codec at module scope would read its
29
+ // exports mid-initialisation and see an empty object. The require and the table it builds are
30
+ // therefore both deferred to first use -- the documented circular-load exception to top-of-file
31
+ // requires. Guards sit BELOW the codec in the dependency order; this is the one place that
32
+ // order is inverted, and it is inverted lazily.
33
+ var asn1 = null;
34
+ function _asn1() { if (asn1 === null) asn1 = require("./asn1-der"); return asn1; }
35
+
36
+ // The strict content reader per universal primitive tag. A tag absent from this table has no
37
+ // validator here and is refused by `element` below -- that refusal is the point.
38
+ var VALUE_READERS = null;
39
+ function _readers() {
40
+ if (VALUE_READERS !== null) return VALUE_READERS;
41
+ var asn1 = _asn1();
42
+ var m = {}, R = asn1.read, T = asn1.TAGS;
43
+ // ENUMERATED shares INTEGER's content rules and is NOT constrained to non-negative
44
+ // values. X.680 (02/2021) sec. 20.2 requires each NamedNumber's SignedNumber to be
45
+ // distinct, and sec. 19 defines SignedNumber ::= number | "-" number; the non-negativity
46
+ // in sec. 20.3 governs only an EnumerationItem written as a bare identifier, which is
47
+ // auto-assigned. `ENUMERATED { lowPriority(-1), normal(0) }` is well-formed, so rejecting
48
+ // a negative here would refuse valid input. This guard validates the ENCODING; which
49
+ // values a particular ENUMERATED admits lives in a type definition an opaque ANY does
50
+ // not carry.
51
+ m[T.BOOLEAN] = R.boolean; m[T.INTEGER] = R.integer; m[T.ENUMERATED] = R.enumerated;
52
+ m[T.BIT_STRING] = R.bitString; m[T.OCTET_STRING] = R.octetString; m[T.NULL] = R.nullValue;
53
+ m[T.OBJECT_IDENTIFIER] = R.oid; m[T.UTC_TIME] = R.time; m[T.GENERALIZED_TIME] = R.time;
54
+ // NumericString reads through its own reader: it is not a DirectoryString type, and routing
55
+ // it through read.string would fold it into the RFC 5280 sec. 7.1 name-comparison identity
56
+ // class (see asn1-der.js).
57
+ m[T.NUMERIC_STRING] = R.numericString;
58
+ [T.UTF8_STRING, T.PRINTABLE_STRING, T.IA5_STRING, T.TELETEX_STRING, T.VISIBLE_STRING,
59
+ T.BMP_STRING, T.UNIVERSAL_STRING].forEach(function (t) { m[t] = R.string; });
60
+ VALUE_READERS = m;
61
+ return VALUE_READERS;
62
+ }
63
+
64
+ // A universal SET's required member order depends on a type the ANY does not carry: X.690
65
+ // sec. 11.6 orders a SET OF by the members' full encodings, while a structured SET is ordered
66
+ // by TAG (X.680 sec. 8.6), and the two differ whenever the constructed bit does (a SEQUENCE
67
+ // member, tag 16, sorts before a PrintableString, tag 19, by tag but after it by octets). A
68
+ // structured SET cannot repeat a tag, so a repeated tag proves SET OF and the octet rule binds;
69
+ // with all-distinct tags either reading is possible, so accept a value that satisfies EITHER
70
+ // (rejecting only what is non-canonical under both readings: sound in both directions, never a
71
+ // guess).
72
+ //
73
+ // KNOWN LIMITATION, and the two consumers do not weigh it identically. The either-reading rule
74
+ // admits a SET OF that is tag-ordered but not octet-ordered, which is not canonical DER. On a
75
+ // RECONSTRUCT path the input already exists and the alternative would refuse a valid structured
76
+ // SET, so accepting is the sound direction. On a SIGNING path the caller composes the bytes and
77
+ // could be held to octet order, so the same permissiveness lets a signer emit a non-canonical
78
+ // SET under a real signature. Tightening it for signers alone would mean a mode flag on a
79
+ // security posture, which this family avoids, and tightening it for both would reject valid
80
+ // structured SETs that c509 round-trips today. The rule is therefore unchanged and the cost is
81
+ // written down rather than discovered later.
82
+ var TAG_CLASS_RANK = { universal: 0, application: 1, context: 2, private: 3 };
83
+ function setOrderOk(kids) {
84
+ var i, dup = false, seen = {};
85
+ for (i = 0; i < kids.length; i++) {
86
+ var key = kids[i].tagClass + ":" + kids[i].tagNumber;
87
+ if (seen[key]) { dup = true; break; }
88
+ seen[key] = true;
89
+ }
90
+ var octetAsc = true, tagAsc = true;
91
+ for (i = 1; i < kids.length; i++) {
92
+ if (Buffer.compare(kids[i - 1].bytes, kids[i].bytes) > 0) octetAsc = false;
93
+ // Tag order ranks by CLASS first (universal < application < context < private, X.680
94
+ // sec. 8.6), then by tag number. Compare the class's NUMBER: the names do not sort in
95
+ // class order.
96
+ var pc = TAG_CLASS_RANK[kids[i - 1].tagClass], cc = TAG_CLASS_RANK[kids[i].tagClass];
97
+ if (pc !== cc ? pc > cc : kids[i - 1].tagNumber > kids[i].tagNumber) tagAsc = false;
98
+ }
99
+ return dup ? octetAsc : (octetAsc || tagAsc);
100
+ }
101
+
102
+ // element(node, E, code, label). Strict-validate an already-decoded DER element at ANY depth.
103
+ // Rejects the reserved EOC tag 0; runs a universal primitive through its strict content reader
104
+ // (or refuses a type with none); recurses into a constructed element's children; holds a
105
+ // universal SET to a canonical order. Only SEQUENCE and SET are accepted as universal
106
+ // CONSTRUCTED types -- an EXTERNAL / EMBEDDED PDV / CHARACTER STRING has mandatory components
107
+ // this gate cannot verify (the degenerate empty form is not a valid encoding of any of them),
108
+ // so it is refused for the same reason an unvalidatable primitive is.
109
+ //
110
+ // @enforced-by guard-shape-reinlined
111
+ // @guard-shape _ANY_VALUE_READERS\[
112
+ function element(node, E, code, label) {
113
+ if (node.tagClass === "universal" && node.tagNumber === 0) {
114
+ throw E(code, label + " must not use the reserved end-of-contents encoding (tag 0)");
115
+ }
116
+ var T = _asn1().TAGS;
117
+ if (node.constructed) {
118
+ if (node.tagClass === "universal" && node.tagNumber !== T.SEQUENCE && node.tagNumber !== T.SET) {
119
+ throw E(code, label + " of universal constructed type " + node.tagNumber + " has no strict DER structure validator here");
120
+ }
121
+ var kids = node.children; // asn1.decode always sets a (possibly empty) children array
122
+ for (var i = 0; i < kids.length; i++) element(kids[i], E, code, label);
123
+ if (node.tagClass === "universal" && node.tagNumber === T.SET && !setOrderOk(kids)) {
124
+ throw E(code, label + " has a SET whose members are in no canonical DER order (X.690 sec. 11.6 / X.680 sec. 8.6)");
125
+ }
126
+ return;
127
+ }
128
+ if (node.tagClass === "universal") {
129
+ var reader = _readers()[node.tagNumber];
130
+ if (!reader) throw E(code, label + " of universal type " + node.tagNumber + " has no strict DER content validator here");
131
+ try { reader(node); }
132
+ catch (e) { throw E(code, label + " is not a valid DER element for its type", e); }
133
+ }
134
+ }
135
+
136
+ // tlv(content, E, code, label) -> content. Raw ANY bytes about to be spliced verbatim must be
137
+ // exactly one non-empty, well-formed AND strictly-valid DER element: framing + no-trailing-data
138
+ // via asn1.decode, then content / structure / SET order via `element`. Returns the bytes so a
139
+ // call site can wrap a splice inline.
140
+ //
141
+ // @enforced-by guard-shape-reinlined
142
+ // @guard-shape must be exactly one well-formed DER element
143
+ function tlv(content, E, code, label) {
144
+ if (!content || content.length === 0) throw E(code, label + " must be a non-empty DER element");
145
+ var node;
146
+ try { node = _asn1().decode(content); }
147
+ catch (e) { throw E(code, label + " must be exactly one well-formed DER element (no trailing data)", e); }
148
+ element(node, E, code, label);
149
+ return content;
150
+ }
151
+
152
+ module.exports = { element: element, tlv: tlv };
@@ -10,28 +10,28 @@
10
10
  // guard-encoding -- strict decode of an untrusted base64 / base64url / hex text
11
11
  // to bytes. Node's Buffer.from(x, "base64"|"base64url"|"hex") is LENIENT: it
12
12
  // silently drops the first invalid character (and everything after a stray one),
13
- // accepts non-canonical trailing bits, and tolerates missing/extra padding, so
14
- // two distinct texts alias one byte string -- or a malformed text decodes to a
15
- // SHORTER, different value than intended. And it allocates the whole decode
16
- // before any size is checked. This is CWE-172 (encoding-transformation error) /
13
+ // accepts non-canonical trailing bits, and tolerates missing/extra padding. Two
14
+ // distinct texts can therefore alias one byte string, and a malformed text can
15
+ // decode to a shorter, different value than intended. And it allocates the whole
16
+ // decode before any size is checked. This is CWE-172 (encoding-transformation error) /
17
17
  // CWE-20 (canonicalization malleability) + CWE-770 (allocate-before-cap): RFC
18
18
  // 4648 sec. 3.5 / sec. 5 and RFC 8555 sec. 6.1 require the UNIQUE canonical
19
19
  // encoding. Each decoder here gates the alphabet, rejects an impossible length,
20
- // enforces the byte cap BEFORE the copy, decodes, and verifies canonicality by a
21
- // re-encode round-trip -- so a value that is not the one canonical encoding of
20
+ // enforces the byte cap before the copy, decodes, and verifies canonicality by a
21
+ // re-encode round-trip, so a value that is not the one canonical encoding of
22
22
  // its bytes fails closed with the caller's typed error.
23
23
  //
24
24
  // E is the caller's (code, message) typed-error factory; `code` the frozen
25
25
  // domain/reason; `label` the field phrase; `maxBytes` an optional decoded-size
26
26
  // cap (null/undefined = uncapped, for a PEM cert body already bounded upstream).
27
27
 
28
- // The alphabets are TABLES, walked one character at a time, rather than regular
29
- // expressions. A guard runs on the most hostile input the toolkit sees, and a
30
- // pattern engine's cost on a non-matching string is a property of the pattern
31
- // rather than of the length -- the one thing a bound cannot be placed on from the
32
- // outside. A table lookup is one array index per character and its cost is the
33
- // length, which the caller already caps. It is also the more honest statement of
34
- // the rule: the set of permitted characters IS the rule, written out.
28
+ // The alphabets are tables, walked one character at a time. Regular expressions
29
+ // would be the obvious alternative, but a guard runs on the most hostile input
30
+ // the toolkit sees, and a pattern engine's cost on a non-matching string is
31
+ // decided by the pattern, which is the one thing a bound cannot be placed on from
32
+ // the outside. A table lookup is one array index per character, so its cost
33
+ // follows the length, which the caller already caps. It is also the more honest
34
+ // statement of the rule: the set of permitted characters is the rule, written out.
35
35
  function _alphabet(chars) {
36
36
  var t = new Uint8Array(128);
37
37
  for (var i = 0; i < chars.length; i++) t[chars.charCodeAt(i)] = 1;
@@ -43,8 +43,8 @@ var B64_ALPHABET = _alphabet(UPPER + LOWER + DIGITS + "+/");
43
43
  var HEX_ALPHABET = _alphabet(DIGITS + "abcdef" + "ABCDEF");
44
44
 
45
45
  // Every character of `text` is in `table`. A code point outside Latin-1's low half
46
- // (including every astral half) is outside all three alphabets, so the table bound
47
- // is the reject rather than an index that reads undefined.
46
+ // (including every astral half) is outside all three alphabets, and the explicit
47
+ // bound rejects it, so no lookup can read undefined off the end of the table.
48
48
  function _inAlphabet(text, table) {
49
49
  for (var i = 0; i < text.length; i++) {
50
50
  var c = text.charCodeAt(i);
@@ -20,9 +20,9 @@
20
20
  var C = require("./constants");
21
21
 
22
22
  // assertField(name, value, E, code) -> value | throws new E(code, ...)
23
- // name: RFC 5322 ftext -- printable ASCII [0x21, 0x7e] except ':' (0x3a), non-empty
23
+ // name: RFC 5322 ftext, printable ASCII [0x21, 0x7e] except ':' (0x3a), non-empty
24
24
  // (a space, control byte, or ':' in a field name breaks the "Name: value" grammar).
25
- // value: reject CR (0x0d) / LF (0x0a) / NUL (0x00) -- the injection bytes; TAB
25
+ // value: reject CR (0x0d), LF (0x0a) and NUL (0x00), the injection bytes; TAB
26
26
  // (0x09, folding whitespace) and printable non-ASCII (a UTF-8 header value, RFC
27
27
  // 6532) pass. E is the caller's typed-error CLASS, thrown `new E(code, message)`
28
28
  // -- the same convention lib/mime.js (this guard's caller) and guard.bytes/text use.
@@ -42,7 +42,7 @@ function assertField(name, value, E, code) {
42
42
  if (vc === 0x00 || vc === 0x0d || vc === 0x0a) throw new E(code, "a header field value must not contain CR / LF / NUL (header injection) in " + JSON.stringify(name));
43
43
  }
44
44
  // RFC 5322 sec. 2.1.1: the serialized "Name: value" line (UTF-8 octets, excluding CRLF) must not exceed
45
- // the line limit, or a relay may re-fold it -- silently changing the signed bytes of a protected header.
45
+ // the line limit, or a relay may re-fold it and silently change the signed bytes of a protected header.
46
46
  if (Buffer.byteLength(name, "utf8") + 2 + Buffer.byteLength(v, "utf8") > C.LIMITS.HEADER_LINE_MAX_OCTETS) {
47
47
  throw new E(code, "a header field line exceeds RFC 5322's " + C.LIMITS.HEADER_LINE_MAX_OCTETS + "-octet limit: " + JSON.stringify(name));
48
48
  }
@@ -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
  };