@blamejs/pki 0.1.22 → 0.1.23
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 +35 -0
- package/README.md +16 -6
- package/index.js +16 -12
- package/lib/asn1-der.js +126 -68
- package/lib/constants.js +46 -21
- package/lib/ct.js +54 -39
- package/lib/framework-error.js +57 -34
- package/lib/oid.js +110 -59
- package/lib/path-validate.js +302 -135
- package/lib/schema-all.js +49 -41
- package/lib/schema-attrcert.js +101 -64
- package/lib/schema-cmp.js +152 -131
- package/lib/schema-cms.js +621 -163
- package/lib/schema-crl.js +43 -21
- package/lib/schema-crmf.js +136 -79
- package/lib/schema-csr.js +63 -14
- package/lib/schema-engine.js +92 -43
- package/lib/schema-ocsp.js +101 -55
- package/lib/schema-pkcs12.js +80 -64
- package/lib/schema-pkcs8.js +12 -12
- package/lib/schema-pkix.js +283 -100
- package/lib/schema-smime.js +39 -39
- package/lib/schema-tsp.js +108 -59
- package/lib/schema-x509.js +36 -25
- package/lib/webcrypto.js +156 -22
- package/package.json +3 -2
- package/sbom.cdx.json +6 -6
package/lib/schema-pkix.js
CHANGED
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
// Copyright (c) blamejs contributors
|
|
3
3
|
"use strict";
|
|
4
4
|
//
|
|
5
|
-
// @internal
|
|
6
|
-
// parsers that compose these factories (pki.schema.x509, pki.schema.crl,
|
|
5
|
+
// @internal -- no operator-facing namespace. The documented surface is the
|
|
6
|
+
// parsers that compose these factories (pki.schema.x509, pki.schema.crl, ...).
|
|
7
7
|
//
|
|
8
8
|
// Shared PKIX structure-schema factories (RFC 5280). Each is a namespace-
|
|
9
9
|
// parameterized FACTORY: given an error namespace `ns` ({ prefix, E, oid }) it
|
|
10
10
|
// returns an asn1-schema that walks the corresponding ASN.1 structure and emits
|
|
11
11
|
// the caller's own <prefix>/* error codes. x509.js, crl.js, and future CMS/CSR
|
|
12
12
|
// parsers compose these so AlgorithmIdentifier / Name / Extension are defined
|
|
13
|
-
// once, not re-derived per format. This module is internal infrastructure
|
|
13
|
+
// once, not re-derived per format. This module is internal infrastructure -- the
|
|
14
14
|
// operator-facing surface is the parsers that consume it.
|
|
15
15
|
|
|
16
16
|
var asn1 = require("./asn1-der");
|
|
@@ -30,8 +30,19 @@ var PEM_RE = /-----BEGIN ([A-Z0-9 ]+)-----([\s\S]*?)-----END \1-----/;
|
|
|
30
30
|
|
|
31
31
|
// pemDecode(text, label, PemError): `label` (when truthy) is enforced, else the
|
|
32
32
|
// first block is taken. Applies the LIMITS.PEM_MAX_BYTES cap before scanning.
|
|
33
|
+
// The body must be CANONICAL base64 (RFC 4648 sec. 3.5): whole 4-character
|
|
34
|
+
// groups, no stray padding, zero trailing bits in the final symbol. Node's
|
|
35
|
+
// lenient Buffer.from would otherwise let several distinct PEM texts alias one
|
|
36
|
+
// DER (PEM-layer malleability) and silently drop trailing content bits.
|
|
33
37
|
function pemDecode(text, label, PemError) {
|
|
34
|
-
|
|
38
|
+
// The size cap is enforced on the raw byte length BEFORE the latin1 string
|
|
39
|
+
// copy: converting first would allocate a full-size string for an input the
|
|
40
|
+
// cap is about to reject, and a buffer above Node's max string length would
|
|
41
|
+
// escape as an untyped ERR_STRING_TOO_LONG instead of pem/too-large.
|
|
42
|
+
if (Buffer.isBuffer(text)) {
|
|
43
|
+
if (text.length > constants.LIMITS.PEM_MAX_BYTES) throw new PemError("pem/too-large", "PEM input exceeds size cap");
|
|
44
|
+
text = text.toString("latin1");
|
|
45
|
+
}
|
|
35
46
|
if (typeof text !== "string") throw new PemError("pem/bad-input", "pemDecode expects a string or Buffer");
|
|
36
47
|
if (text.length > constants.LIMITS.PEM_MAX_BYTES) throw new PemError("pem/too-large", "PEM input exceeds size cap");
|
|
37
48
|
var m = PEM_RE.exec(text);
|
|
@@ -39,11 +50,21 @@ function pemDecode(text, label, PemError) {
|
|
|
39
50
|
if (label && m[1] !== label) throw new PemError("pem/label-mismatch", "expected " + JSON.stringify(label) + " block, got " + JSON.stringify(m[1]));
|
|
40
51
|
var b64 = m[2].replace(/[\r\n\t ]+/g, "");
|
|
41
52
|
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(b64)) throw new PemError("pem/bad-base64", "PEM body is not valid base64");
|
|
42
|
-
|
|
53
|
+
if (b64.length % 4 !== 0) throw new PemError("pem/bad-base64", "PEM base64 body must be whole 4-character groups (RFC 4648 sec. 3.5)");
|
|
54
|
+
var der = Buffer.from(b64, "base64");
|
|
55
|
+
if (der.toString("base64") !== b64) throw new PemError("pem/bad-base64", "PEM base64 body is not canonical (RFC 4648 sec. 3.5)");
|
|
56
|
+
return der;
|
|
43
57
|
}
|
|
44
58
|
|
|
59
|
+
// The label must be re-readable by this module's own PEM_RE (uppercase
|
|
60
|
+
// alphanumeric words separated by single spaces, a subset of the RFC 7468
|
|
61
|
+
// sec. 3 label grammar). Anything else (lowercase, a leading/trailing space,
|
|
62
|
+
// an embedded "-----" or newline) would emit armor pemDecode itself rejects
|
|
63
|
+
// or corrupt the armor grammar -- a config-time authoring fault, thrown here.
|
|
45
64
|
function pemEncode(der, label, PemError) {
|
|
46
|
-
if (typeof label !== "string" ||
|
|
65
|
+
if (typeof label !== "string" || !/^[A-Z0-9]+( [A-Z0-9]+)*$/.test(label)) {
|
|
66
|
+
throw new PemError("pem/bad-label", "pemEncode requires an uppercase A-Z0-9 label with single spaces");
|
|
67
|
+
}
|
|
47
68
|
var buf = Buffer.isBuffer(der) ? der : Buffer.from(der);
|
|
48
69
|
var b64 = buf.toString("base64").replace(/(.{64})/g, "$1\n").replace(/\n$/, "");
|
|
49
70
|
return "-----BEGIN " + label + "-----\n" + b64 + "\n-----END " + label + "-----\n";
|
|
@@ -63,8 +84,8 @@ function coerceToDer(input, opts) {
|
|
|
63
84
|
|
|
64
85
|
// Does a Buffer carry PEM armor (a .pem read with fs.readFileSync) rather than
|
|
65
86
|
// raw DER? It does iff "-----BEGIN" appears and everything before it is TEXT
|
|
66
|
-
// (UTF-8 BOM / whitespace / RFC 7468 explanatory preamble). DER is binary
|
|
67
|
-
// leading tag+length bytes are non-printable
|
|
87
|
+
// (UTF-8 BOM / whitespace / RFC 7468 explanatory preamble). DER is binary -- its
|
|
88
|
+
// leading tag+length bytes are non-printable -- so a non-SEQUENCE DER (a bare
|
|
68
89
|
// SET / INTEGER) is NOT misrouted here; it decodes and fails closed structurally.
|
|
69
90
|
function _isPemArmor(buf) {
|
|
70
91
|
var head = buf.slice(0, 4096).toString("latin1");
|
|
@@ -83,8 +104,8 @@ function _isPemArmor(buf) {
|
|
|
83
104
|
function decodeRoot(der, opts) {
|
|
84
105
|
// A format whose wire encoding is normatively BER (RFC 7292 PKCS#12) opts
|
|
85
106
|
// in to the codec's ber mode for the WHOLE decode. BER-vs-DER divergence is
|
|
86
|
-
// not always a decode-time throw
|
|
87
|
-
// OCTET STRING decodes strictly and only diverges at the typed reader
|
|
107
|
+
// not always a decode-time throw -- a definite-length constructed IMPLICIT
|
|
108
|
+
// OCTET STRING decodes strictly and only diverges at the typed reader -- so
|
|
88
109
|
// a strict-first-retry-on-throw boundary would miss legal BER shapes. A
|
|
89
110
|
// strict-DER input decodes identically under the ber mode, every other
|
|
90
111
|
// strictness verdict included.
|
|
@@ -101,7 +122,7 @@ function runParse(input, opts) {
|
|
|
101
122
|
return schema.walk(opts.topSchema, decodeRoot(coerceToDer(input, opts), opts), opts.ns).result;
|
|
102
123
|
}
|
|
103
124
|
|
|
104
|
-
// Distinguished-name attribute short labels (RFC 4514
|
|
125
|
+
// Distinguished-name attribute short labels (RFC 4514 sec. 3 + common use).
|
|
105
126
|
var DN_SHORT = {
|
|
106
127
|
commonName: "CN",
|
|
107
128
|
countryName: "C",
|
|
@@ -121,7 +142,7 @@ var DN_SHORT = {
|
|
|
121
142
|
// The error/OID namespace every format module walks its schema under:
|
|
122
143
|
// { prefix, E:(code,message,cause)=>new ErrorClass(...), oid }. Factored so a
|
|
123
144
|
// format declares it in one line instead of repeating the error-constructor
|
|
124
|
-
// closure (a caller that never passes a cause is unaffected
|
|
145
|
+
// closure (a caller that never passes a cause is unaffected -- withCause ignores
|
|
125
146
|
// an undefined third arg).
|
|
126
147
|
function makeNS(prefix, ErrorClass, oidModule) {
|
|
127
148
|
return { prefix: prefix, E: function (code, message, cause) { return new ErrorClass(code, message, cause); }, oid: oidModule };
|
|
@@ -129,10 +150,10 @@ function makeNS(prefix, ErrorClass, oidModule) {
|
|
|
129
150
|
|
|
130
151
|
// A bounded universal-INTEGER version reader. `accept` maps each legal wire value
|
|
131
152
|
// (as a decimal string) to its surfaced version number; any other value is a
|
|
132
|
-
// <prefix>/bad-version fault. The one genuine per-format divergence
|
|
153
|
+
// <prefix>/bad-version fault. The one genuine per-format divergence -- the cert
|
|
133
154
|
// rejects 0 and maps 1->2/2->3, a CRL accepts only 1->2, a CSR only 0->1, a PKCS#8
|
|
134
|
-
// 0->1/1->2
|
|
135
|
-
// RFC 2986
|
|
155
|
+
// 0->1/1->2 -- is expressed purely as the accept map (RFC 5280 sec. 4.1.2.1 / sec. 5.1.2.1,
|
|
156
|
+
// RFC 2986 sec. 4.1, RFC 5958 sec. 2). read.integer is strict, so an ENUMERATED-tagged
|
|
136
157
|
// version is rejected at the leaf (asn1/*).
|
|
137
158
|
function versionReader(ns, accept) {
|
|
138
159
|
return schema.decode(function (n) {
|
|
@@ -144,8 +165,20 @@ function versionReader(ns, accept) {
|
|
|
144
165
|
|
|
145
166
|
// opts.implicitTag (optional): read the AlgorithmIdentifier as a [tag] IMPLICIT
|
|
146
167
|
// SEQUENCE (a context-class constructed node whose children are algorithm + parameters),
|
|
147
|
-
// for the RFC 5652
|
|
168
|
+
// for the RFC 5652 sec. 6.2.4 pwri.keyDerivationAlgorithm [0]. With no opts the shape is a
|
|
148
169
|
// universal SEQUENCE, byte-identical to every existing caller.
|
|
170
|
+
//
|
|
171
|
+
// Parameter-rule tiering: only the params-MUST-BE-ABSENT families (ML-DSA,
|
|
172
|
+
// SLH-DSA, the Edwards/Montgomery curves -- new registrations with no legacy
|
|
173
|
+
// corpus) are enforced at parse time, in the build below. The CLASSICAL
|
|
174
|
+
// per-OID rules (RSA parameters MUST be NULL, RFC 3279 sec. 2.2.1/2.3.1;
|
|
175
|
+
// ecdsa-with-SHA2 parameters MUST be omitted, RFC 5758 sec. 3.2) are
|
|
176
|
+
// deliberately NOT parse-time rejects: a long tail of deployed certificates
|
|
177
|
+
// omits the RSA NULL, so the parse tier stays interop-lenient for those
|
|
178
|
+
// families and the strict classical check runs on the signature-verification
|
|
179
|
+
// path (path-validate's signature-algorithm registry), where the verdict
|
|
180
|
+
// matters. SPKI key AlgorithmIdentifiers follow the same tiering: lenient at
|
|
181
|
+
// parse, validated by the key-import layer that consumes them.
|
|
149
182
|
function algorithmIdentifier(ns, opts) {
|
|
150
183
|
opts = opts || {};
|
|
151
184
|
return schema.seq([
|
|
@@ -156,14 +189,16 @@ function algorithmIdentifier(ns, opts) {
|
|
|
156
189
|
arity: { min: 1 }, code: ns.prefix + "/bad-algorithm-identifier", what: "AlgorithmIdentifier",
|
|
157
190
|
build: function (m, ctx) {
|
|
158
191
|
var dotted = m.fields.algorithm.value;
|
|
159
|
-
//
|
|
160
|
-
// signature families (ML-DSA, SLH-DSA
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
//
|
|
192
|
+
// For every algorithm the registry marks parameters-absent -- the PQC
|
|
193
|
+
// signature families (ML-DSA RFC 9881, SLH-DSA RFC 9909/9814), the
|
|
194
|
+
// Edwards/Montgomery curves (RFC 8410), ML-KEM (RFC 9936), and the HKDF
|
|
195
|
+
// identifiers (RFC 8619) -- the `parameters` field MUST be absent: no
|
|
196
|
+
// explicit NULL, no bytes. Enforced ONCE here so every consumer of the
|
|
197
|
+
// shared AlgorithmIdentifier inherits the rule; the guard-parity bug
|
|
198
|
+
// class is structurally impossible.
|
|
164
199
|
if (m.fields.parameters.present && ctx.oid.paramsMustBeAbsent(dotted)) {
|
|
165
200
|
throw ctx.E(ctx.prefix + "/bad-algorithm-parameters",
|
|
166
|
-
"the " + (ctx.oid.name(dotted) || dotted) + " AlgorithmIdentifier parameters field MUST be absent
|
|
201
|
+
"the " + (ctx.oid.name(dotted) || dotted) + " AlgorithmIdentifier parameters field MUST be absent");
|
|
167
202
|
}
|
|
168
203
|
return { oid: dotted, name: ctx.oid.name(dotted) || null, parameters: m.fields.parameters.present ? m.fields.parameters.node.bytes : null };
|
|
169
204
|
},
|
|
@@ -173,44 +208,108 @@ function algorithmIdentifier(ns, opts) {
|
|
|
173
208
|
// attrValueToString(ns): the AttributeValue decode-leaf. A malformed KNOWN
|
|
174
209
|
// string type (invalid UTF-8, a non-IA5 byte, a PrintableString character
|
|
175
210
|
// outside its set, ...) surfaces as an asn1/bad-* content error and must fail
|
|
176
|
-
// closed
|
|
211
|
+
// closed -- do NOT hex-encode it away, or the decoder's strict string validation
|
|
177
212
|
// is silently bypassed on the DN path. A value that is simply not a decodable
|
|
178
213
|
// primitive string is NOT malformed and stays representable: an ANY-typed
|
|
179
214
|
// non-string tag (asn1/expected-string) or a constructed universal type such as
|
|
180
|
-
// a SEQUENCE (asn1/expected-primitive) renders per RFC 4514
|
|
215
|
+
// a SEQUENCE (asn1/expected-primitive) renders per RFC 4514 sec. 2.4 as "#" plus the
|
|
181
216
|
// hex of its FULL DER encoding (node.bytes), round-tripping intact.
|
|
217
|
+
// A GENUINE string whose first character is '#' or '\' is surfaced with a
|
|
218
|
+
// leading '\' escape (RFC 4514 sec. 2.4) so it can never collide with the
|
|
219
|
+
// '#'+hex form; the encode direction strips that one leading escape back off.
|
|
182
220
|
function attrValueToString(ns) {
|
|
183
221
|
return schema.decode(function (node) {
|
|
184
|
-
|
|
222
|
+
var s;
|
|
223
|
+
try { s = asn1.read.string(node); }
|
|
185
224
|
catch (e) {
|
|
186
225
|
if (!e || (e.code !== "asn1/expected-string" && e.code !== "asn1/expected-primitive")) {
|
|
187
226
|
throw ns.E(ns.prefix + "/bad-atv", "malformed string in attribute value: " + ((e && e.message) || String(e)));
|
|
188
227
|
}
|
|
189
228
|
return "#" + node.bytes.toString("hex");
|
|
190
229
|
}
|
|
230
|
+
if (s.charAt(0) === "#" || s.charAt(0) === "\\") return "\\" + s;
|
|
231
|
+
return s;
|
|
191
232
|
}, function (value) {
|
|
192
|
-
// encode: a
|
|
193
|
-
//
|
|
233
|
+
// encode: a leading '\' escapes the next character (the decode's escape of a
|
|
234
|
+
// literal '#'/'\'); a "#hex" form (RFC 4514 sec. 2.4) round-trips its raw DER
|
|
235
|
+
// verbatim -- the hex is validated (even-length hex digits encoding exactly one
|
|
236
|
+
// DER TLV) and throws rather than silently emitting truncated bytes, since
|
|
237
|
+
// Buffer.from(str, "hex") stops at the first invalid character. Any other
|
|
238
|
+
// string encodes as UTF8String (the decode does not preserve the original
|
|
194
239
|
// string type, so this is the canonical re-encoding, not a byte-exact one).
|
|
195
|
-
if (typeof value === "string" && value.charAt(0) === "
|
|
240
|
+
if (typeof value === "string" && value.charAt(0) === "\\") return asn1.build.utf8(value.slice(1));
|
|
241
|
+
if (typeof value === "string" && value.charAt(0) === "#") {
|
|
242
|
+
var hex = value.slice(1);
|
|
243
|
+
if (!/^(?:[0-9A-Fa-f]{2})+$/.test(hex)) {
|
|
244
|
+
throw ns.E(ns.prefix + "/bad-atv", "a #hex attribute value must be a non-empty even run of hex digits (RFC 4514 sec. 2.4)");
|
|
245
|
+
}
|
|
246
|
+
var raw = Buffer.from(hex, "hex");
|
|
247
|
+
try { asn1.decode(raw); }
|
|
248
|
+
catch (e) { throw ns.E(ns.prefix + "/bad-atv", "a #hex attribute value must encode exactly one DER TLV", e); }
|
|
249
|
+
return raw;
|
|
250
|
+
}
|
|
196
251
|
return asn1.build.utf8(value);
|
|
197
252
|
});
|
|
198
253
|
}
|
|
199
254
|
|
|
255
|
+
// The always-escape specials of RFC 4514 sec. 2.4, plus its positional rules:
|
|
256
|
+
// NUL escapes as \00, a trailing space as '\ ', and a leading '#' or space as
|
|
257
|
+
// '\#' / '\ ' -- without them the rendered dn is ambiguous (a literal '#0500'
|
|
258
|
+
// value would be indistinguishable from the hexstring form) and leading /
|
|
259
|
+
// trailing spaces would be silently significant.
|
|
200
260
|
function _escapeDnValue(v) {
|
|
201
|
-
|
|
261
|
+
var s = v.replace(/([,+"\\<>;])/g, "\\$1").split("\u0000").join("\\00");
|
|
262
|
+
if (s.length && s.charAt(s.length - 1) === " ") s = s.slice(0, -1) + "\\ ";
|
|
263
|
+
if (s.charAt(0) === "#" || s.charAt(0) === " ") s = "\\" + s;
|
|
264
|
+
return s;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// The dn RENDERING of a surfaced AttributeValue: the '#'-leading hex form (a
|
|
268
|
+
// non-string value) renders bare -- escaping its '#' would turn the RFC 4514
|
|
269
|
+
// hexstring form into an escaped literal -- while a genuine string (surfaced
|
|
270
|
+
// with its leading '#'/'\' escape, see attrValueToString) is unescaped back to
|
|
271
|
+
// the semantic string and then rendered with the full escape set.
|
|
272
|
+
function _dnDisplayValue(v) {
|
|
273
|
+
if (typeof v !== "string") return v;
|
|
274
|
+
if (v.charAt(0) === "#") return v;
|
|
275
|
+
return _escapeDnValue(v.charAt(0) === "\\" ? v.slice(1) : v);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// time(ns): the RFC 5280 Time production under its profile encoding rule
|
|
279
|
+
// (sec. 4.1.2.5 for certificate validity, restated for the CRL fields in
|
|
280
|
+
// sec. 5.1.2.4-5.1.2.6): a date through the year 2049 MUST be UTCTime and
|
|
281
|
+
// GeneralizedTime is reserved for 2050 onward, so a GeneralizedTime carrying a
|
|
282
|
+
// 1950..2049 date is a non-conforming second encoding of a representable value
|
|
283
|
+
// and is rejected (<prefix>/bad-time) -- accepting it would let two wire forms
|
|
284
|
+
// alias one abstract time (a parser-differential channel). The engine's
|
|
285
|
+
// schema.time already applies the cutover on ENCODE; this leaf composes it and
|
|
286
|
+
// enforces the same rule on DECODE. A GeneralizedTime-only field (an RFC 3161
|
|
287
|
+
// genTime, a CRL invalidityDate, an RFC 5755 AttCertValidityPeriod) must NOT
|
|
288
|
+
// take this leaf -- its syntax has no UTCTime alternative to cut over to.
|
|
289
|
+
function time(ns) {
|
|
290
|
+
var base = schema.time(ns);
|
|
291
|
+
return schema.decode(function (n, ctx) {
|
|
292
|
+
var d = base.fn(n, ctx);
|
|
293
|
+
if (n.tagNumber === asn1.TAGS.GENERALIZED_TIME) {
|
|
294
|
+
var y = d.getUTCFullYear();
|
|
295
|
+
if (y >= 1950 && y < 2050) {
|
|
296
|
+
throw ctx.E(ctx.prefix + "/bad-time", "a date through 2049 must be encoded as UTCTime, not GeneralizedTime (RFC 5280 sec. 4.1.2.5)");
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return d;
|
|
300
|
+
}, base.write);
|
|
202
301
|
}
|
|
203
302
|
|
|
204
303
|
// Name ::= RDNSequence ::= SEQUENCE OF RelativeDistinguishedName; RDN ::= SET OF
|
|
205
304
|
// AttributeTypeAndValue ::= SEQUENCE { type OID, value ANY }. The atv asserts
|
|
206
|
-
//
|
|
207
|
-
//
|
|
305
|
+
// its SEQUENCE tag (a SET-tagged body is a different ASN.1 type), and repeated
|
|
306
|
+
// RDN attribute types stay legal (no uniqueness).
|
|
208
307
|
function attributeTypeAndValue(ns) {
|
|
209
308
|
return schema.seq([
|
|
210
309
|
schema.field("type", schema.oidLeaf()),
|
|
211
310
|
schema.field("value", attrValueToString(ns)),
|
|
212
311
|
], {
|
|
213
|
-
assert: "
|
|
312
|
+
assert: "sequence", arity: { min: 2 }, code: ns.prefix + "/bad-atv", what: "AttributeTypeAndValue",
|
|
214
313
|
build: function (m, ctx) {
|
|
215
314
|
var typeOid = m.fields.type.value;
|
|
216
315
|
return { type: typeOid, name: ctx.oid.name(typeOid) || null, value: m.fields.value.value };
|
|
@@ -218,13 +317,13 @@ function attributeTypeAndValue(ns) {
|
|
|
218
317
|
});
|
|
219
318
|
}
|
|
220
319
|
function relativeDistinguishedName(ns) {
|
|
221
|
-
// RelativeDistinguishedName ::= SET SIZE (1..MAX)
|
|
320
|
+
// RelativeDistinguishedName ::= SET SIZE (1..MAX) -- an empty SET {} is malformed.
|
|
222
321
|
return schema.setOf(attributeTypeAndValue(ns), { assert: "set", min: 1, code: ns.prefix + "/bad-rdn", what: "RelativeDistinguishedName" });
|
|
223
322
|
}
|
|
224
|
-
// opts.implicitTag (optional): read the Name as a [tag] IMPLICIT RDNSequence
|
|
323
|
+
// opts.implicitTag (optional): read the Name as a [tag] IMPLICIT RDNSequence -- the
|
|
225
324
|
// context tag REPLACES the universal SEQUENCE tag, so the node is a context-class
|
|
226
325
|
// constructed [tag] whose children ARE the RDN SETs (the CRMF CertTemplate issuer
|
|
227
|
-
// [3] / subject [5] IMPLICIT arm, RFC 4211
|
|
326
|
+
// [3] / subject [5] IMPLICIT arm, RFC 4211 sec. 5). With no opts the shape is a bare
|
|
228
327
|
// universal SEQUENCE OF, byte-identical to every existing caller.
|
|
229
328
|
function name(ns, opts) {
|
|
230
329
|
opts = opts || {};
|
|
@@ -236,7 +335,7 @@ function name(ns, opts) {
|
|
|
236
335
|
var a = atvItem.value.result; // atvItem.value = the atv seq-match; .result = its build result
|
|
237
336
|
atvs.push(a);
|
|
238
337
|
var label = (a.name && DN_SHORT[a.name]) || a.name || a.type;
|
|
239
|
-
atvParts.push(label + "=" +
|
|
338
|
+
atvParts.push(label + "=" + _dnDisplayValue(a.value));
|
|
240
339
|
});
|
|
241
340
|
rdns.push(atvs);
|
|
242
341
|
parts.push(atvParts.join("+"));
|
|
@@ -251,25 +350,85 @@ function name(ns, opts) {
|
|
|
251
350
|
assert: "sequence", code: ns.prefix + "/bad-name", what: "Name", build: nameBuild });
|
|
252
351
|
}
|
|
253
352
|
|
|
254
|
-
// GeneralName ::= CHOICE (RFC 5280
|
|
353
|
+
// GeneralName ::= CHOICE (RFC 5280 sec. 4.2.1.6). A validate-and-surface-raw leaf for a
|
|
255
354
|
// caller that keeps the value RAW but needs it to be a well-formed GeneralName: it
|
|
256
355
|
// checks the chosen alternative's tag, form (constructed vs primitive per X.690
|
|
257
|
-
//
|
|
356
|
+
// sec. 10.2), and content -- otherName [0] is a SEQUENCE { type-id OID, value [0] EXPLICIT
|
|
258
357
|
// }; x400Address [3] / ediPartyName [5] are non-empty constructed; directoryName [4]
|
|
259
358
|
// EXPLICIT wraps a valid Name; rfc822Name [1] / dNSName [2] / uniformResourceIdentifier
|
|
260
359
|
// [6] are primitive non-empty IA5String (7-bit); iPAddress [7] is a 4- or 16-octet
|
|
261
|
-
// OCTET STRING; registeredID [8] is a primitive OBJECT IDENTIFIER
|
|
360
|
+
// OCTET STRING; registeredID [8] is a primitive OBJECT IDENTIFIER -- then surfaces the
|
|
262
361
|
// value RAW ({ bytes, tagClass, tagNumber }). `opts.code` is the caller's error code
|
|
263
362
|
// (e.g. ocsp/bad-requestor-name, tsp/bad-tsa). Shared so the two parsers cannot drift.
|
|
264
363
|
// opts.decodeValue (default false): in addition to the raw { bytes, tagClass,
|
|
265
|
-
// tagNumber }, surface the DECODED `value` per arm
|
|
364
|
+
// tagNumber }, surface the DECODED `value` per arm -- IA5 text (string), an
|
|
266
365
|
// iPAddress Buffer, a directoryName { rdns, dn }, a registeredID OID string,
|
|
267
366
|
// or an otherName { typeId, valueBytes }. The path validator's name-constraint
|
|
268
367
|
// matcher needs the decoded value; the tsp/ocsp/attrcert consumers pass no
|
|
269
368
|
// flag and get the byte-identical raw-only shape.
|
|
270
369
|
// opts.subtreeBase (default false): the GeneralSubtree.base form (RFC 5280
|
|
271
|
-
//
|
|
370
|
+
// sec. 4.2.1.10) -- an iPAddress base is an address+mask (8 octets IPv4 / 32 IPv6),
|
|
272
371
|
// NOT the 4/16-octet SAN address form. Only the iPAddress size rule changes.
|
|
372
|
+
// A GeneralizedTime-only leaf. Several formats fix their times to
|
|
373
|
+
// GeneralizedTime and reject UTCTime (OCSP RFC 6960, TSP RFC 3161 sec. 2.4.2,
|
|
374
|
+
// CMP RFC 9810 sec. 5.1.1, attribute-certificate validity RFC 5755 sec. 4.2.6);
|
|
375
|
+
// ONE ns-parameterized factory owns the tag assert so the per-format code and
|
|
376
|
+
// message differ but the check cannot drift. opts.allowFractional enables the
|
|
377
|
+
// X.690 sec. 11.7 fractional-seconds profile (the RFC 3161 genTime).
|
|
378
|
+
function generalizedTime(ns, opts) {
|
|
379
|
+
opts = opts || {};
|
|
380
|
+
var code = opts.code || (ns.prefix + "/bad-time");
|
|
381
|
+
var message = opts.message || "the time must be a GeneralizedTime";
|
|
382
|
+
var readOpts = opts.allowFractional ? { allowFractional: true } : undefined;
|
|
383
|
+
return schema.decode(function (n, ctx) {
|
|
384
|
+
if (n.tagClass !== "universal" || n.tagNumber !== asn1.TAGS.GENERALIZED_TIME) {
|
|
385
|
+
throw ctx.E(code, message);
|
|
386
|
+
}
|
|
387
|
+
return asn1.read.time(n, readOpts);
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// A UTF8String-only leaf -- the PKIFreeText element shape (RFC 3161 sec. 2.4.2,
|
|
392
|
+
// RFC 9810 sec. 5.1.1) shared by the TSP and CMP parsers.
|
|
393
|
+
function utf8Text(ns, opts) {
|
|
394
|
+
opts = opts || {};
|
|
395
|
+
var code = opts.code || (ns.prefix + "/bad-freetext");
|
|
396
|
+
var message = opts.message || "the element must be a UTF8String";
|
|
397
|
+
return schema.decode(function (n, ctx) {
|
|
398
|
+
if (n.tagClass !== "universal" || n.tagNumber !== asn1.TAGS.UTF8_STRING) {
|
|
399
|
+
throw ctx.E(code, message);
|
|
400
|
+
}
|
|
401
|
+
return asn1.read.string(n);
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// A raw NON-EMPTY universal SEQUENCE leaf -- surfaces the exact TLV bytes of an
|
|
406
|
+
// embedded element (a Certificate, a CertificateList, a PKIPublicationInfo)
|
|
407
|
+
// after asserting the SEQUENCE tag and at least one child. An empty SEQUENCE
|
|
408
|
+
// (30 00) has the right tag but is none of those structures, so it rejects
|
|
409
|
+
// rather than surfacing degenerate bytes to certificate processing.
|
|
410
|
+
function rawNonEmptySequence(ns, opts) {
|
|
411
|
+
opts = opts || {};
|
|
412
|
+
var code = opts.code || (ns.prefix + "/bad-sequence");
|
|
413
|
+
var message = opts.message || "expected a non-empty universal SEQUENCE";
|
|
414
|
+
return schema.decode(function (n, ctx) {
|
|
415
|
+
if (!(n.tagClass === "universal" && n.tagNumber === asn1.TAGS.SEQUENCE && n.children && n.children.length >= 1)) {
|
|
416
|
+
throw ctx.E(code, message);
|
|
417
|
+
}
|
|
418
|
+
return n.bytes;
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// CRLReason ::= ENUMERATED (RFC 5280 sec. 5.3.1) -- value 7 is unused/reserved.
|
|
423
|
+
// The ONE canonical value->name table every consumer validates against: the
|
|
424
|
+
// CRL reasonCode decoder surfaces the numeric code, the OCSP RevokedInfo
|
|
425
|
+
// decoder the name; both draw the legal set from here so they cannot drift.
|
|
426
|
+
var CRL_REASON_NAMES = {
|
|
427
|
+
"0": "unspecified", "1": "keyCompromise", "2": "cACompromise", "3": "affiliationChanged",
|
|
428
|
+
"4": "superseded", "5": "cessationOfOperation", "6": "certificateHold",
|
|
429
|
+
"8": "removeFromCRL", "9": "privilegeWithdrawn", "10": "aACompromise",
|
|
430
|
+
};
|
|
431
|
+
|
|
273
432
|
var GN_CONSTRUCTED = { 0: 1, 3: 1, 4: 1, 5: 1 };
|
|
274
433
|
var GN_IA5 = { 1: 1, 2: 1, 6: 1 };
|
|
275
434
|
function generalName(ns, opts) {
|
|
@@ -280,13 +439,13 @@ function generalName(ns, opts) {
|
|
|
280
439
|
var NAME = name(ns);
|
|
281
440
|
return schema.decode(function (n, ctx) {
|
|
282
441
|
if (n.tagClass !== "context" || n.tagNumber < 0 || n.tagNumber > 8) {
|
|
283
|
-
throw ctx.E(code, "value must be a GeneralName (context tag [0]..[8]) (RFC 5280
|
|
442
|
+
throw ctx.E(code, "value must be a GeneralName (context tag [0]..[8]) (RFC 5280 sec. 4.2.1.6)");
|
|
284
443
|
}
|
|
285
444
|
var t = n.tagNumber;
|
|
286
445
|
var constructed = !!n.children;
|
|
287
446
|
var value;
|
|
288
447
|
if (GN_CONSTRUCTED[t]) {
|
|
289
|
-
if (!constructed || n.children.length < 1) throw ctx.E(code, "GeneralName [" + t + "] must be a non-empty constructed value (RFC 5280
|
|
448
|
+
if (!constructed || n.children.length < 1) throw ctx.E(code, "GeneralName [" + t + "] must be a non-empty constructed value (RFC 5280 sec. 4.2.1.6)");
|
|
290
449
|
if (t === 0) {
|
|
291
450
|
// otherName ::= SEQUENCE { type-id OBJECT IDENTIFIER, value [0] EXPLICIT ANY }.
|
|
292
451
|
if (n.children.length !== 2) throw ctx.E(code, "GeneralName otherName [0] must be a SEQUENCE { type-id, value [0] }");
|
|
@@ -299,17 +458,17 @@ function generalName(ns, opts) {
|
|
|
299
458
|
}
|
|
300
459
|
if (decodeValue) value = { typeId: typeId, valueBytes: ov.children[0].bytes };
|
|
301
460
|
} else if (t === 4) {
|
|
302
|
-
// directoryName [4] EXPLICIT Name
|
|
461
|
+
// directoryName [4] EXPLICIT Name -- validate the wrapped RDNSequence.
|
|
303
462
|
if (n.children.length !== 1) throw ctx.E(code, "GeneralName directoryName [4] must wrap exactly one Name");
|
|
304
463
|
var dnMatch = schema.walk(NAME, n.children[0], ctx);
|
|
305
464
|
if (decodeValue) value = dnMatch.result;
|
|
306
465
|
}
|
|
307
466
|
} else {
|
|
308
|
-
if (constructed) throw ctx.E(code, "GeneralName [" + t + "] must be primitive (X.690
|
|
467
|
+
if (constructed) throw ctx.E(code, "GeneralName [" + t + "] must be primitive (X.690 sec. 10.2)");
|
|
309
468
|
if (GN_IA5[t]) {
|
|
310
469
|
if (n.content.length === 0) throw ctx.E(code, "GeneralName [" + t + "] must be a non-empty IA5String");
|
|
311
470
|
for (var i = 0; i < n.content.length; i++) {
|
|
312
|
-
// 7-bit IA5, and no C0/DEL control byte
|
|
471
|
+
// 7-bit IA5, and no C0/DEL control byte -- an embedded NUL/control in a
|
|
313
472
|
// dNSName/rfc822Name/URI enables a name-truncation/confusion bypass
|
|
314
473
|
// (CVE-2009-2408 class) downstream, so reject it at decode.
|
|
315
474
|
if (n.content[i] < 0x20 || n.content[i] > 0x7e) throw ctx.E(code, "GeneralName [" + t + "] must be a printable IA5String (no control bytes)");
|
|
@@ -317,7 +476,7 @@ function generalName(ns, opts) {
|
|
|
317
476
|
if (decodeValue) value = n.content.toString("latin1");
|
|
318
477
|
} else if (t === 7) {
|
|
319
478
|
if (subtreeBase) {
|
|
320
|
-
if (n.content.length !== 8 && n.content.length !== 32) throw ctx.E(code, "GeneralName iPAddress [7] subtree base must be an 8- or 32-octet address+mask (RFC 5280
|
|
479
|
+
if (n.content.length !== 8 && n.content.length !== 32) throw ctx.E(code, "GeneralName iPAddress [7] subtree base must be an 8- or 32-octet address+mask (RFC 5280 sec. 4.2.1.10)");
|
|
321
480
|
} else if (n.content.length !== 4 && n.content.length !== 16) {
|
|
322
481
|
throw ctx.E(code, "GeneralName iPAddress [7] must be a 4- or 16-octet address");
|
|
323
482
|
}
|
|
@@ -337,14 +496,14 @@ function generalName(ns, opts) {
|
|
|
337
496
|
});
|
|
338
497
|
}
|
|
339
498
|
|
|
340
|
-
// GeneralNames ::= SEQUENCE OF GeneralName (RFC 5280
|
|
499
|
+
// GeneralNames ::= SEQUENCE OF GeneralName (RFC 5280 sec. 4.2.1.6), SIZE (1..MAX). Every
|
|
341
500
|
// element is validated by generalName (its CHOICE alternative's form + content) and
|
|
342
501
|
// surfaced raw, so a caller carrying a GeneralNames field cannot accept a malformed
|
|
343
502
|
// element by treating the whole sequence as opaque bytes. Returns { names, bytes }
|
|
344
|
-
// where each `names[i]` is the generalName leaf's return
|
|
345
|
-
// tagNumber } and, when opts.decodeValue is set, the decoded `value`
|
|
503
|
+
// where each `names[i]` is the generalName leaf's return -- { bytes, tagClass,
|
|
504
|
+
// tagNumber } and, when opts.decodeValue is set, the decoded `value` -- and `bytes`
|
|
346
505
|
// is the raw outer DER.
|
|
347
|
-
// `opts.implicitTag` handles a [tag] IMPLICIT GeneralNames
|
|
506
|
+
// `opts.implicitTag` handles a [tag] IMPLICIT GeneralNames -- the context tag REPLACES
|
|
348
507
|
// the universal SEQUENCE tag (RFC 5755 Holder.entityName [1]); otherwise it is a bare
|
|
349
508
|
// universal SEQUENCE OF. `opts.code` is the caller's error code. Shared so the x509 /
|
|
350
509
|
// attribute-certificate / (future) CRMF parsers validate a GeneralNames identically.
|
|
@@ -361,17 +520,17 @@ function generalNames(ns, opts) {
|
|
|
361
520
|
return schema.seqOf(gn, { assert: "sequence", min: 1, code: code, what: opts.what || "GeneralNames", build: build });
|
|
362
521
|
}
|
|
363
522
|
|
|
364
|
-
// certExtensionDecoders(ns)
|
|
523
|
+
// certExtensionDecoders(ns) -- the ns-parameterized RFC 5280 sec. 4.2.1 extension
|
|
365
524
|
// VALUE decoders. `x509.parse` surfaces each extension as { oid, name, critical,
|
|
366
525
|
// value } with `value` the raw inner OCTET-STRING content (a Buffer); the path
|
|
367
526
|
// validator and the future complete-extension-set surface need the STRUCTURE
|
|
368
527
|
// inside. Each decoder takes that raw Buffer and returns the decoded value, or
|
|
369
528
|
// throws a typed `<prefix>/bad-*` code, mirroring the CRL's fail-closed
|
|
370
|
-
// `decodeExt` pattern (asn1.decode
|
|
529
|
+
// `decodeExt` pattern (asn1.decode -> read.* / schema.walk, wrapped). Returns
|
|
371
530
|
// { byOid } keyed by dotted OID so a consumer dispatches by ext.oid.
|
|
372
531
|
var _T = asn1.TAGS;
|
|
373
532
|
|
|
374
|
-
// assertPolicyQualifiers(qNode, fail)
|
|
533
|
+
// assertPolicyQualifiers(qNode, fail) -- RFC 5280 sec. 4.2.1.4: a PolicyInformation's
|
|
375
534
|
// policyQualifiers is a non-empty SEQUENCE OF PolicyQualifierInfo, each a SEQUENCE
|
|
376
535
|
// { policyQualifierId OID, qualifier } of EXACTLY two members led by an OID. The
|
|
377
536
|
// structure is validated fail-closed; the qualifier body stays opaque (surfaced
|
|
@@ -382,11 +541,11 @@ var _T = asn1.TAGS;
|
|
|
382
541
|
// leaf fault as the error cause when one is available.
|
|
383
542
|
function assertPolicyQualifiers(qNode, fail) {
|
|
384
543
|
if (qNode.tagClass !== "universal" || qNode.tagNumber !== _T.SEQUENCE || !qNode.children || !qNode.children.length) {
|
|
385
|
-
fail("policyQualifiers must be a non-empty SEQUENCE (RFC 5280
|
|
544
|
+
fail("policyQualifiers must be a non-empty SEQUENCE (RFC 5280 sec. 4.2.1.4)");
|
|
386
545
|
}
|
|
387
546
|
qNode.children.forEach(function (pq) {
|
|
388
547
|
if (pq.tagClass !== "universal" || pq.tagNumber !== _T.SEQUENCE || !pq.children || pq.children.length !== 2) {
|
|
389
|
-
fail("policyQualifiers element must be a PolicyQualifierInfo SEQUENCE { policyQualifierId, qualifier } (RFC 5280
|
|
548
|
+
fail("policyQualifiers element must be a PolicyQualifierInfo SEQUENCE { policyQualifierId, qualifier } (RFC 5280 sec. 4.2.1.4)");
|
|
390
549
|
}
|
|
391
550
|
try { asn1.read.oid(pq.children[0]); } catch (e) { fail("PolicyQualifierInfo must lead with a policyQualifierId OID", e); }
|
|
392
551
|
});
|
|
@@ -404,7 +563,7 @@ function certExtensionDecoders(ns) {
|
|
|
404
563
|
function seqChildren(buf, code, what) {
|
|
405
564
|
var n = decodeTop(buf, code, what);
|
|
406
565
|
if (n.tagClass !== "universal" || n.tagNumber !== _T.SEQUENCE || !n.children) {
|
|
407
|
-
throw ns.E(code, what + " must be a SEQUENCE (RFC 5280
|
|
566
|
+
throw ns.E(code, what + " must be a SEQUENCE (RFC 5280 sec. 4.2.1)");
|
|
408
567
|
}
|
|
409
568
|
return n.children;
|
|
410
569
|
}
|
|
@@ -421,13 +580,13 @@ function certExtensionDecoders(ns) {
|
|
|
421
580
|
if (kids[i] && kids[i].tagClass === "universal" && kids[i].tagNumber === _T.BOOLEAN) {
|
|
422
581
|
var v;
|
|
423
582
|
try { v = asn1.read.boolean(kids[i]); } catch (e) { throw ns.E(C, "BasicConstraints cA must be a BOOLEAN", e); }
|
|
424
|
-
if (v !== true) throw ns.E(C, "BasicConstraints cA DEFAULT FALSE must be omitted, not an explicit FALSE (X.690
|
|
583
|
+
if (v !== true) throw ns.E(C, "BasicConstraints cA DEFAULT FALSE must be omitted, not an explicit FALSE (X.690 sec. 11.5)");
|
|
425
584
|
cA = true; i++;
|
|
426
585
|
}
|
|
427
586
|
if (kids[i] && kids[i].tagClass === "universal" && kids[i].tagNumber === _T.INTEGER) {
|
|
428
|
-
if (!cA) throw ns.E(C, "BasicConstraints pathLenConstraint is only permitted when cA is TRUE (RFC 5280
|
|
587
|
+
if (!cA) throw ns.E(C, "BasicConstraints pathLenConstraint is only permitted when cA is TRUE (RFC 5280 sec. 4.2.1.9)");
|
|
429
588
|
var pl = readInt(kids[i], C, "pathLenConstraint");
|
|
430
|
-
// Bounded before Number() narrowing
|
|
589
|
+
// Bounded before Number() narrowing -- a value past the safe-integer range
|
|
431
590
|
// would round silently and be compared as a path-length ceiling, so it is
|
|
432
591
|
// exact-or-rejected (the same rule the RSASSA-PSS/PKCS#12 counters follow).
|
|
433
592
|
if (pl < 0n || pl > 2147483647n) throw ns.E(C, "BasicConstraints pathLenConstraint must be a non-negative integer within range");
|
|
@@ -443,15 +602,15 @@ function certExtensionDecoders(ns) {
|
|
|
443
602
|
function keyUsage(buf) {
|
|
444
603
|
var C = ns.prefix + "/bad-key-usage";
|
|
445
604
|
var n = decodeTop(buf, C, "KeyUsage");
|
|
446
|
-
if (n.tagClass !== "universal" || n.tagNumber !== _T.BIT_STRING) throw ns.E(C, "KeyUsage must be a BIT STRING (RFC 5280
|
|
605
|
+
if (n.tagClass !== "universal" || n.tagNumber !== _T.BIT_STRING) throw ns.E(C, "KeyUsage must be a BIT STRING (RFC 5280 sec. 4.2.1.3)");
|
|
447
606
|
var bs;
|
|
448
607
|
try { bs = asn1.read.bitString(n); } catch (e) { throw ns.E(C, "KeyUsage must be a well-formed BIT STRING", e); }
|
|
449
|
-
// At least one bit MUST be set (RFC 5280
|
|
608
|
+
// At least one bit MUST be set (RFC 5280 sec. 4.2.1.3) -- an all-zero value (a
|
|
450
609
|
// non-empty byte run of only zero bits, e.g. 03 02 07 00) is malformed too.
|
|
451
610
|
var anyBit = false;
|
|
452
611
|
for (var z = 0; z < bs.bytes.length; z++) { if (bs.bytes[z] !== 0) { anyBit = true; break; } }
|
|
453
|
-
if (!anyBit) throw ns.E(C, "KeyUsage must assert at least one bit (RFC 5280
|
|
454
|
-
// KeyUsage is a NamedBitList
|
|
612
|
+
if (!anyBit) throw ns.E(C, "KeyUsage must assert at least one bit (RFC 5280 sec. 4.2.1.3)");
|
|
613
|
+
// KeyUsage is a NamedBitList -- enforce the shared X.690 sec. 11.2.2 minimal-DER rule.
|
|
455
614
|
schema.assertMinimalNamedBits(bs.unusedBits, bs.bytes, function (m) { throw ns.E(C, m); });
|
|
456
615
|
var out = {};
|
|
457
616
|
KU_BITS.forEach(function (nm, bit) {
|
|
@@ -464,9 +623,9 @@ function certExtensionDecoders(ns) {
|
|
|
464
623
|
// GeneralSubtree ::= SEQUENCE { base GeneralName, minimum [0] DEFAULT 0, maximum [1] OPTIONAL }
|
|
465
624
|
function subtreeList(node, C) {
|
|
466
625
|
// node is the [0]/[1] IMPLICIT SEQUENCE OF GeneralSubtree (context-constructed).
|
|
467
|
-
// GeneralSubtrees is SIZE(1..MAX)
|
|
626
|
+
// GeneralSubtrees is SIZE(1..MAX) -- an explicitly present but empty subtree
|
|
468
627
|
// list (a0 00) absorbs no constraint and is malformed, not a no-op.
|
|
469
|
-
if (!node.children || node.children.length < 1) throw ns.E(C, "NameConstraints permittedSubtrees/excludedSubtrees must be a non-empty GeneralSubtrees (SIZE 1..MAX, RFC 5280
|
|
628
|
+
if (!node.children || node.children.length < 1) throw ns.E(C, "NameConstraints permittedSubtrees/excludedSubtrees must be a non-empty GeneralSubtrees (SIZE 1..MAX, RFC 5280 sec. 4.2.1.10)");
|
|
470
629
|
return node.children.map(function (st) {
|
|
471
630
|
if (st.tagClass !== "universal" || st.tagNumber !== _T.SEQUENCE || !st.children || st.children.length < 1) {
|
|
472
631
|
throw ns.E(C, "GeneralSubtree must be a SEQUENCE { base, minimum?, maximum? }");
|
|
@@ -475,8 +634,8 @@ function certExtensionDecoders(ns) {
|
|
|
475
634
|
for (var j = 1; j < st.children.length; j++) {
|
|
476
635
|
var f = st.children[j];
|
|
477
636
|
if (f.tagClass !== "context" || (f.tagNumber !== 0 && f.tagNumber !== 1)) throw ns.E(C, "GeneralSubtree has an unexpected field");
|
|
478
|
-
if (f.tagNumber === 0) throw ns.E(C, "GeneralSubtree minimum DEFAULT 0 must be omitted (RFC 5280
|
|
479
|
-
if (f.tagNumber === 1) throw ns.E(C, "GeneralSubtree maximum is not permitted in the RFC 5280 profile (
|
|
637
|
+
if (f.tagNumber === 0) throw ns.E(C, "GeneralSubtree minimum DEFAULT 0 must be omitted (RFC 5280 sec. 4.2.1.10 requires minimum = 0)");
|
|
638
|
+
if (f.tagNumber === 1) throw ns.E(C, "GeneralSubtree maximum is not permitted in the RFC 5280 profile (sec. 4.2.1.10)");
|
|
480
639
|
}
|
|
481
640
|
return { base: base };
|
|
482
641
|
});
|
|
@@ -493,7 +652,7 @@ function certExtensionDecoders(ns) {
|
|
|
493
652
|
else if (k.tagNumber === 1) { if (sawE) throw ns.E(C, "duplicate excludedSubtrees"); sawE = true; excluded = subtreeList(k, C); }
|
|
494
653
|
else throw ns.E(C, "NameConstraints has an unexpected field [" + k.tagNumber + "]");
|
|
495
654
|
});
|
|
496
|
-
if (!sawP && !sawE) throw ns.E(C, "NameConstraints must contain permittedSubtrees or excludedSubtrees (RFC 5280
|
|
655
|
+
if (!sawP && !sawE) throw ns.E(C, "NameConstraints must contain permittedSubtrees or excludedSubtrees (RFC 5280 sec. 4.2.1.10)");
|
|
497
656
|
return { permittedSubtrees: permitted, excludedSubtrees: excluded };
|
|
498
657
|
}
|
|
499
658
|
|
|
@@ -501,18 +660,18 @@ function certExtensionDecoders(ns) {
|
|
|
501
660
|
function certificatePolicies(buf) {
|
|
502
661
|
var C = ns.prefix + "/bad-policy";
|
|
503
662
|
var kids = seqChildren(buf, C, "CertificatePolicies");
|
|
504
|
-
if (kids.length < 1) throw ns.E(C, "CertificatePolicies must contain at least one PolicyInformation (RFC 5280
|
|
663
|
+
if (kids.length < 1) throw ns.E(C, "CertificatePolicies must contain at least one PolicyInformation (RFC 5280 sec. 4.2.1.4)");
|
|
505
664
|
var seen = {};
|
|
506
665
|
return kids.map(function (pi) {
|
|
507
666
|
// PolicyInformation ::= SEQUENCE { policyIdentifier, policyQualifiers
|
|
508
|
-
// SEQUENCE SIZE(1..MAX) OPTIONAL }
|
|
667
|
+
// SEQUENCE SIZE(1..MAX) OPTIONAL } -- exactly one or two fields; a second
|
|
509
668
|
// field, if present, MUST be a SEQUENCE. Extra/mis-typed fields are malformed.
|
|
510
669
|
if (pi.tagClass !== "universal" || pi.tagNumber !== _T.SEQUENCE || !pi.children || pi.children.length < 1 || pi.children.length > 2) {
|
|
511
670
|
throw ns.E(C, "PolicyInformation must be a SEQUENCE { policyIdentifier, policyQualifiers? }");
|
|
512
671
|
}
|
|
513
672
|
var pid;
|
|
514
673
|
try { pid = asn1.read.oid(pi.children[0]); } catch (e) { throw ns.E(C, "PolicyInformation policyIdentifier must be an OBJECT IDENTIFIER", e); }
|
|
515
|
-
if (seen[pid]) throw ns.E(C, "duplicate policy OID " + pid + " (RFC 5280
|
|
674
|
+
if (seen[pid]) throw ns.E(C, "duplicate policy OID " + pid + " (RFC 5280 sec. 4.2.1.4)");
|
|
516
675
|
seen[pid] = true;
|
|
517
676
|
var qualifiers = null;
|
|
518
677
|
if (pi.children.length > 1) {
|
|
@@ -530,7 +689,7 @@ function certExtensionDecoders(ns) {
|
|
|
530
689
|
function policyMappings(buf) {
|
|
531
690
|
var C = ns.prefix + "/bad-policy";
|
|
532
691
|
var kids = seqChildren(buf, C, "PolicyMappings");
|
|
533
|
-
if (kids.length < 1) throw ns.E(C, "PolicyMappings must contain at least one mapping (RFC 5280
|
|
692
|
+
if (kids.length < 1) throw ns.E(C, "PolicyMappings must contain at least one mapping (RFC 5280 sec. 4.2.1.5)");
|
|
534
693
|
return kids.map(function (mp) {
|
|
535
694
|
if (mp.tagClass !== "universal" || mp.tagNumber !== _T.SEQUENCE || !mp.children || mp.children.length !== 2) {
|
|
536
695
|
throw ns.E(C, "policy mapping must be a SEQUENCE { issuerDomainPolicy, subjectDomainPolicy }");
|
|
@@ -546,7 +705,7 @@ function certExtensionDecoders(ns) {
|
|
|
546
705
|
function policyConstraints(buf) {
|
|
547
706
|
var C = ns.prefix + "/bad-policy";
|
|
548
707
|
var kids = seqChildren(buf, C, "PolicyConstraints");
|
|
549
|
-
if (kids.length < 1) throw ns.E(C, "PolicyConstraints must contain at least one field (RFC 5280
|
|
708
|
+
if (kids.length < 1) throw ns.E(C, "PolicyConstraints must contain at least one field (RFC 5280 sec. 4.2.1.11)");
|
|
550
709
|
var rep = null, ipm = null, pcLastTag = -1;
|
|
551
710
|
kids.forEach(function (k) {
|
|
552
711
|
if (k.tagClass !== "context") throw ns.E(C, "PolicyConstraints fields are context-tagged [0]/[1]");
|
|
@@ -555,7 +714,7 @@ function certExtensionDecoders(ns) {
|
|
|
555
714
|
var v;
|
|
556
715
|
try { v = asn1.read.integerImplicit(k, k.tagNumber); } catch (e) { throw ns.E(C, "PolicyConstraints field must be an INTEGER", e); }
|
|
557
716
|
// Bounded before Number() narrowing (a skip count past the safe-integer
|
|
558
|
-
// range would round silently)
|
|
717
|
+
// range would round silently) -- exact-or-rejected.
|
|
559
718
|
if (v < 0n || v > 2147483647n) throw ns.E(C, "PolicyConstraints skip count must be a non-negative integer within range");
|
|
560
719
|
if (k.tagNumber === 0) rep = Number(v);
|
|
561
720
|
else if (k.tagNumber === 1) ipm = Number(v);
|
|
@@ -568,15 +727,15 @@ function certExtensionDecoders(ns) {
|
|
|
568
727
|
function inhibitAnyPolicy(buf) {
|
|
569
728
|
var C = ns.prefix + "/bad-policy";
|
|
570
729
|
var n = decodeTop(buf, C, "InhibitAnyPolicy");
|
|
571
|
-
if (n.tagClass !== "universal" || n.tagNumber !== _T.INTEGER) throw ns.E(C, "InhibitAnyPolicy must be an INTEGER (RFC 5280
|
|
730
|
+
if (n.tagClass !== "universal" || n.tagNumber !== _T.INTEGER) throw ns.E(C, "InhibitAnyPolicy must be an INTEGER (RFC 5280 sec. 4.2.1.14)");
|
|
572
731
|
var v = readInt(n, C, "InhibitAnyPolicy");
|
|
573
732
|
// Bounded before Number() narrowing (a skip count past the safe-integer
|
|
574
|
-
// range would round silently)
|
|
733
|
+
// range would round silently) -- exact-or-rejected.
|
|
575
734
|
if (v < 0n || v > 2147483647n) throw ns.E(C, "InhibitAnyPolicy skip count must be a non-negative integer within range");
|
|
576
735
|
return Number(v);
|
|
577
736
|
}
|
|
578
737
|
|
|
579
|
-
// subjectAltName / issuerAltName ::= GeneralNames
|
|
738
|
+
// subjectAltName / issuerAltName ::= GeneralNames -- decoded values surfaced.
|
|
580
739
|
function altName(buf) {
|
|
581
740
|
var C = ns.prefix + "/bad-extension-value";
|
|
582
741
|
var n = decodeTop(buf, C, "GeneralNames");
|
|
@@ -587,7 +746,7 @@ function certExtensionDecoders(ns) {
|
|
|
587
746
|
function extKeyUsage(buf) {
|
|
588
747
|
var C = ns.prefix + "/bad-extension-value";
|
|
589
748
|
var kids = seqChildren(buf, C, "ExtKeyUsage");
|
|
590
|
-
if (kids.length < 1) throw ns.E(C, "ExtKeyUsage must contain at least one KeyPurposeId (RFC 5280
|
|
749
|
+
if (kids.length < 1) throw ns.E(C, "ExtKeyUsage must contain at least one KeyPurposeId (RFC 5280 sec. 4.2.1.12)");
|
|
591
750
|
return kids.map(function (k) {
|
|
592
751
|
try { return asn1.read.oid(k); } catch (e) { throw ns.E(C, "ExtKeyUsage KeyPurposeId must be an OBJECT IDENTIFIER", e); }
|
|
593
752
|
});
|
|
@@ -614,7 +773,7 @@ function certExtensionDecoders(ns) {
|
|
|
614
773
|
else throw ns.E(C, "AuthorityKeyIdentifier has an unexpected field [" + k.tagNumber + "]");
|
|
615
774
|
});
|
|
616
775
|
if ((out.authorityCertIssuer === null) !== (out.authorityCertSerialNumber === null)) {
|
|
617
|
-
throw ns.E(C, "AuthorityKeyIdentifier authorityCertIssuer and authorityCertSerialNumber must both be present or both absent (RFC 5280
|
|
776
|
+
throw ns.E(C, "AuthorityKeyIdentifier authorityCertIssuer and authorityCertSerialNumber must both be present or both absent (RFC 5280 sec. 4.2.1.1)");
|
|
618
777
|
}
|
|
619
778
|
return out;
|
|
620
779
|
}
|
|
@@ -624,10 +783,10 @@ function certExtensionDecoders(ns) {
|
|
|
624
783
|
var C = ns.prefix + "/bad-extension-value";
|
|
625
784
|
var n = decodeTop(buf, C, "SubjectKeyIdentifier");
|
|
626
785
|
try { return Buffer.concat([asn1.read.octetString(n)]); }
|
|
627
|
-
catch (e) { throw ns.E(C, "SubjectKeyIdentifier must be an OCTET STRING (RFC 5280
|
|
786
|
+
catch (e) { throw ns.E(C, "SubjectKeyIdentifier must be an OCTET STRING (RFC 5280 sec. 4.2.1.2)", e); }
|
|
628
787
|
}
|
|
629
788
|
|
|
630
|
-
// signedCertificateTimestampList (RFC 6962
|
|
789
|
+
// signedCertificateTimestampList (RFC 6962 sec. 3.3) -- the extension value is a DER
|
|
631
790
|
// OCTET STRING wrapping the TLS-encoded SCT list. Delegated to the CT module,
|
|
632
791
|
// which owns the inner peel + the bounded TLS decode; structure is decoded, the
|
|
633
792
|
// signature stays raw and unverified. The registry contract is that a decoder
|
|
@@ -636,21 +795,29 @@ function certExtensionDecoders(ns) {
|
|
|
636
795
|
function sctList(buf) {
|
|
637
796
|
var C = ns.prefix + "/bad-extension-value";
|
|
638
797
|
try { return ct.parseSctList(buf); }
|
|
639
|
-
catch (e) { throw ns.E(C, "malformed signedCertificateTimestampList extension value (RFC 6962
|
|
798
|
+
catch (e) { throw ns.E(C, "malformed signedCertificateTimestampList extension value (RFC 6962 sec. 3.3)", e); }
|
|
640
799
|
}
|
|
641
800
|
|
|
642
|
-
// precertificatePoison (RFC 6962
|
|
801
|
+
// precertificatePoison (RFC 6962 sec. 3.1) -- a critical extension whose value is
|
|
643
802
|
// exactly ASN.1 NULL (05 00), the marker distinguishing a precertificate from
|
|
644
803
|
// a final certificate. Content-validated, not merely tag-checked.
|
|
645
804
|
function precertPoison(buf) {
|
|
646
805
|
var C = ns.prefix + "/bad-extension-value";
|
|
647
806
|
var n = decodeTop(buf, C, "PrecertificatePoison");
|
|
648
807
|
try { asn1.read.nullValue(n); }
|
|
649
|
-
catch (e) { throw ns.E(C, "the precertificate poison extension value must be ASN.1 NULL (RFC 6962
|
|
808
|
+
catch (e) { throw ns.E(C, "the precertificate poison extension value must be ASN.1 NULL (RFC 6962 sec. 3.1)", e); }
|
|
650
809
|
return { poison: true };
|
|
651
810
|
}
|
|
652
811
|
|
|
653
|
-
|
|
812
|
+
// byName returns undefined for an unregistered name; keying a decoder row
|
|
813
|
+
// under "undefined" would silently drop that extension from dispatch (the
|
|
814
|
+
// consumer would treat the extension as structurally unvalidated / absent),
|
|
815
|
+
// so a typo'd key-list entry must fail here, at module load.
|
|
816
|
+
function O(nm) {
|
|
817
|
+
var d = ns.oid.byName(nm);
|
|
818
|
+
if (typeof d !== "string") throw new TypeError("certExtensionDecoders: " + JSON.stringify(nm) + " is not a registered OID name");
|
|
819
|
+
return d;
|
|
820
|
+
}
|
|
654
821
|
var byOid = {};
|
|
655
822
|
byOid[O("basicConstraints")] = basicConstraints;
|
|
656
823
|
byOid[O("keyUsage")] = keyUsage;
|
|
@@ -673,10 +840,10 @@ function certExtensionDecoders(ns) {
|
|
|
673
840
|
// extnValue OCTET STRING }. `critical` is a universal BOOLEAN present-by-count
|
|
674
841
|
// (not context-tagged), so the per-extension decode handles the 2-vs-3-child
|
|
675
842
|
// shape directly; the seqOf centralizes the SEQUENCE / SIZE(1..MAX) assertion
|
|
676
|
-
// and the RFC 5280
|
|
843
|
+
// and the RFC 5280 sec. 4.2 per-OID uniqueness.
|
|
677
844
|
function extension(ns) {
|
|
678
845
|
return schema.decode(function (ext) {
|
|
679
|
-
// Extension ::= SEQUENCE { extnID, critical DEFAULT FALSE, extnValue }
|
|
846
|
+
// Extension ::= SEQUENCE { extnID, critical DEFAULT FALSE, extnValue } -- a
|
|
680
847
|
// UNIVERSAL SEQUENCE of exactly 2 (critical omitted) or 3 children. A
|
|
681
848
|
// context-tagged item (e.g. [5]{OID, OCTET STRING}) or a wrong child count
|
|
682
849
|
// is malformed; assert the tag, don't just count children (fail closed).
|
|
@@ -688,7 +855,7 @@ function extension(ns) {
|
|
|
688
855
|
var critical = false, valueNode;
|
|
689
856
|
if (ext.children.length === 3) {
|
|
690
857
|
critical = asn1.read.boolean(ext.children[1]);
|
|
691
|
-
// critical is BOOLEAN DEFAULT FALSE
|
|
858
|
+
// critical is BOOLEAN DEFAULT FALSE -- DER omits the field when false, so an
|
|
692
859
|
// explicitly-encoded FALSE is a non-canonical form; reject it fail-closed.
|
|
693
860
|
if (critical === false) throw ns.E(ns.prefix + "/bad-extension", "an explicit critical FALSE must be omitted (BOOLEAN DEFAULT FALSE)");
|
|
694
861
|
valueNode = ext.children[2];
|
|
@@ -699,8 +866,8 @@ function extension(ns) {
|
|
|
699
866
|
});
|
|
700
867
|
}
|
|
701
868
|
// opts.implicitTag (optional): read Extensions as a [tag] IMPLICIT SEQUENCE OF
|
|
702
|
-
// Extension
|
|
703
|
-
// CertTemplate extensions [9] (RFC 4211
|
|
869
|
+
// Extension -- the context tag REPLACES the universal SEQUENCE tag, for the CRMF
|
|
870
|
+
// CertTemplate extensions [9] (RFC 4211 sec. 5). With no opts the shape is a bare
|
|
704
871
|
// universal SEQUENCE OF, byte-identical to every existing caller.
|
|
705
872
|
function extensions(ns, opts) {
|
|
706
873
|
opts = opts || {};
|
|
@@ -715,13 +882,13 @@ function extensions(ns, opts) {
|
|
|
715
882
|
}
|
|
716
883
|
|
|
717
884
|
// SubjectPublicKeyInfo ::= SEQUENCE { algorithm AlgorithmIdentifier,
|
|
718
|
-
// subjectPublicKey BIT STRING } (RFC 5280
|
|
719
|
-
// universal SEQUENCE
|
|
885
|
+
// subjectPublicKey BIT STRING } (RFC 5280 sec. 4.1.2.7, RFC 2986 sec. 4.1). Asserted as a
|
|
886
|
+
// universal SEQUENCE -- a context-tagged or SET-tagged constructed node carrying
|
|
720
887
|
// two well-formed children is NOT a SubjectPublicKeyInfo. Shared by the
|
|
721
888
|
// certificate and CSR parsers.
|
|
722
889
|
// opts.implicitTag (optional): read the SubjectPublicKeyInfo as a [tag] IMPLICIT
|
|
723
890
|
// SEQUENCE (a context-class constructed node whose children are algorithm +
|
|
724
|
-
// subjectPublicKey), for the CRMF CertTemplate publicKey [6] (RFC 4211
|
|
891
|
+
// subjectPublicKey), for the CRMF CertTemplate publicKey [6] (RFC 4211 sec. 5). With no
|
|
725
892
|
// opts the shape is a universal SEQUENCE, byte-identical to every existing caller.
|
|
726
893
|
function spki(ns, opts) {
|
|
727
894
|
opts = opts || {};
|
|
@@ -745,10 +912,10 @@ function spki(ns, opts) {
|
|
|
745
912
|
}
|
|
746
913
|
|
|
747
914
|
// Attribute ::= SEQUENCE { type OBJECT IDENTIFIER, values SET OF AttributeValue }.
|
|
748
|
-
// AttributeValue is ANY, kept as raw DER (node.bytes)
|
|
915
|
+
// AttributeValue is ANY, kept as raw DER (node.bytes) -- an unrecognized attribute
|
|
749
916
|
// type never fails the parse. values is SET SIZE (1..MAX): an empty SET is
|
|
750
917
|
// rejected; there is NO SET-OF uniqueness. Shared by the CSR (requested
|
|
751
|
-
// attributes, RFC 2986
|
|
918
|
+
// attributes, RFC 2986 sec. 4.1) and PKCS#8 (private-key attributes, RFC 5958 sec. 2).
|
|
752
919
|
function attribute(ns) {
|
|
753
920
|
return schema.seq([
|
|
754
921
|
schema.field("type", schema.oidLeaf()),
|
|
@@ -783,7 +950,7 @@ function signedEnvelopeTbs(root) {
|
|
|
783
950
|
return tbs;
|
|
784
951
|
}
|
|
785
952
|
|
|
786
|
-
// rootSequenceChildren(root, minLen, maxLen)
|
|
953
|
+
// rootSequenceChildren(root, minLen, maxLen) -- the shared root-shape guard the
|
|
787
954
|
// format matches() detectors re-inline: returns root.children when root is a
|
|
788
955
|
// NON-EMPTY universal SEQUENCE whose child count is within [minLen, maxLen]
|
|
789
956
|
// (maxLen optional/Infinity), else null. Sibling of signedEnvelopeTbs.
|
|
@@ -800,20 +967,31 @@ function rootSequenceChildren(root, minLen, maxLen) {
|
|
|
800
967
|
// bound parser so a format declares its configuration once and never re-writes the
|
|
801
968
|
// coerce -> decode -> walk wrapper. `opts`: { pemLabel, PemError, ErrorClass,
|
|
802
969
|
// prefix, what, topSchema, ns }.
|
|
970
|
+
// The top-level schema's reject code -- the verdict for a DER that is not this
|
|
971
|
+
// structure at all -- is `<prefix>/not-a-<structure>` (x509/not-a-certificate,
|
|
972
|
+
// cms/not-a-content-info, ...). Three codes predate that convention and stay
|
|
973
|
+
// frozen on the public error-code surface (ocsp/bad-ocsp-response,
|
|
974
|
+
// tsp/bad-response, crmf/bad-cert-req-messages); a new format follows the
|
|
975
|
+
// not-a-* form, never those precedents.
|
|
803
976
|
function makeParser(opts) {
|
|
804
977
|
return function (input) { return runParse(input, opts); };
|
|
805
978
|
}
|
|
806
979
|
|
|
807
|
-
// The X.509 SIGNED{ToBeSigned} macro (RFC 5280
|
|
980
|
+
// The X.509 SIGNED{ToBeSigned} macro (RFC 5280 sec. 4.1.1.3): the outer
|
|
808
981
|
// SEQUENCE { toBeSigned, signatureAlgorithm AlgorithmIdentifier,
|
|
809
982
|
// signatureValue BIT STRING } shared by Certificate, CertificateList and
|
|
810
983
|
// CertificationRequest. `tbsSchema` parses the first element; the SEQUENCE-of-3
|
|
811
984
|
// shape, the arity, the signature extraction and the raw tbs / outer-signature
|
|
812
985
|
// bytes (for the cert/CRL outer==inner agreement check) are owned here once, and
|
|
813
986
|
// each format's `opts.build(envelope, ctx)` shapes its own object from the
|
|
814
|
-
// envelope. A CSR's build simply omits the agreement check
|
|
815
|
-
// signature AlgorithmIdentifier
|
|
987
|
+
// envelope. A CSR's build simply omits the agreement check -- its CRI has no inner
|
|
988
|
+
// signature AlgorithmIdentifier -- so the omission is structural, not a copy that
|
|
816
989
|
// forgot a guard.
|
|
990
|
+
// signatureValue is surfaced RAW as { unusedBits, bytes }: for the envelope
|
|
991
|
+
// family, octet alignment is a verify-layer rule (path-validate rejects
|
|
992
|
+
// unusedBits != 0 for certification paths; an external verifier must do the
|
|
993
|
+
// same). A format with no in-tree verify layer (OCSP, CMP protection, CRMF
|
|
994
|
+
// POP) instead enforces alignment at parse.
|
|
817
995
|
function signedEnvelope(ns, tbsSchema, opts) {
|
|
818
996
|
return schema.seq([
|
|
819
997
|
schema.field("toBeSigned", tbsSchema),
|
|
@@ -844,6 +1022,7 @@ module.exports = {
|
|
|
844
1022
|
makeNS: makeNS,
|
|
845
1023
|
versionReader: versionReader,
|
|
846
1024
|
DN_SHORT: DN_SHORT,
|
|
1025
|
+
time: time,
|
|
847
1026
|
algorithmIdentifier: algorithmIdentifier,
|
|
848
1027
|
spki: spki,
|
|
849
1028
|
makeParser: makeParser,
|
|
@@ -857,6 +1036,10 @@ module.exports = {
|
|
|
857
1036
|
name: name,
|
|
858
1037
|
generalName: generalName,
|
|
859
1038
|
generalNames: generalNames,
|
|
1039
|
+
generalizedTime: generalizedTime,
|
|
1040
|
+
utf8Text: utf8Text,
|
|
1041
|
+
rawNonEmptySequence: rawNonEmptySequence,
|
|
1042
|
+
CRL_REASON_NAMES: CRL_REASON_NAMES,
|
|
860
1043
|
certExtensionDecoders: certExtensionDecoders,
|
|
861
1044
|
attribute: attribute,
|
|
862
1045
|
extension: extension,
|