@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.
- package/CHANGELOG.md +418 -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 +5 -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-der.js +152 -0
- 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 +48 -20
- 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 +149 -197
- 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-der.js
ADDED
|
@@ -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 };
|
package/lib/guard-encoding.js
CHANGED
|
@@ -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
|
|
14
|
-
//
|
|
15
|
-
//
|
|
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
|
|
21
|
-
// re-encode round-trip
|
|
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
|
|
29
|
-
//
|
|
30
|
-
// pattern engine's cost on a non-matching string is
|
|
31
|
-
//
|
|
32
|
-
// outside. A table lookup is one array index per character
|
|
33
|
-
// length, which the caller already caps. It is also the more honest
|
|
34
|
-
// the rule: the set of permitted characters
|
|
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,
|
|
47
|
-
//
|
|
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);
|
package/lib/guard-header.js
CHANGED
|
@@ -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
|
|
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)
|
|
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
|
|
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
|
}
|
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
|
};
|