@blamejs/pki 0.5.8 → 0.5.10
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 +43 -18
- package/README.md +1 -1
- package/lib/acme.js +1 -1
- package/lib/cmc-build.js +5 -5
- package/lib/cmc-verify.js +7 -7
- package/lib/cms-decrypt.js +2 -2
- package/lib/cms-verify.js +2 -2
- package/lib/constants.js +1 -1
- package/lib/est.js +3 -3
- package/lib/guard-all.js +14 -12
- package/lib/guard-bytes.js +4 -4
- package/lib/guard-compress.js +2 -2
- package/lib/guard-der.js +152 -0
- package/lib/guard-identifier.js +3 -3
- package/lib/guard-limits.js +2 -2
- package/lib/guard-parsed.js +1 -1
- package/lib/guard-range.js +1 -1
- package/lib/guard-secret.js +1 -1
- package/lib/guard-text.js +1 -1
- package/lib/hpke.js +1 -1
- package/lib/http-digest.js +1 -1
- package/lib/jose.js +1 -1
- package/lib/lint.js +1 -1
- package/lib/ocsp.js +1 -1
- package/lib/oid.js +1 -1
- package/lib/path-validate.js +5 -5
- package/lib/pki-build.js +27 -1
- package/lib/schema-c509.js +10 -60
- package/lib/schema-cmp.js +1 -1
- package/lib/schema-crl.js +1 -1
- package/lib/schema-engine.js +2 -2
- package/lib/schema-pkix.js +1 -1
- package/lib/smime.js +1 -1
- package/lib/validator-cose.js +3 -3
- package/lib/webauthn-mds.js +12 -12
- package/lib/webauthn.js +11 -11
- package/lib/webcrypto.js +1 -1
- package/package.json +4 -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-initialization 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-identifier.js
CHANGED
|
@@ -84,9 +84,9 @@ function assertCanonicalOid(str, E, code, label, boundsCode) {
|
|
|
84
84
|
// The shape this replaces was hand-written in a dozen callers, and it is worth one home because
|
|
85
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
|
-
// looser
|
|
87
|
+
// looser behavior and no error. Two details are easy to lose in a re-inline and are fixed here.
|
|
88
88
|
// `known` is consulted with hasOwnProperty and not a truthiness test, so an inherited Object
|
|
89
|
-
// member ("constructor", "toString") cannot read as a
|
|
89
|
+
// member ("constructor", "toString") cannot read as a recognized key; and the walk is over own
|
|
90
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
|
|
@@ -99,7 +99,7 @@ function assertCanonicalOid(str, E, code, label, boundsCode) {
|
|
|
99
99
|
//
|
|
100
100
|
// @enforced-by guard-shape-reinlined
|
|
101
101
|
// The shape requires the throw: an identical walk whose body filters on the same table (copying the
|
|
102
|
-
//
|
|
102
|
+
// recognized keys onward instead of rejecting the unrecognized 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-limits.js
CHANGED
|
@@ -90,7 +90,7 @@ function depthCap(value, key, dflt) {
|
|
|
90
90
|
function counter(max, E, code, label) {
|
|
91
91
|
// The ceiling is an authoring input: an undefined / NaN / fractional max
|
|
92
92
|
// builds a counter whose `n > max` never fires, leaving a silently dead fanout
|
|
93
|
-
//
|
|
93
|
+
// defense. 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");
|
|
96
96
|
}
|
|
@@ -117,7 +117,7 @@ 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, and the size
|
|
120
|
+
// max makes `length > max` never fire, and the size defense is then silently
|
|
121
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");
|
package/lib/guard-parsed.js
CHANGED
|
@@ -44,7 +44,7 @@ var bytes = require("./guard-bytes");
|
|
|
44
44
|
// ArrayBuffer. Testing `Buffer.isBuffer(x) || x instanceof Uint8Array` is narrower than what the
|
|
45
45
|
// verbs document, and the narrowness is invisible in the good case: an ArrayBuffer would fall
|
|
46
46
|
// through to the object branch and be refused as a rebuilt structure, which reads to the caller as
|
|
47
|
-
// "your certificate is malformed" when what happened is that their container was not
|
|
47
|
+
// "your certificate is malformed" when what happened is that their container was not recognized.
|
|
48
48
|
// guard.bytes is what finally reads them, and it accepts all four, so the door has to as well.
|
|
49
49
|
function _isBytes(x) {
|
|
50
50
|
return Buffer.isBuffer(x) || ArrayBuffer.isView(x) || x instanceof ArrayBuffer;
|
package/lib/guard-range.js
CHANGED
|
@@ -105,7 +105,7 @@ function uint64(value, E, code, label) {
|
|
|
105
105
|
// take. Accepts a bigint, or a Number that is a safe integer (isSafeInteger, not
|
|
106
106
|
// isInteger, because a Number above 2^53 has already lost precision by the time it
|
|
107
107
|
// arrives), so a large identifier MUST come as a bigint and cannot be silently
|
|
108
|
-
// rounded to a
|
|
108
|
+
// rounded to a neighbor. Everything else is a config-time reject.
|
|
109
109
|
//
|
|
110
110
|
// Distinct from int() on purpose: int() BOUNDS a value the wire decoded and narrows
|
|
111
111
|
// it to Number; this one is unbounded and returns BigInt, because the field has no
|
package/lib/guard-secret.js
CHANGED
|
@@ -72,7 +72,7 @@ function zeroize(value, ErrorClass, code, label) {
|
|
|
72
72
|
// @enforced-by behavioral -- this is a loop over zeroize, which carries the family's only
|
|
73
73
|
// rename-proof shape (the `.fill(0)` above). It introduces no shape of its own, so a lexical
|
|
74
74
|
// detector here would anchor on a renameable symbol and go silently green (drift rule sec. 3).
|
|
75
|
-
// The
|
|
75
|
+
// The behavioral guards are guard-secret.test.js (holes tolerated, every member cleared) and the
|
|
76
76
|
// CMS vectors that assert the shared secret and KEK are wiped on both the success and failure paths.
|
|
77
77
|
function zeroizeAll(list, ErrorClass, code, label) {
|
|
78
78
|
if (!list) return list;
|
package/lib/guard-text.js
CHANGED
|
@@ -50,7 +50,7 @@ function decode(input, maxBytes, ErrorClass, spec) {
|
|
|
50
50
|
if (Buffer.isBuffer(input)) {
|
|
51
51
|
// Re-view through the byte guard first so a detached backing ArrayBuffer (a
|
|
52
52
|
// transferred / structuredClone'd Buffer, which reads as zero-length) fails
|
|
53
|
-
// closed here -- the same detached-buffer
|
|
53
|
+
// closed here -- the same detached-buffer defense the byte boundaries get --
|
|
54
54
|
// instead of being decoded as an empty string. Then cap, then decode.
|
|
55
55
|
input = bytes.view(input, ErrorClass, spec.badInput, spec.label);
|
|
56
56
|
if (input.length > maxBytes) throw new ErrorClass(spec.tooLarge, spec.label + " exceeds the size cap");
|
package/lib/hpke.js
CHANGED
|
@@ -390,7 +390,7 @@ function _recipPrivate(suite, sk) {
|
|
|
390
390
|
// psk / auth / auth-psk. An unknown mode must fail closed, never key-schedule
|
|
391
391
|
// with an out-of-registry mode byte.
|
|
392
392
|
// The sender and the recipient read the same option object from OPPOSITE ends, so each direction
|
|
393
|
-
// gets its own table. A shared union
|
|
393
|
+
// gets its own table. A shared union recognizes every name at both ends and so accepts the one that
|
|
394
394
|
// cannot do anything there: `senderPublicKey` handed to setupS, or
|
|
395
395
|
// `senderKey` handed to setupR, is silently ignored, which is the exact silence these tables exist
|
|
396
396
|
// to remove, in a wider form. An option that means nothing where it was passed is a misunderstanding
|
package/lib/http-digest.js
CHANGED
|
@@ -144,7 +144,7 @@ function _validateDigest(paramText, E, code) {
|
|
|
144
144
|
? p.domain.value.replace(/^\s+|\s+$/g, "").split(/\s+/) : null;
|
|
145
145
|
// charset (RFC 7616 sec. 3.3): the one permitted value is the unquoted token "UTF-8". A quoted charset, or any
|
|
146
146
|
// other value, is malformed, since answering it would hash the credentials in the wrong encoding, so it is
|
|
147
|
-
// rejected (and thus skipped during multi-offer selection in
|
|
147
|
+
// rejected (and thus skipped during multi-offer selection in favor of a conforming offer).
|
|
148
148
|
if (p.charset && (p.charset.quoted || String(p.charset.value).toUpperCase() !== "UTF-8")) throw E(code, "the Digest charset must be the unquoted token UTF-8 (RFC 7616 sec. 3.3)");
|
|
149
149
|
// stale (RFC 7616 sec. 3.3) is the unquoted token "true" or "false". A quoted or otherwise invalid value
|
|
150
150
|
// (stale=maybe) is malformed and rejected (thus skipped in selection), never parsed as a false flag that
|
package/lib/jose.js
CHANGED
|
@@ -368,7 +368,7 @@ function assertPublicJwk(jwk) {
|
|
|
368
368
|
// `key` NAMES the key this message must be signed under and `profile` selects which header rules
|
|
369
369
|
// apply -- both narrow what verifies, so a misspelling of either silently widens it back to the
|
|
370
370
|
// default. That is the shape this check exists for: the caller asked for something stricter and got
|
|
371
|
-
// the looser
|
|
371
|
+
// the looser behavior, with nothing said.
|
|
372
372
|
var _VERIFY_KEYS = { key: 1, profile: 1 };
|
|
373
373
|
async function verify(jws, opts) {
|
|
374
374
|
opts = opts || {};
|
package/lib/lint.js
CHANGED
|
@@ -231,7 +231,7 @@ function _subjectCNs(cert) {
|
|
|
231
231
|
return out;
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
-
// An extension's criticality is
|
|
234
|
+
// An extension's criticality is honored (RFC 5280 4.2: a consumer MUST reject a critical extension
|
|
235
235
|
// it cannot process) only when this toolkit actually processes its semantics. That authority is
|
|
236
236
|
// path-validate's PROCESSED_EXTENSIONS (the RFC 5280 sec. 6.1 path-processing set), never the decoder
|
|
237
237
|
// table: certExtensionDecoders also decodes many extensions purely for display (qcStatements, the MS
|
package/lib/ocsp.js
CHANGED
|
@@ -530,7 +530,7 @@ function verify(response, opts) {
|
|
|
530
530
|
// the closed direction for `revoked`: revocation does not expire the way
|
|
531
531
|
// non-revocation does, so discarding a signed, current, authorized revoked
|
|
532
532
|
// verdict because it was replayed would hand a soft-fail caller the certificate
|
|
533
|
-
// the responder just refused -- turning the anti-replay
|
|
533
|
+
// the responder just refused -- turning the anti-replay defense into the thing
|
|
534
534
|
// that accepts a revoked certificate. `nonceMatched: false` still reports that
|
|
535
535
|
// this response was not bound to this request.
|
|
536
536
|
if (!matched && verdict.status === "good") {
|
package/lib/oid.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* attribute types, `2.5.29` for the extensions, `2.16.840.1.101.3.4` for
|
|
23
23
|
* the NIST algorithms), and each member names only its trailing arc. The
|
|
24
24
|
* full OID is derived from base + leaf at load, so the arc hierarchy that
|
|
25
|
-
* IS the OID namespace is
|
|
25
|
+
* IS the OID namespace is modeled directly instead of re-spelled per
|
|
26
26
|
* entry. It covers the RFC 5280 attribute types and extensions, the
|
|
27
27
|
* classical signature / public-key / digest algorithms, and the
|
|
28
28
|
* NIST-assigned post-quantum arcs (ML-DSA, ML-KEM, SLH-DSA). Operators
|
package/lib/path-validate.js
CHANGED
|
@@ -306,7 +306,7 @@ function resolveDescriptor(sigAlg) {
|
|
|
306
306
|
// defense). For the one-shot families whose public key shares the signature OID
|
|
307
307
|
// (EdDSA, ML-DSA, SLH-DSA), Node's WebCrypto imports an SPKI of a DIFFERENT type
|
|
308
308
|
// under the requested name and verifies with the real key, so an Ed25519-signed
|
|
309
|
-
// certificate
|
|
309
|
+
// certificate labeled SLH-DSA would otherwise validate. Enforce structurally:
|
|
310
310
|
// the issuer SPKI's algorithm OID MUST equal the signature algorithm OID. (For
|
|
311
311
|
// RSA/ECDSA -- different key vs signature OIDs -- WebCrypto's import already rejects
|
|
312
312
|
// a mismatched key type, so `sameKeyOid` is not set and this is a no-op.)
|
|
@@ -1753,7 +1753,7 @@ function classifyCrls(parsed) {
|
|
|
1753
1753
|
// sec. 5.2.4 @3447 makes deltaCRLIndicator a MUST-be-critical extension. A non-critical one
|
|
1754
1754
|
// is non-conforming, so it does not earn the NEW capability of being merged -- merging can
|
|
1755
1755
|
// RELEASE a certificate, and that must rest on a conforming indicator. It is still classified
|
|
1756
|
-
// as a delta and still consulted for revocation, which is the shipped
|
|
1756
|
+
// as a delta and still consulted for revocation, which is the shipped behavior and the
|
|
1757
1757
|
// conservative direction.
|
|
1758
1758
|
if (crlNumberWithinBound(n) && deltaCritical) { rec.baseCrlNumber = n; rec.mergeable = true; }
|
|
1759
1759
|
} catch (_e) {
|
|
@@ -2393,7 +2393,7 @@ function ocspChecker(responses) {
|
|
|
2393
2393
|
* `pki.schema.ocsp.parseResponse` result. A REBUILT parsed response is refused. A
|
|
2394
2394
|
* signature check has three parts -- the signature, the algorithm that verifies it,
|
|
2395
2395
|
* and the bytes it covers -- and on a parsed object all three are separate properties:
|
|
2396
|
-
* a genuine CA signature over a certificate that CA issued,
|
|
2396
|
+
* a genuine CA signature over a certificate that CA issued, relabeled, verifies as a
|
|
2397
2397
|
* ResponseData signature for a response that never existed. The parser marks what it
|
|
2398
2398
|
* returns, so those three are known to have been derived together from one byte
|
|
2399
2399
|
* string; `Object.assign`, spread and a JSON round-trip all drop the mark, which is
|
|
@@ -2795,7 +2795,7 @@ async function _fetchAiaIssuers(current, aia) {
|
|
|
2795
2795
|
for (var c = 0; c < certs.length; c++) {
|
|
2796
2796
|
var parsed;
|
|
2797
2797
|
try { parsed = coerceCert(certs[c]); }
|
|
2798
|
-
catch (_e3) { /* allow:swallow-unverified verified-unreachable: every cert here already passed the IDENTICAL x509.parse in _aiaParseBody (single-DER validates `body`; certs-only validates each via parseCertsOnly), so coerceCert re-parsing the same bytes cannot throw -- the guard stays as
|
|
2798
|
+
catch (_e3) { /* allow:swallow-unverified verified-unreachable: every cert here already passed the IDENTICAL x509.parse in _aiaParseBody (single-DER validates `body`; certs-only validates each via parseCertsOnly), so coerceCert re-parsing the same bytes cannot throw -- the guard stays as defense-in-depth */ continue; }
|
|
2799
2799
|
out.push(parsed);
|
|
2800
2800
|
}
|
|
2801
2801
|
}
|
|
@@ -3111,7 +3111,7 @@ module.exports = {
|
|
|
3111
3111
|
// The set of extension OIDs whose CRITICAL semantics this validator processes (RFC 5280 sec. 6.1).
|
|
3112
3112
|
// Exposed so a linter can distinguish "processed" from "merely decoded" and stay consistent with
|
|
3113
3113
|
// the path-validation verdict on a critical extension -- a decoder in certExtensionDecoders is NOT
|
|
3114
|
-
// by itself proof the criticality is
|
|
3114
|
+
// by itself proof the criticality is honored. Both sets are frozen so a caller cannot mutate them.
|
|
3115
3115
|
PROCESSED_EXTENSIONS: PROCESSED_EXTENSIONS,
|
|
3116
3116
|
// Extensions that ARE processed for an intermediate CA but are unprocessed on the target/leaf, so a
|
|
3117
3117
|
// critical instance on the target fails closed (RFC 5280 sec. 6.1.5(f)) -- policyMappings is
|
package/lib/pki-build.js
CHANGED
|
@@ -129,7 +129,33 @@ function makeBuilder(ctx) {
|
|
|
129
129
|
if (!Buffer.isBuffer(ipBuf) || (ipBuf.length !== 4 && ipBuf.length !== 16)) throw E("bad-input", "iPAddress must be a 4- or 16-octet Buffer or an IPv4/IPv6 string");
|
|
130
130
|
return b.contextPrimitive(7, ipBuf);
|
|
131
131
|
case "directoryName": return b.explicit(4, encodeName(v)); // Name is a CHOICE -> the context tag is EXPLICIT
|
|
132
|
-
|
|
132
|
+
case "otherName":
|
|
133
|
+
// otherName ::= SEQUENCE { type-id OBJECT IDENTIFIER, value [0] EXPLICIT ANY }, tagged
|
|
134
|
+
// [0] IMPLICIT (RFC 5280 sec. 4.2.1.6). The value is EXPLICIT because ANY carries no tag
|
|
135
|
+
// of its own, so the wrapper is what makes the encoding unambiguous -- the same shape the
|
|
136
|
+
// decoder requires, and without it an SmtpUTF8Mailbox (RFC 8398 sec. 3) could be read but
|
|
137
|
+
// never written.
|
|
138
|
+
if (typeof v !== "object" || Buffer.isBuffer(v)) throw E("bad-input", "otherName must be an object { typeId, value }");
|
|
139
|
+
if (typeof v.typeId !== "string" || !v.typeId) throw E("bad-input", "otherName requires a `typeId` OID string");
|
|
140
|
+
if (!Buffer.isBuffer(v.value) || v.value.length === 0) {
|
|
141
|
+
throw E("bad-input", "otherName requires a `value` Buffer holding one DER element");
|
|
142
|
+
}
|
|
143
|
+
// The value is spliced in raw and this verb SIGNS the result, so it is strictly
|
|
144
|
+
// validated rather than taken on the caller's word. Framing alone is not DER: a
|
|
145
|
+
// BOOLEAN whose content octet is 0x01, a NumericString holding "@", or a SET in no
|
|
146
|
+
// canonical order all frame cleanly and would ship inside a [0] EXPLICIT wrapper
|
|
147
|
+
// under a real signature. guard.der.tlv is the one place that rule lives.
|
|
148
|
+
guard.der.tlv(v.value, E, "bad-input", "otherName `value`");
|
|
149
|
+
// [0] IMPLICIT on a SEQUENCE replaces the SEQUENCE tag, so the content is the two
|
|
150
|
+
// members concatenated -- build.contextConstructed takes content bytes, not elements.
|
|
151
|
+
// The type-id encode is wrapped so a malformed OID reports in the CALLER's namespace
|
|
152
|
+
// (x509/bad-input, csr/bad-input, ...) like every other raw-OID path here, rather than
|
|
153
|
+
// surfacing the codec's own oid/* code from a shared builder the caller never named.
|
|
154
|
+
var typeIdDer;
|
|
155
|
+
try { typeIdDer = b.oid(v.typeId); }
|
|
156
|
+
catch (e) { throw E("bad-input", "invalid otherName type-id OID " + JSON.stringify(v.typeId) + " (violates the X.660 arc bounds)", e); }
|
|
157
|
+
return b.contextConstructed(0, Buffer.concat([typeIdDer, b.explicit(0, v.value)]));
|
|
158
|
+
default: throw E("bad-input", "unsupported GeneralName form " + JSON.stringify(k) + " (supported: rfc822Name, dNSName, uniformResourceIdentifier, iPAddress, directoryName, otherName)");
|
|
133
159
|
}
|
|
134
160
|
}
|
|
135
161
|
|
package/lib/schema-c509.js
CHANGED
|
@@ -1594,17 +1594,9 @@ function _extValueFromDer(name, der) {
|
|
|
1594
1594
|
// and RFC 3161 timestamping, and must not creep into a third consumer). On the ENCODE path such a value simply
|
|
1595
1595
|
// degrades to the byte-exact ~oid + byte-string form, losing nothing; on the DECODE path it is a fail-closed
|
|
1596
1596
|
// verdict.
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
m[T.BIT_STRING] = R.bitString; m[T.OCTET_STRING] = R.octetString; m[T.NULL] = R.nullValue;
|
|
1601
|
-
m[T.OBJECT_IDENTIFIER] = R.oid; m[T.UTC_TIME] = R.time; m[T.GENERALIZED_TIME] = R.time;
|
|
1602
|
-
// NumericString reads through its own reader: it is not a DirectoryString type, and routing it through
|
|
1603
|
-
// read.string would fold it into the RFC 5280 sec. 7.1 name-comparison identity class (see asn1-der.js).
|
|
1604
|
-
m[T.NUMERIC_STRING] = R.numericString;
|
|
1605
|
-
[T.UTF8_STRING, T.PRINTABLE_STRING, T.IA5_STRING, T.TELETEX_STRING, T.VISIBLE_STRING, T.BMP_STRING, T.UNIVERSAL_STRING].forEach(function (t) { m[t] = R.string; });
|
|
1606
|
-
return m;
|
|
1607
|
-
})();
|
|
1597
|
+
// The per-type strict content readers, the SET canonical-order rule and the recursive element
|
|
1598
|
+
// walk all moved to guard.der, so this module and pki-build cannot drift on what "one strictly
|
|
1599
|
+
// valid DER element" means.
|
|
1608
1600
|
|
|
1609
1601
|
// A universal SET's required member order depends on a type the ANY does not carry: X.690 sec. 11.6 orders a
|
|
1610
1602
|
// SET OF by the members' full encodings, while a structured SET is ordered by TAG (X.680 sec. 8.6), and the two
|
|
@@ -1612,24 +1604,6 @@ var _ANY_VALUE_READERS = (function () {
|
|
|
1612
1604
|
// tag but after it by octets). A structured SET cannot repeat a tag, so a repeated tag proves SET OF and the
|
|
1613
1605
|
// octet rule binds; with all-distinct tags either reading is possible, so accept a value that satisfies EITHER
|
|
1614
1606
|
// (rejecting only what is non-canonical under both readings: sound in both directions, never a guess).
|
|
1615
|
-
var _TAG_CLASS_RANK = { universal: 0, application: 1, context: 2, private: 3 };
|
|
1616
|
-
function _setOrderOk(kids) {
|
|
1617
|
-
var i, dup = false, seen = {};
|
|
1618
|
-
for (i = 0; i < kids.length; i++) {
|
|
1619
|
-
var key = kids[i].tagClass + ":" + kids[i].tagNumber;
|
|
1620
|
-
if (seen[key]) { dup = true; break; }
|
|
1621
|
-
seen[key] = true;
|
|
1622
|
-
}
|
|
1623
|
-
var octetAsc = true, tagAsc = true;
|
|
1624
|
-
for (i = 1; i < kids.length; i++) {
|
|
1625
|
-
if (Buffer.compare(kids[i - 1].bytes, kids[i].bytes) > 0) octetAsc = false;
|
|
1626
|
-
// Tag order ranks by CLASS first (universal < application < context < private, X.680 sec. 8.6), then by tag
|
|
1627
|
-
// number. Compare the class's NUMBER: the names do not sort in class order.
|
|
1628
|
-
var pc = _TAG_CLASS_RANK[kids[i - 1].tagClass], cc = _TAG_CLASS_RANK[kids[i].tagClass];
|
|
1629
|
-
if (pc !== cc ? pc > cc : kids[i - 1].tagNumber > kids[i].tagNumber) tagAsc = false;
|
|
1630
|
-
}
|
|
1631
|
-
return dup ? octetAsc : (octetAsc || tagAsc);
|
|
1632
|
-
}
|
|
1633
1607
|
|
|
1634
1608
|
// Strict-validate a decoded DER element at ANY depth, in the caller's error domain. Rejects the reserved EOC tag
|
|
1635
1609
|
// 0; runs a universal primitive through its strict content reader (or rejects a type with none); recurses into a
|
|
@@ -1638,37 +1612,13 @@ function _setOrderOk(kids) {
|
|
|
1638
1612
|
// components this gate cannot verify (the degenerate empty form is not a valid encoding of any of them), so it is
|
|
1639
1613
|
// refused for the same reason an unvalidatable primitive is. A NON-universal element (a legitimately context- or
|
|
1640
1614
|
// application-tagged ANY) passes on its framing, but its constructed children are still walked.
|
|
1641
|
-
function _strictDerElement(node, code, label) {
|
|
1642
|
-
if (node.tagClass === "universal" && node.tagNumber === 0) throw _err(code, label + " must not use the reserved end-of-contents encoding (tag 0)");
|
|
1643
|
-
if (node.constructed) {
|
|
1644
|
-
if (node.tagClass === "universal" && node.tagNumber !== asn1.TAGS.SEQUENCE && node.tagNumber !== asn1.TAGS.SET) {
|
|
1645
|
-
throw _err(code, label + " of universal constructed type " + node.tagNumber + " has no strict DER structure validator here");
|
|
1646
|
-
}
|
|
1647
|
-
var kids = node.children; // asn1.decode always sets a (possibly empty) children array on a constructed node
|
|
1648
|
-
for (var i = 0; i < kids.length; i++) _strictDerElement(kids[i], code, label);
|
|
1649
|
-
if (node.tagClass === "universal" && node.tagNumber === asn1.TAGS.SET && !_setOrderOk(kids)) {
|
|
1650
|
-
throw _err(code, label + " has a SET whose members are in no canonical DER order (X.690 sec. 11.6 / X.680 sec. 8.6)");
|
|
1651
|
-
}
|
|
1652
|
-
return;
|
|
1653
|
-
}
|
|
1654
|
-
if (node.tagClass === "universal") {
|
|
1655
|
-
// Validate-or-reject: a universal primitive with no strict content reader is never accepted on framing alone
|
|
1656
|
-
// (asn1.decode frames a malformed NumericString "12 01 40" happily), so the map is exhaustive by refusal.
|
|
1657
|
-
var reader = _ANY_VALUE_READERS[node.tagNumber];
|
|
1658
|
-
if (!reader) throw _err(code, label + " of universal type " + node.tagNumber + " has no strict DER content validator here");
|
|
1659
|
-
try { reader(node); } catch (e) { throw _err(code, label + " is not a valid DER element for its type", e); }
|
|
1660
|
-
}
|
|
1661
|
-
}
|
|
1662
1615
|
// Raw ANY bytes about to be spliced verbatim must be exactly one non-empty, well-formed AND strictly-valid DER
|
|
1663
|
-
// element
|
|
1664
|
-
//
|
|
1665
|
-
//
|
|
1616
|
+
// element. The rule itself lives in guard.der -- framing + no-trailing-data, then content / structure / SET
|
|
1617
|
+
// order -- so this module and the certificate builder cannot drift apart on what "one DER element" means. This
|
|
1618
|
+
// stays as a named local because every reconstruct site here reads better naming the intent than the guard.
|
|
1619
|
+
|
|
1666
1620
|
function _requireStrictDerTlv(content, code, label) {
|
|
1667
|
-
|
|
1668
|
-
var node;
|
|
1669
|
-
try { node = asn1.decode(content); } catch (e) { throw _err(code, label + " must be exactly one well-formed DER element (no trailing data)", e); }
|
|
1670
|
-
_strictDerElement(node, code, label);
|
|
1671
|
-
return content;
|
|
1621
|
+
return guard.der.tlv(content, _err, code, label);
|
|
1672
1622
|
}
|
|
1673
1623
|
|
|
1674
1624
|
// asn1.build.set SORTS its members, so handing it a non-canonically-ordered values list would silently rewrite
|
|
@@ -1700,7 +1650,7 @@ function _sdaToDer(node, isNative) {
|
|
|
1700
1650
|
if (tname === undefined) throw _err("c509/bad-extensions", "a subjectDirectoryAttributes attribute type int " + ti + " has no C509 sec. 8.6 registry row");
|
|
1701
1651
|
// countryName / serialNumber carry a CHARACTER restriction (draft sec. 3.1.4 "SHALL contain only
|
|
1702
1652
|
// characters from the 74-character ASCII subset permitted by PrintableString"), and not a sign override --
|
|
1703
|
-
// _reconAttrValue asserts the charset and
|
|
1653
|
+
// _reconAttrValue asserts the charset and honors the declared string type. Requiring the negative sign
|
|
1704
1654
|
// here would also make these attributes unrepresentable in a NATIVE certificate, whose ints SHALL all be
|
|
1705
1655
|
// non-negative (same sec.), so the rule is the charset, not the sign.
|
|
1706
1656
|
for (var vi = 0; vi < valuesNode.children.length; vi++) {
|
|
@@ -1896,7 +1846,7 @@ function _reconAttrValue(rdn) {
|
|
|
1896
1846
|
if (rdn.type === "emailAddress") return b.ia5(s);
|
|
1897
1847
|
// serialNumber / countryName carry a CHARACTER restriction, not a string-type override: draft sec. 3.1.4
|
|
1898
1848
|
// "SHALL contain only characters from the 74-character ASCII subset permitted by PrintableString". Enforce
|
|
1899
|
-
// that on the CHARACTERS and still
|
|
1849
|
+
// that on the CHARACTERS and still honor the sign for the string type. Coercing them to PrintableString
|
|
1900
1850
|
// regardless of sign would make the +N and -N encodings of one value reconstruct IDENTICAL DER, so a single
|
|
1901
1851
|
// X.509 signature would cover two distinct C509 encodings (a malleability window in the type-3 transform).
|
|
1902
1852
|
// b.printable IS the PrintableString charset authority -- run it for the assert even when the sign selects
|
package/lib/schema-cmp.js
CHANGED
|
@@ -460,7 +460,7 @@ var CERT_RESPONSE = schema.seq([
|
|
|
460
460
|
// accepted (0) or grantedWithMods (1). Any other status (rejection, waiting,
|
|
461
461
|
// the revocation / keyUpdate warnings) denies or defers the request, so a
|
|
462
462
|
// certificate under it is a malformed response even when no explicit failInfo
|
|
463
|
-
// bit is set (a rejection is commonly
|
|
463
|
+
// bit is set (a rejection is commonly signaled by status alone). Keying the
|
|
464
464
|
// rule off failInfo presence alone would let a bare-rejection status ship a
|
|
465
465
|
// certificate (RFC 9810 sec. 5.3.4).
|
|
466
466
|
if (certifiedKeyPair !== null && status.status.code !== 0 && status.status.code !== 1) {
|
package/lib/schema-crl.js
CHANGED
|
@@ -41,7 +41,7 @@ var CRL_REASONS = pkix.CRL_REASON_NAMES;
|
|
|
41
41
|
|
|
42
42
|
// Extension-value decoding is keyed off the stable dotted OID (resolved once at
|
|
43
43
|
// load from the canonical name) and not the mutable display name, so a caller's
|
|
44
|
-
// pki.oid.register() display override cannot change parse
|
|
44
|
+
// pki.oid.register() display override cannot change parse behavior.
|
|
45
45
|
var OID_CRL_NUMBER = oid.byName("cRLNumber");
|
|
46
46
|
var OID_REASON_CODE = oid.byName("reasonCode");
|
|
47
47
|
var OID_INVALIDITY_DATE = oid.byName("invalidityDate");
|
package/lib/schema-engine.js
CHANGED
|
@@ -40,11 +40,11 @@ function _fail(ctx, code, message) {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
// ---- shape assertions ------------------------------------------------
|
|
43
|
-
// A schema's `assert` mode is a real
|
|
43
|
+
// A schema's `assert` mode is a real behavior-preservation control: some
|
|
44
44
|
// hand-written guards checked the universal SEQUENCE tag (algorithmIdentifier,
|
|
45
45
|
// Name), others checked only that the node had children (Validity, SPKI, the
|
|
46
46
|
// tbs body, an AttributeTypeAndValue). Collapsing them to one "is a SEQUENCE"
|
|
47
|
-
// check would silently change
|
|
47
|
+
// check would silently change behavior on SET-wrapped input, so each is kept.
|
|
48
48
|
|
|
49
49
|
function _assertShape(schema, node, ctx) {
|
|
50
50
|
var mode = schema.assert || "sequence";
|
package/lib/schema-pkix.js
CHANGED
|
@@ -1228,7 +1228,7 @@ function certExtensionDecoders(ns) {
|
|
|
1228
1228
|
|
|
1229
1229
|
// qcStatements ::= SEQUENCE OF QCStatement (RFC 3739 sec. 3.2.6). QCStatement ::= SEQUENCE {
|
|
1230
1230
|
// statementId OBJECT IDENTIFIER, statementInfo ANY DEFINED BY statementId OPTIONAL }. Known statementIds
|
|
1231
|
-
// (RFC 3739 id-qcs + the ETSI EN 319 412-5 esi4
|
|
1231
|
+
// (RFC 3739 id-qcs + the ETSI EN 319 412-5 esi4 catalogue) decode their statementInfo; an unknown statementId
|
|
1232
1232
|
// is preserved OPAQUE (the RFC 3739 open type), never rejected. The DEFINED-BY selection is an OID re-dispatch.
|
|
1233
1233
|
function _qcStr(node, tag, C, what) {
|
|
1234
1234
|
if (!node || node.tagClass !== "universal" || node.tagNumber !== tag) throw ns.E(C, what);
|
package/lib/smime.js
CHANGED
|
@@ -846,7 +846,7 @@ function _capped(msg) {
|
|
|
846
846
|
* compared under RFC 5280 sec. 7.5: the local-part exactly, the host-part case-insensitively. The
|
|
847
847
|
* address is read from the `subjectAltName` `rfc822Name` entries (RFC 8550 sec. 4.4.3) and, where the
|
|
848
848
|
* extension carries none, from the subject DN's PKCS #9 `emailAddress` attribute, which RFC 8550 sec. 3
|
|
849
|
-
* requires a receiving agent to
|
|
849
|
+
* requires a receiving agent to recognize. Where both are present the extension wins. Only a signer
|
|
850
850
|
* whose signature verified contributes an identity, so a tampered message cannot report a binding. `match` is THREE-valued and a caller enforcing sender binding tests
|
|
851
851
|
* `match === true` -- `false` means every identity was comparable and none matched, and `null` means the
|
|
852
852
|
* question was not answered (no `expectedSender` and no single outer `From`, a signer certificate
|
package/lib/validator-cose.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// validator-cose -- the SINGLE home for "is this a conformant WebAuthn credential
|
|
9
9
|
// COSE_Key" (RFC 9052 sec. 7 structure + RFC 9053 EC2/OKP/RSA key parameters + the
|
|
10
10
|
// CTAP2 canonical-CBOR profile WebAuthn sec. 6.5.1 imposes). Sibling to the guard
|
|
11
|
-
// family: where a guard owns a CVE-class fail-closed
|
|
11
|
+
// family: where a guard owns a CVE-class fail-closed defense once, a validator owns a
|
|
12
12
|
// decoded TYPE's COMPLETE conformance rule set once, so a format module composes the
|
|
13
13
|
// family instead of re-deriving a partial subset inline (the drift that leaks MUSTs
|
|
14
14
|
// out one review round at a time). Enforced by the validator-shape-reinlined
|
|
@@ -242,10 +242,10 @@ function assertKeyMaterial(key, E, code, unsupportedCode) {
|
|
|
242
242
|
// PROFILE: the declared alg must match the key type (and, for EC2, the curve).
|
|
243
243
|
var prof = ALG_PROFILE[String(key.alg)];
|
|
244
244
|
// An algorithm this verifier does not implement is not a malformed key. The key can be perfectly
|
|
245
|
-
// well-formed (the same bytes may parse under a
|
|
245
|
+
// well-formed (the same bytes may parse under a neighboring algorithm id), and a relying
|
|
246
246
|
// party migrating credential rows written elsewhere needs to tell "I cannot check this
|
|
247
247
|
// algorithm" from "these bytes are wrong", since only one of those is fixable by re-registering.
|
|
248
|
-
// Callers that do not distinguish the two pass one code and keep the previous
|
|
248
|
+
// Callers that do not distinguish the two pass one code and keep the previous behavior.
|
|
249
249
|
if (!prof) throw new E(unsupportedCode || code, "unsupported credential key algorithm " + key.alg);
|
|
250
250
|
if (prof.kty !== key.kty) throw bad("credential key algorithm " + key.alg + " is inconsistent with key type " + key.kty);
|
|
251
251
|
if (prof.crv != null && prof.crv !== key.crv) throw bad("credential key algorithm " + key.alg + " requires a different curve");
|