@blamejs/pki 0.1.5 → 0.1.7

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.
@@ -0,0 +1,346 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ /**
5
+ * @module pki.schema.engine
6
+ * @nav Schema
7
+ * @title Schema engine
8
+ * @order 60
9
+ *
10
+ * @intro
11
+ * L2 of the ASN.1 stack — a declarative structure-schema engine. A schema is
12
+ * plain data (`{kind, ...}` descriptors built by the combinators here) and
13
+ * `walk(schema, node, ctx)` interprets it against a decoded DER node. The
14
+ * engine is where every cross-cutting structural rule lives ONCE: the shape
15
+ * assertion (SEQUENCE / SET / bare-constructed), bounds-checked positional
16
+ * reads, optional and context-tagged fields in strictly-increasing tag order,
17
+ * SET-OF uniqueness, and fail-closed typed errors. A format module declares a
18
+ * schema and calls `walk` under an error namespace `ctx = { E, prefix, oid }`;
19
+ * it never hand-rolls `children[idx++]`, so the positional-read and
20
+ * duplicate-field bug classes are structurally retired. This is the shared
21
+ * base the certificate parser (and, later, CRL / CMS) composes.
22
+ *
23
+ * @card
24
+ * Declarative ASN.1 structure schemas + one walk engine — the shared base the
25
+ * certificate / CRL / CMS parsers compose instead of hand-writing.
26
+ */
27
+
28
+ var asn1 = require("./asn1-der.js");
29
+
30
+ var TAGS = asn1.TAGS;
31
+
32
+ // ---- context ---------------------------------------------------------
33
+ // A walk receives ctx = { E, prefix, oid } where E(code, message) constructs
34
+ // (does not throw) the format's typed error, prefix names the error family
35
+ // ("x509", "crl", ...) and oid is the OID registry (for name resolution in a
36
+ // build fn). _fail throws so a schema node reads as a single expression.
37
+
38
+ function _fail(ctx, code, message) {
39
+ throw ctx.E(code, message);
40
+ }
41
+
42
+ // ---- shape assertions ------------------------------------------------
43
+ // A schema's `assert` mode is a real behaviour-preservation control: some
44
+ // hand-written guards checked the universal SEQUENCE tag (algorithmIdentifier,
45
+ // Name), others checked only that the node had children (Validity, SPKI, the
46
+ // tbs body, an AttributeTypeAndValue). Collapsing them to one "is a SEQUENCE"
47
+ // check would silently change behaviour on SET-wrapped input, so each is kept.
48
+
49
+ function _assertShape(schema, node, ctx) {
50
+ var mode = schema.assert || "sequence";
51
+ if (mode === "sequence") {
52
+ if (node.tagClass !== "universal" || node.tagNumber !== TAGS.SEQUENCE || !node.children) {
53
+ _fail(ctx, schema.code, (schema.what || "value") + " must be a SEQUENCE");
54
+ }
55
+ } else if (mode === "set") {
56
+ if (node.tagClass !== "universal" || node.tagNumber !== TAGS.SET || !node.children) {
57
+ _fail(ctx, schema.code, (schema.what || "value") + " must be a SET");
58
+ }
59
+ } else if (mode === "constructed") {
60
+ // Bare constructed: the historical guards only required decoded children,
61
+ // never a specific universal tag.
62
+ if (!node.children) {
63
+ _fail(ctx, schema.code, (schema.what || "value") + " must be a constructed value");
64
+ }
65
+ } else {
66
+ _fail(ctx, schema.code, "unknown assert mode " + JSON.stringify(mode));
67
+ }
68
+ return node.children;
69
+ }
70
+
71
+ function _assertArity(schema, kids, ctx) {
72
+ var a = schema.arity;
73
+ if (!a) return;
74
+ if (a.exact != null && kids.length !== a.exact) {
75
+ _fail(ctx, schema.code, (schema.what || "value") + " must have exactly " + a.exact + " elements");
76
+ }
77
+ if (a.min != null && kids.length < a.min) {
78
+ _fail(ctx, schema.code, (schema.what || "value") + " must have at least " + a.min + " elements");
79
+ }
80
+ }
81
+
82
+ // ---- leaf / value combinators ---------------------------------------
83
+ // Leaves read a primitive off a node via the L1 codec; the codec's own
84
+ // asn1/* errors propagate unchanged (the frozen oracle expects those codes on
85
+ // the OID / INTEGER / string reads). `decode(fn)` hands the whole node to fn,
86
+ // which owns its try/catch and code mapping. `any()` yields the node itself.
87
+
88
+ function oidLeaf() { return { kind: "leaf", read: asn1.read.oid }; }
89
+ function integerLeaf() { return { kind: "leaf", read: asn1.read.integer }; }
90
+ function boolean() { return { kind: "leaf", read: asn1.read.boolean }; }
91
+ function octetString() { return { kind: "leaf", read: asn1.read.octetString }; }
92
+ function bitString() { return { kind: "leaf", read: function (n) { var b = asn1.read.bitString(n); return { unusedBits: b.unusedBits, bytes: b.bytes }; } }; }
93
+ function any() { return { kind: "any" }; }
94
+ function decode(fn) { return { kind: "decode", fn: fn }; }
95
+
96
+ // time(ns): a UTCTime / GeneralizedTime value, asserting the tag before the
97
+ // codec reads it (mirrors _parseValidityTime's x509/bad-time guard).
98
+ function time(ns) {
99
+ return decode(function (n, ctx) {
100
+ if (n.tagClass !== "universal" || (n.tagNumber !== TAGS.UTC_TIME && n.tagNumber !== TAGS.GENERALIZED_TIME)) {
101
+ _fail(ctx, ns.prefix + "/bad-time", "time must be UTCTime or GeneralizedTime");
102
+ }
103
+ return asn1.read.time(n);
104
+ });
105
+ }
106
+
107
+ // ---- field descriptors (members of a seq) ---------------------------
108
+
109
+ function field(name, schema) { return { fkind: "required", name: name, schema: schema }; }
110
+ function optional(name, schema, opts) {
111
+ opts = opts || {};
112
+ // How the optional field is recognized at its position:
113
+ // - default: a context [tag] (the certificate version [0] shape).
114
+ // - whenUniversal: the next element iff its UNIVERSAL tag is in the set —
115
+ // the CRL TBSCertList shape (bare INTEGER version, Time nextUpdate,
116
+ // SEQUENCE revokedCertificates), disambiguated by tag, not a context [n].
117
+ // - whenAny: the next element whatever its tag — an OPTIONAL ANY like
118
+ // AlgorithmIdentifier.parameters.
119
+ // The recognizer lets _walkSeq CONSUME the element so a closed sequence can
120
+ // reject whatever is left over (without it, a trailing ANY looks unconsumed).
121
+ var match = opts.whenAny
122
+ ? function () { return true; }
123
+ : opts.whenUniversal
124
+ ? function (n) { return n.tagClass === "universal" && opts.whenUniversal.indexOf(n.tagNumber) !== -1; }
125
+ : function (n) { return n.tagClass === "context" && n.tagNumber === opts.tag; };
126
+ return { fkind: "optional", name: name, schema: schema, tag: opts.tag, match: match,
127
+ explicit: !!opts.explicit, emptyCode: opts.emptyCode, hasDefault: ("default" in opts), def: opts.default };
128
+ }
129
+ // trailing: the [minTag..maxTag] optional context fields, each at most once in
130
+ // strictly-increasing tag order (the tbs issuerUniqueID[1]/subjectUniqueID[2]/
131
+ // extensions[3] block). members: [{ tag, name, schema, explicit? }].
132
+ function trailing(members, opts) {
133
+ opts = opts || {};
134
+ return { fkind: "trailing", members: members, minTag: opts.minTag, maxTag: opts.maxTag,
135
+ unexpectedCode: opts.unexpectedCode, orderCode: opts.orderCode };
136
+ }
137
+
138
+ // ---- structural combinators -----------------------------------------
139
+
140
+ function seq(fields, opts) {
141
+ opts = opts || {};
142
+ return { kind: "seq", fields: fields, assert: opts.assert || "sequence", arity: opts.arity,
143
+ code: opts.code, what: opts.what, build: opts.build, checks: opts.checks || [] };
144
+ }
145
+
146
+ // A NON-optional EXPLICIT context wrapper (CMS ContentInfo; the tbs [3] uses it
147
+ // inside `trailing`). Asserts context class + tag, constructed, >=1 child, then
148
+ // walks the inner schema on children[0].
149
+ function explicit(tag, schema, opts) {
150
+ opts = opts || {};
151
+ return { kind: "explicit", tag: tag, schema: schema, emptyCode: opts.emptyCode, code: opts.code, what: opts.what };
152
+ }
153
+
154
+ function choice(alts, opts) {
155
+ opts = opts || {};
156
+ return { kind: "choice", alts: alts, code: opts.code, what: opts.what };
157
+ }
158
+
159
+ function seqOf(item, opts) {
160
+ opts = opts || {};
161
+ return { kind: "repeat", item: item, assert: opts.assert || "sequence", code: opts.code, what: opts.what,
162
+ min: opts.min, unique: opts.unique, dupCode: opts.dupCode, build: opts.build };
163
+ }
164
+ function setOf(item, opts) {
165
+ opts = opts || {};
166
+ return { kind: "repeat", item: item, assert: opts.assert || "set", code: opts.code, what: opts.what,
167
+ min: opts.min, unique: opts.unique, dupCode: opts.dupCode, build: opts.build };
168
+ }
169
+ function setOfUnique(item, keyFn, opts) {
170
+ return setOf(item, Object.assign({ unique: keyFn }, opts || {}));
171
+ }
172
+
173
+ // ---- the walk engine -------------------------------------------------
174
+
175
+ /**
176
+ * @primitive pki.schema.engine.walk
177
+ * @signature pki.schema.engine.walk(schema, node, ctx) -> value
178
+ * @since 0.1.7
179
+ * @status experimental
180
+ * @spec X.690, X.680
181
+ * @related pki.asn1.decode, pki.schema.x509.parse
182
+ *
183
+ * Interpret a declarative schema against a decoded DER node, enforcing the
184
+ * schema's structural rules (shape assertion, arity, optional / context-tagged
185
+ * fields in increasing tag order, SET-OF uniqueness) and returning the built
186
+ * value — or the match tree (`{ node, fields | items }`, with the build output
187
+ * on `.result`) for a structure with no build fn. `ctx = { E, prefix, oid }`
188
+ * supplies the typed-error constructor, the error-code family prefix, and the
189
+ * OID registry a build fn resolves names through.
190
+ *
191
+ * The schema is assembled from the combinators this module exports — structural
192
+ * (`seq` / `field` / `optional` / `explicit` / `trailing` / `seqOf` / `setOf` /
193
+ * `setOfUnique` / `choice`) and value (`oidLeaf` / `integerLeaf` / `boolean` /
194
+ * `octetString` / `bitString` / `any` / `decode` / `time`).
195
+ *
196
+ * @example
197
+ * var S = pki.schema.engine;
198
+ * var ALGID = S.seq([S.field("algorithm", S.oidLeaf())],
199
+ * { assert: "sequence", arity: { min: 1 }, code: "app/bad-alg" });
200
+ * S.walk(ALGID, pki.asn1.decode(der), { prefix: "app", E: MyError, oid: pki.oid });
201
+ */
202
+ function walk(schema, node, ctx) {
203
+ switch (schema.kind) {
204
+ case "leaf": return schema.read(node);
205
+ case "any": return node;
206
+ case "decode": return schema.fn(node, ctx);
207
+ case "seq": return _walkSeq(schema, node, ctx);
208
+ case "explicit": return _walkExplicit(schema, node, ctx);
209
+ case "repeat": return _walkRepeat(schema, node, ctx);
210
+ case "choice": return _walkChoice(schema, node, ctx);
211
+ default: _fail(ctx, (ctx.prefix || "schema") + "/bad-schema", "unknown schema kind " + JSON.stringify(schema.kind));
212
+ }
213
+ return undefined;
214
+ }
215
+
216
+ // An EXPLICIT [tag] wrapper carries EXACTLY ONE inner value; 0 children is
217
+ // empty and 2+ would silently drop all but the first (the same drop-extra
218
+ // fail-open the seq guard closes). Assert exactly one, return it.
219
+ function _explicitInner(node, tag, ctx, code) {
220
+ if (!node.children || node.children.length !== 1) {
221
+ _fail(ctx, code, "EXPLICIT [" + tag + "] must wrap exactly one value");
222
+ }
223
+ return node.children[0];
224
+ }
225
+
226
+ function _walkExplicit(schema, node, ctx) {
227
+ if (node.tagClass !== "context" || node.tagNumber !== schema.tag) {
228
+ _fail(ctx, schema.emptyCode || schema.code, "expected an EXPLICIT [" + schema.tag + "] wrapper");
229
+ }
230
+ return walk(schema.schema, _explicitInner(node, schema.tag, ctx, schema.emptyCode || schema.code), ctx);
231
+ }
232
+
233
+ function _walkChoice(schema, node, ctx) {
234
+ for (var i = 0; i < schema.alts.length; i++) {
235
+ var w = schema.alts[i].when;
236
+ if (node.tagClass === w.tagClass && node.tagNumber === w.tagNumber) {
237
+ return walk(schema.alts[i].schema, node, ctx);
238
+ }
239
+ }
240
+ _fail(ctx, schema.code, "no CHOICE alternative matched " + node.tagClass + "/" + node.tagNumber);
241
+ return undefined;
242
+ }
243
+
244
+ function _walkRepeat(schema, node, ctx) {
245
+ var kids = _assertShape(schema, node, ctx);
246
+ if (schema.min != null && kids.length < schema.min) {
247
+ _fail(ctx, schema.code, (schema.what || "value") + " must contain at least " + schema.min + " element(s)");
248
+ }
249
+ var items = [];
250
+ var seen = schema.unique ? new Set() : null;
251
+ for (var i = 0; i < kids.length; i++) {
252
+ // The item wrapper is the SAME shape unique() and build() see: { node,
253
+ // value: <walk result> }. keyFn reads off item.value just like build does.
254
+ var item = { node: kids[i], value: walk(schema.item, kids[i], ctx) };
255
+ if (seen) {
256
+ var key = schema.unique(item);
257
+ if (seen.has(key)) _fail(ctx, schema.dupCode || schema.code, "duplicate element " + key);
258
+ seen.add(key);
259
+ }
260
+ items.push(item);
261
+ }
262
+ var match = { kind: "repeat", node: node, items: items };
263
+ if (schema.build) match.result = schema.build(match, ctx);
264
+ return match;
265
+ }
266
+
267
+ function _walkSeq(schema, node, ctx) {
268
+ var kids = _assertShape(schema, node, ctx);
269
+ _assertArity(schema, kids, ctx);
270
+
271
+ var fields = {};
272
+ var idx = 0;
273
+ for (var f = 0; f < schema.fields.length; f++) {
274
+ var fld = schema.fields[f];
275
+ if (fld.fkind === "required") {
276
+ if (idx >= kids.length) _fail(ctx, schema.code, "missing required field " + JSON.stringify(fld.name));
277
+ var child = kids[idx++];
278
+ fields[fld.name] = { node: child, value: walk(fld.schema, child, ctx) };
279
+ } else if (fld.fkind === "optional") {
280
+ var next = idx < kids.length ? kids[idx] : null;
281
+ if (next && fld.match(next)) {
282
+ idx++;
283
+ var inner = next;
284
+ if (fld.explicit) {
285
+ inner = _explicitInner(next, fld.tag, ctx, fld.emptyCode || schema.code);
286
+ }
287
+ fields[fld.name] = { node: next, present: true, value: walk(fld.schema, inner, ctx) };
288
+ } else {
289
+ fields[fld.name] = { present: false, value: fld.hasDefault ? fld.def : undefined };
290
+ }
291
+ } else if (fld.fkind === "trailing") {
292
+ _consumeTrailing(fld, kids, idx, fields, ctx);
293
+ idx = kids.length;
294
+ }
295
+ }
296
+
297
+ // Every child must be consumed by a field. A leftover element (no trailing
298
+ // field ran to absorb it) is malformed — dropping it silently would let a
299
+ // closed sequence of optional fields accept a duplicate/extra element.
300
+ if (idx < kids.length) {
301
+ _fail(ctx, schema.code, (schema.what || "value") + " has an unexpected element after its last field");
302
+ }
303
+
304
+ var match = { kind: "seq", node: node, fields: fields };
305
+ for (var c = 0; c < schema.checks.length; c++) schema.checks[c](match, ctx);
306
+ if (schema.build) match.result = schema.build(match, ctx);
307
+ return match;
308
+ }
309
+
310
+ function _consumeTrailing(fld, kids, start, fields, ctx) {
311
+ var byTag = {};
312
+ for (var m = 0; m < fld.members.length; m++) byTag[fld.members[m].tag] = fld.members[m];
313
+ // The monotonic-order sentinel must start below the lowest accepted tag.
314
+ // Context tag numbers are non-negative, so -1 is below any member tag when
315
+ // minTag is absent — otherwise a trailing block whose first member is [0]
316
+ // would reject that field (0 <= last==0) as repeated/out-of-order.
317
+ var last = fld.minTag != null ? fld.minTag - 1 : -1;
318
+ for (var i = start; i < kids.length; i++) {
319
+ var t = kids[i];
320
+ if (t.tagClass !== "context" || (fld.minTag != null && t.tagNumber < fld.minTag) || (fld.maxTag != null && t.tagNumber > fld.maxTag) || !byTag[t.tagNumber]) {
321
+ _fail(ctx, fld.unexpectedCode, "unexpected trailing field [" + (t.tagClass === "context" ? t.tagNumber : t.tagClass) + "]");
322
+ }
323
+ if (t.tagNumber <= last) _fail(ctx, fld.orderCode, "trailing field [" + t.tagNumber + "] is repeated or out of order");
324
+ last = t.tagNumber;
325
+ var member = byTag[t.tagNumber];
326
+ var inner = t;
327
+ if (member.explicit) {
328
+ inner = _explicitInner(t, t.tagNumber, ctx, member.emptyCode);
329
+ }
330
+ fields[member.name] = { node: t, present: true, value: walk(member.schema, inner, ctx) };
331
+ }
332
+ for (var n = 0; n < fld.members.length; n++) {
333
+ if (!fields[fld.members[n].name]) fields[fld.members[n].name] = { present: false, value: undefined };
334
+ }
335
+ }
336
+
337
+ module.exports = {
338
+ // structural
339
+ seq: seq, field: field, optional: optional, explicit: explicit, trailing: trailing,
340
+ seqOf: seqOf, setOf: setOf, setOfUnique: setOfUnique, choice: choice,
341
+ // leaves
342
+ oidLeaf: oidLeaf, integerLeaf: integerLeaf, boolean: boolean, octetString: octetString,
343
+ bitString: bitString, any: any, decode: decode, time: time,
344
+ // engine
345
+ walk: walk,
346
+ };
@@ -0,0 +1,239 @@
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
6
+ // parsers that compose these factories (pki.schema.x509, pki.schema.crl, …).
7
+ //
8
+ // Shared PKIX structure-schema factories (RFC 5280). Each is a namespace-
9
+ // parameterized FACTORY: given an error namespace `ns` ({ prefix, E, oid }) it
10
+ // returns an asn1-schema that walks the corresponding ASN.1 structure and emits
11
+ // the caller's own <prefix>/* error codes. x509.js, crl.js, and future CMS/CSR
12
+ // parsers compose these so AlgorithmIdentifier / Name / Extension are defined
13
+ // once, not re-derived per format. This module is internal infrastructure — the
14
+ // operator-facing surface is the parsers that consume it.
15
+
16
+ var asn1 = require("./asn1-der");
17
+ var constants = require("./constants");
18
+ var schema = require("./schema-engine");
19
+
20
+ var PEM_RE = /-----BEGIN ([A-Z0-9 ]+)-----([\s\S]*?)-----END \1-----/;
21
+
22
+ // ---- shared parse-entry ----------------------------------------------
23
+ // Input handling, PEM unwrapping (with the size cap), DER-decode wrapping, and
24
+ // the walk are defined ONCE here so no format can diverge on a guard. Each
25
+ // format supplies only its labels, error class, code prefix, and top schema.
26
+
27
+ // pemDecode(text, label, PemError): `label` (when truthy) is enforced, else the
28
+ // first block is taken. Applies the LIMITS.PEM_MAX_BYTES cap before scanning.
29
+ function pemDecode(text, label, PemError) {
30
+ if (Buffer.isBuffer(text)) text = text.toString("latin1");
31
+ if (typeof text !== "string") throw new PemError("pem/bad-input", "pemDecode expects a string or Buffer");
32
+ if (text.length > constants.LIMITS.PEM_MAX_BYTES) throw new PemError("pem/too-large", "PEM input exceeds size cap");
33
+ var m = PEM_RE.exec(text);
34
+ if (!m) throw new PemError("pem/no-block", "no PEM block found");
35
+ if (label && m[1] !== label) throw new PemError("pem/label-mismatch", "expected " + JSON.stringify(label) + " block, got " + JSON.stringify(m[1]));
36
+ var b64 = m[2].replace(/[\r\n\t ]+/g, "");
37
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(b64)) throw new PemError("pem/bad-base64", "PEM body is not valid base64");
38
+ return Buffer.from(b64, "base64");
39
+ }
40
+
41
+ function pemEncode(der, label, PemError) {
42
+ if (typeof label !== "string" || label.length === 0) throw new PemError("pem/bad-label", "pemEncode requires a label");
43
+ var buf = Buffer.isBuffer(der) ? der : Buffer.from(der);
44
+ var b64 = buf.toString("base64").replace(/(.{64})/g, "$1\n").replace(/\n$/, "");
45
+ return "-----BEGIN " + label + "-----\n" + b64 + "\n-----END " + label + "-----\n";
46
+ }
47
+
48
+ // Coerce parse input to DER bytes. A PEM string OR a PEM Buffer (a .pem file
49
+ // read with fs.readFileSync) is unwrapped with opts.pemLabel; a DER Buffer or
50
+ // Uint8Array is taken as bytes. opts: { pemLabel, PemError, ErrorClass, prefix }.
51
+ function coerceToDer(input, opts) {
52
+ if (typeof input === "string") return pemDecode(input, opts.pemLabel, opts.PemError);
53
+ if (input instanceof Uint8Array && !Buffer.isBuffer(input)) input = Buffer.from(input);
54
+ if (Buffer.isBuffer(input)) {
55
+ return _isPemArmor(input) ? pemDecode(input, opts.pemLabel, opts.PemError) : input;
56
+ }
57
+ throw new opts.ErrorClass(opts.prefix + "/bad-input", "parse expects a DER Buffer or a PEM string");
58
+ }
59
+
60
+ // Does a Buffer carry PEM armor (a .pem read with fs.readFileSync) rather than
61
+ // raw DER? It does iff "-----BEGIN" appears and everything before it is TEXT
62
+ // (UTF-8 BOM / whitespace / RFC 7468 explanatory preamble). DER is binary — its
63
+ // leading tag+length bytes are non-printable — so a non-SEQUENCE DER (a bare
64
+ // SET / INTEGER) is NOT misrouted here; it decodes and fails closed structurally.
65
+ function _isPemArmor(buf) {
66
+ var head = buf.slice(0, 4096).toString("latin1");
67
+ var idx = head.indexOf("-----BEGIN");
68
+ if (idx === -1) return false;
69
+ for (var i = 0; i < idx; i++) {
70
+ var c = buf[i];
71
+ var textByte = (c >= 0x20 && c < 0x7f) || c === 0x09 || c === 0x0a || c === 0x0d ||
72
+ c === 0xef || c === 0xbb || c === 0xbf; // printable ASCII, tab/newlines, UTF-8 BOM
73
+ if (!textByte) return false;
74
+ }
75
+ return true;
76
+ }
77
+
78
+ // Decode the DER root, wrapping a codec fault in the caller's <prefix>/bad-der.
79
+ function decodeRoot(der, opts) {
80
+ try { return asn1.decode(der); }
81
+ catch (e) { throw new opts.ErrorClass(opts.prefix + "/bad-der", (opts.what || "input") + " DER did not decode: " + ((e && e.message) || String(e)), e); }
82
+ }
83
+
84
+ // The shared parse entry: coerce -> decode -> walk the top schema. A format's
85
+ // parse() is one call to this; the guard-parity bug class (a new format not
86
+ // mirroring an existing format's input handling) is structurally impossible.
87
+ function runParse(input, opts) {
88
+ return schema.walk(opts.topSchema, decodeRoot(coerceToDer(input, opts), opts), opts.ns).result;
89
+ }
90
+
91
+ // Distinguished-name attribute short labels (RFC 4514 §3 + common use).
92
+ var DN_SHORT = {
93
+ commonName: "CN",
94
+ countryName: "C",
95
+ localityName: "L",
96
+ stateOrProvinceName: "ST",
97
+ streetAddress: "STREET",
98
+ organizationName: "O",
99
+ organizationalUnitName: "OU",
100
+ domainComponent: "DC",
101
+ surname: "SN",
102
+ givenName: "GN",
103
+ serialNumber: "SERIALNUMBER",
104
+ emailAddress: "emailAddress",
105
+ };
106
+
107
+ // AlgorithmIdentifier ::= SEQUENCE { algorithm OBJECT IDENTIFIER, parameters ANY OPTIONAL }
108
+ function algorithmIdentifier(ns) {
109
+ return schema.seq([
110
+ schema.field("algorithm", schema.oidLeaf()),
111
+ schema.optional("parameters", schema.any(), { whenAny: true }),
112
+ ], {
113
+ assert: "sequence", arity: { min: 1 }, code: ns.prefix + "/bad-algorithm-identifier", what: "AlgorithmIdentifier",
114
+ build: function (m, ctx) {
115
+ var dotted = m.fields.algorithm.value;
116
+ return { oid: dotted, name: ctx.oid.name(dotted) || null, parameters: m.fields.parameters.present ? m.fields.parameters.node.bytes : null };
117
+ },
118
+ });
119
+ }
120
+
121
+ // attrValueToString(ns): the AttributeValue decode-leaf. A malformed KNOWN
122
+ // string type (invalid UTF-8, a non-IA5 byte, a PrintableString character
123
+ // outside its set, ...) surfaces as an asn1/bad-* content error and must fail
124
+ // closed — do NOT hex-encode it away, or the decoder's strict string validation
125
+ // is silently bypassed on the DN path. A value that is simply not a decodable
126
+ // primitive string is NOT malformed and stays representable: an ANY-typed
127
+ // non-string tag (asn1/expected-string) or a constructed universal type such as
128
+ // a SEQUENCE (asn1/expected-primitive) renders per RFC 4514 §2.4 as "#" plus the
129
+ // hex of its FULL DER encoding (node.bytes), round-tripping intact.
130
+ function attrValueToString(ns) {
131
+ return schema.decode(function (node) {
132
+ try { return asn1.read.string(node); }
133
+ catch (e) {
134
+ if (!e || (e.code !== "asn1/expected-string" && e.code !== "asn1/expected-primitive")) {
135
+ throw ns.E(ns.prefix + "/bad-atv", "malformed string in attribute value: " + ((e && e.message) || String(e)));
136
+ }
137
+ return "#" + node.bytes.toString("hex");
138
+ }
139
+ });
140
+ }
141
+
142
+ function _escapeDnValue(v) {
143
+ return v.replace(/([,+"\\<>;])/g, "\\$1");
144
+ }
145
+
146
+ // Name ::= RDNSequence ::= SEQUENCE OF RelativeDistinguishedName; RDN ::= SET OF
147
+ // AttributeTypeAndValue ::= SEQUENCE { type OID, value ANY }. The atv asserts
148
+ // bare-constructed (min 2) — matching the historical guard that never checked
149
+ // the SEQUENCE tag — and repeated RDN attribute types stay legal (no uniqueness).
150
+ function attributeTypeAndValue(ns) {
151
+ return schema.seq([
152
+ schema.field("type", schema.oidLeaf()),
153
+ schema.field("value", attrValueToString(ns)),
154
+ ], {
155
+ assert: "constructed", arity: { min: 2 }, code: ns.prefix + "/bad-atv", what: "AttributeTypeAndValue",
156
+ build: function (m, ctx) {
157
+ var typeOid = m.fields.type.value;
158
+ return { type: typeOid, name: ctx.oid.name(typeOid) || null, value: m.fields.value.value };
159
+ },
160
+ });
161
+ }
162
+ function relativeDistinguishedName(ns) {
163
+ // RelativeDistinguishedName ::= SET SIZE (1..MAX) — an empty SET {} is malformed.
164
+ return schema.setOf(attributeTypeAndValue(ns), { assert: "set", min: 1, code: ns.prefix + "/bad-rdn", what: "RelativeDistinguishedName" });
165
+ }
166
+ function name(ns) {
167
+ return schema.seqOf(relativeDistinguishedName(ns), {
168
+ assert: "sequence", code: ns.prefix + "/bad-name", what: "Name",
169
+ build: function (m) {
170
+ var rdns = [], parts = [];
171
+ m.items.forEach(function (rdnItem) {
172
+ var atvs = [], atvParts = [];
173
+ rdnItem.value.items.forEach(function (atvItem) {
174
+ var a = atvItem.value.result; // atvItem.value = the atv seq-match; .result = its build result
175
+ atvs.push(a);
176
+ var label = (a.name && DN_SHORT[a.name]) || a.name || a.type;
177
+ atvParts.push(label + "=" + _escapeDnValue(a.value));
178
+ });
179
+ rdns.push(atvs);
180
+ parts.push(atvParts.join("+"));
181
+ });
182
+ return { rdns: rdns, dn: parts.join(", ") };
183
+ },
184
+ });
185
+ }
186
+
187
+ // Extension ::= SEQUENCE { extnID OID, critical BOOLEAN DEFAULT FALSE,
188
+ // extnValue OCTET STRING }. `critical` is a universal BOOLEAN present-by-count
189
+ // (not context-tagged), so the per-extension decode handles the 2-vs-3-child
190
+ // shape directly; the seqOf centralizes the SEQUENCE / SIZE(1..MAX) assertion
191
+ // and the RFC 5280 §4.2 per-OID uniqueness.
192
+ function extension(ns) {
193
+ return schema.decode(function (ext) {
194
+ // Extension ::= SEQUENCE { extnID, critical DEFAULT FALSE, extnValue } — a
195
+ // UNIVERSAL SEQUENCE of exactly 2 (critical omitted) or 3 children. A
196
+ // context-tagged item (e.g. [5]{OID, OCTET STRING}) or a wrong child count
197
+ // is malformed; assert the tag, don't just count children (fail closed).
198
+ if (!ext.children || ext.tagClass !== "universal" || ext.tagNumber !== asn1.TAGS.SEQUENCE ||
199
+ ext.children.length < 2 || ext.children.length > 3) {
200
+ throw ns.E(ns.prefix + "/bad-extension", "Extension must be a SEQUENCE of {extnID, critical?, extnValue}");
201
+ }
202
+ var extnID = asn1.read.oid(ext.children[0]);
203
+ var critical = false, valueNode;
204
+ if (ext.children.length === 3) {
205
+ critical = asn1.read.boolean(ext.children[1]);
206
+ // critical is BOOLEAN DEFAULT FALSE — DER omits the field when false, so an
207
+ // explicitly-encoded FALSE is a non-canonical form; reject it fail-closed.
208
+ if (critical === false) throw ns.E(ns.prefix + "/bad-extension", "an explicit critical FALSE must be omitted (BOOLEAN DEFAULT FALSE)");
209
+ valueNode = ext.children[2];
210
+ } else {
211
+ valueNode = ext.children[1];
212
+ }
213
+ return { oid: extnID, name: ns.oid.name(extnID) || null, critical: critical, value: asn1.read.octetString(valueNode) };
214
+ });
215
+ }
216
+ function extensions(ns) {
217
+ return schema.seqOf(extension(ns), {
218
+ assert: "sequence", min: 1, code: ns.prefix + "/bad-extensions", what: "Extensions",
219
+ unique: function (item) { return item.value.oid; }, dupCode: ns.prefix + "/duplicate-extension",
220
+ build: function (m) { return m.items.map(function (it) { return it.value; }); },
221
+ });
222
+ }
223
+
224
+ module.exports = {
225
+ pemDecode: pemDecode,
226
+ pemEncode: pemEncode,
227
+ coerceToDer: coerceToDer,
228
+ decodeRoot: decodeRoot,
229
+ runParse: runParse,
230
+ DN_SHORT: DN_SHORT,
231
+ algorithmIdentifier: algorithmIdentifier,
232
+ attrValueToString: attrValueToString,
233
+ attributeTypeAndValue: attributeTypeAndValue,
234
+ relativeDistinguishedName: relativeDistinguishedName,
235
+ name: name,
236
+ extension: extension,
237
+ extensions: extensions,
238
+ };
239
+