@blamejs/pki 0.1.4 → 0.1.6

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 CHANGED
@@ -4,6 +4,32 @@ All notable changes to `@blamejs/pki` are documented here. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
5
5
  project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## v0.1.6 — 2026-07-04
8
+
9
+ A declarative ASN.1 structure-schema engine; the X.509 parser is rebuilt on it.
10
+
11
+ ### Added
12
+
13
+ - pki.asn1.schema — a declarative ASN.1 structure-schema engine. A schema is plain data built from combinators (seq / field / optional / explicit / trailing / seqOf / setOf / setOfUnique / choice, plus the value leaves oidLeaf / integerLeaf / boolean / octetString / bitString / any / decode / time); pki.asn1.schema.walk(schema, node, ctx) interprets it against a decoded DER node under an error namespace, enforcing the structural rules — shape assertion, bounds-checked positional reads, optional / context-tagged fields in strictly increasing tag order, SET-OF uniqueness, and fail-closed typed errors — in one place. This is the shared base the certificate parser is built on and the forthcoming CRL / CMS parsers compose, so a new format is declared as data rather than hand-written.
14
+
15
+ ### Changed
16
+
17
+ - pki.x509.parse is now built on the schema engine: the Certificate, tbsCertificate, and every sub-structure (AlgorithmIdentifier, Name, Validity, SubjectPublicKeyInfo, Extensions) are declared as schemas and walked. Every valid certificate parses to the same result as before, and every malformed certificate is still rejected — the full existing test suite passes unchanged. The certificate's structural rules (positional bounds, the trailing-field grammar, extension uniqueness, the signature-algorithm agreement) now live in one auditable place instead of a hand-written decoder, and the format is structurally incapable of the positional-read and duplicate-field bug classes. The parser now validates the full certificate structure before applying cross-field checks, so a certificate carrying more than one defect at once may be rejected with a different (still fail-closed) error than a prior release reported.
18
+
19
+ ## v0.1.5 — 2026-07-04
20
+
21
+ Container healthcheck honors WIKI_PORT; release-tooling supply-chain hardening.
22
+
23
+ ### Fixed
24
+
25
+ - The example wiki container's HEALTHCHECK now probes the port from WIKI_PORT (defaulting to 3009) rather than a hardcoded 3009, so overriding WIKI_PORT at runtime no longer leaves the container reporting unhealthy while the server is serving on the configured port.
26
+
27
+ ### Security
28
+
29
+ - The CI secret-scan gate now fetches the gitleaks binary over authenticated requests and verifies it against the checksums file published in the same release before executing it, so a corrupted or tampered download fails closed instead of running as the gate. Tracking the latest release keeps detection rules current.
30
+ - The release-container workflow validates that the base image resolved to a well-formed sha256 digest before building against it, so a failed resolution can no longer silently produce an unpinned base — the scanned image is always the published one.
31
+ - The workflow-security audit re-runs when its own configuration file changes, so an edit that would suppress a finding is itself audited.
32
+
7
33
  ## v0.1.4 — 2026-07-04
8
34
 
9
35
  The ASN.1 codec's universal-type metadata moves to a single descriptor registry.
package/index.js CHANGED
@@ -25,6 +25,7 @@
25
25
  var constants = require("./lib/constants");
26
26
  var errors = require("./lib/framework-error");
27
27
  var asn1 = require("./lib/asn1-der");
28
+ var asn1Schema = require("./lib/asn1-schema");
28
29
  var oid = require("./lib/oid");
29
30
  var webcrypto = require("./lib/webcrypto");
30
31
  var x509 = require("./lib/x509");
@@ -35,7 +36,9 @@ module.exports = {
35
36
  C: constants,
36
37
  constants: constants,
37
38
  errors: errors,
38
- asn1: asn1,
39
+ // `asn1.schema` (L2) is the declarative structure-schema engine, exposed on
40
+ // the asn1 namespace alongside the codec (decode/encode/build/read/TAGS).
41
+ asn1: Object.assign({}, asn1, { schema: asn1Schema }),
39
42
  oid: oid,
40
43
  x509: x509,
41
44
  // A ready W3C Crypto instance (globalThis.crypto shape) + the classes
@@ -0,0 +1,340 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ /**
5
+ * @module pki.asn1.schema
6
+ * @nav ASN.1
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. Default: a context
113
+ // [tag] (the certificate version [0] shape). `whenAny`: the next element
114
+ // whatever its tag — an OPTIONAL ANY like AlgorithmIdentifier.parameters. The
115
+ // recognizer lets _walkSeq CONSUME the element so a closed sequence can reject
116
+ // whatever is left over (without it, a trailing ANY looks unconsumed).
117
+ var match = opts.whenAny
118
+ ? function () { return true; }
119
+ : function (n) { return n.tagClass === "context" && n.tagNumber === opts.tag; };
120
+ return { fkind: "optional", name: name, schema: schema, tag: opts.tag, match: match,
121
+ explicit: !!opts.explicit, emptyCode: opts.emptyCode, hasDefault: ("default" in opts), def: opts.default };
122
+ }
123
+ // trailing: the [minTag..maxTag] optional context fields, each at most once in
124
+ // strictly-increasing tag order (the tbs issuerUniqueID[1]/subjectUniqueID[2]/
125
+ // extensions[3] block). members: [{ tag, name, schema, explicit? }].
126
+ function trailing(members, opts) {
127
+ opts = opts || {};
128
+ return { fkind: "trailing", members: members, minTag: opts.minTag, maxTag: opts.maxTag,
129
+ unexpectedCode: opts.unexpectedCode, orderCode: opts.orderCode };
130
+ }
131
+
132
+ // ---- structural combinators -----------------------------------------
133
+
134
+ function seq(fields, opts) {
135
+ opts = opts || {};
136
+ return { kind: "seq", fields: fields, assert: opts.assert || "sequence", arity: opts.arity,
137
+ code: opts.code, what: opts.what, build: opts.build, checks: opts.checks || [] };
138
+ }
139
+
140
+ // A NON-optional EXPLICIT context wrapper (CMS ContentInfo; the tbs [3] uses it
141
+ // inside `trailing`). Asserts context class + tag, constructed, >=1 child, then
142
+ // walks the inner schema on children[0].
143
+ function explicit(tag, schema, opts) {
144
+ opts = opts || {};
145
+ return { kind: "explicit", tag: tag, schema: schema, emptyCode: opts.emptyCode, code: opts.code, what: opts.what };
146
+ }
147
+
148
+ function choice(alts, opts) {
149
+ opts = opts || {};
150
+ return { kind: "choice", alts: alts, code: opts.code, what: opts.what };
151
+ }
152
+
153
+ function seqOf(item, opts) {
154
+ opts = opts || {};
155
+ return { kind: "repeat", item: item, assert: opts.assert || "sequence", code: opts.code, what: opts.what,
156
+ min: opts.min, unique: opts.unique, dupCode: opts.dupCode, build: opts.build };
157
+ }
158
+ function setOf(item, opts) {
159
+ opts = opts || {};
160
+ return { kind: "repeat", item: item, assert: opts.assert || "set", code: opts.code, what: opts.what,
161
+ min: opts.min, unique: opts.unique, dupCode: opts.dupCode, build: opts.build };
162
+ }
163
+ function setOfUnique(item, keyFn, opts) {
164
+ return setOf(item, Object.assign({ unique: keyFn }, opts || {}));
165
+ }
166
+
167
+ // ---- the walk engine -------------------------------------------------
168
+
169
+ /**
170
+ * @primitive pki.asn1.schema.walk
171
+ * @signature pki.asn1.schema.walk(schema, node, ctx) -> value
172
+ * @since 0.1.6
173
+ * @status experimental
174
+ * @spec X.690, X.680
175
+ * @related pki.asn1.decode, pki.x509.parse
176
+ *
177
+ * Interpret a declarative schema against a decoded DER node, enforcing the
178
+ * schema's structural rules (shape assertion, arity, optional / context-tagged
179
+ * fields in increasing tag order, SET-OF uniqueness) and returning the built
180
+ * value — or the match tree (`{ node, fields | items }`, with the build output
181
+ * on `.result`) for a structure with no build fn. `ctx = { E, prefix, oid }`
182
+ * supplies the typed-error constructor, the error-code family prefix, and the
183
+ * OID registry a build fn resolves names through.
184
+ *
185
+ * The schema is assembled from the combinators this module exports — structural
186
+ * (`seq` / `field` / `optional` / `explicit` / `trailing` / `seqOf` / `setOf` /
187
+ * `setOfUnique` / `choice`) and value (`oidLeaf` / `integerLeaf` / `boolean` /
188
+ * `octetString` / `bitString` / `any` / `decode` / `time`).
189
+ *
190
+ * @example
191
+ * var S = pki.asn1.schema;
192
+ * var ALGID = S.seq([S.field("algorithm", S.oidLeaf())],
193
+ * { assert: "sequence", arity: { min: 1 }, code: "app/bad-alg" });
194
+ * S.walk(ALGID, pki.asn1.decode(der), { prefix: "app", E: MyError, oid: pki.oid });
195
+ */
196
+ function walk(schema, node, ctx) {
197
+ switch (schema.kind) {
198
+ case "leaf": return schema.read(node);
199
+ case "any": return node;
200
+ case "decode": return schema.fn(node, ctx);
201
+ case "seq": return _walkSeq(schema, node, ctx);
202
+ case "explicit": return _walkExplicit(schema, node, ctx);
203
+ case "repeat": return _walkRepeat(schema, node, ctx);
204
+ case "choice": return _walkChoice(schema, node, ctx);
205
+ default: _fail(ctx, (ctx.prefix || "schema") + "/bad-schema", "unknown schema kind " + JSON.stringify(schema.kind));
206
+ }
207
+ return undefined;
208
+ }
209
+
210
+ // An EXPLICIT [tag] wrapper carries EXACTLY ONE inner value; 0 children is
211
+ // empty and 2+ would silently drop all but the first (the same drop-extra
212
+ // fail-open the seq guard closes). Assert exactly one, return it.
213
+ function _explicitInner(node, tag, ctx, code) {
214
+ if (!node.children || node.children.length !== 1) {
215
+ _fail(ctx, code, "EXPLICIT [" + tag + "] must wrap exactly one value");
216
+ }
217
+ return node.children[0];
218
+ }
219
+
220
+ function _walkExplicit(schema, node, ctx) {
221
+ if (node.tagClass !== "context" || node.tagNumber !== schema.tag) {
222
+ _fail(ctx, schema.emptyCode || schema.code, "expected an EXPLICIT [" + schema.tag + "] wrapper");
223
+ }
224
+ return walk(schema.schema, _explicitInner(node, schema.tag, ctx, schema.emptyCode || schema.code), ctx);
225
+ }
226
+
227
+ function _walkChoice(schema, node, ctx) {
228
+ for (var i = 0; i < schema.alts.length; i++) {
229
+ var w = schema.alts[i].when;
230
+ if (node.tagClass === w.tagClass && node.tagNumber === w.tagNumber) {
231
+ return walk(schema.alts[i].schema, node, ctx);
232
+ }
233
+ }
234
+ _fail(ctx, schema.code, "no CHOICE alternative matched " + node.tagClass + "/" + node.tagNumber);
235
+ return undefined;
236
+ }
237
+
238
+ function _walkRepeat(schema, node, ctx) {
239
+ var kids = _assertShape(schema, node, ctx);
240
+ if (schema.min != null && kids.length < schema.min) {
241
+ _fail(ctx, schema.code, (schema.what || "value") + " must contain at least " + schema.min + " element(s)");
242
+ }
243
+ var items = [];
244
+ var seen = schema.unique ? new Set() : null;
245
+ for (var i = 0; i < kids.length; i++) {
246
+ // The item wrapper is the SAME shape unique() and build() see: { node,
247
+ // value: <walk result> }. keyFn reads off item.value just like build does.
248
+ var item = { node: kids[i], value: walk(schema.item, kids[i], ctx) };
249
+ if (seen) {
250
+ var key = schema.unique(item);
251
+ if (seen.has(key)) _fail(ctx, schema.dupCode || schema.code, "duplicate element " + key);
252
+ seen.add(key);
253
+ }
254
+ items.push(item);
255
+ }
256
+ var match = { kind: "repeat", node: node, items: items };
257
+ if (schema.build) match.result = schema.build(match, ctx);
258
+ return match;
259
+ }
260
+
261
+ function _walkSeq(schema, node, ctx) {
262
+ var kids = _assertShape(schema, node, ctx);
263
+ _assertArity(schema, kids, ctx);
264
+
265
+ var fields = {};
266
+ var idx = 0;
267
+ for (var f = 0; f < schema.fields.length; f++) {
268
+ var fld = schema.fields[f];
269
+ if (fld.fkind === "required") {
270
+ if (idx >= kids.length) _fail(ctx, schema.code, "missing required field " + JSON.stringify(fld.name));
271
+ var child = kids[idx++];
272
+ fields[fld.name] = { node: child, value: walk(fld.schema, child, ctx) };
273
+ } else if (fld.fkind === "optional") {
274
+ var next = idx < kids.length ? kids[idx] : null;
275
+ if (next && fld.match(next)) {
276
+ idx++;
277
+ var inner = next;
278
+ if (fld.explicit) {
279
+ inner = _explicitInner(next, fld.tag, ctx, fld.emptyCode || schema.code);
280
+ }
281
+ fields[fld.name] = { node: next, present: true, value: walk(fld.schema, inner, ctx) };
282
+ } else {
283
+ fields[fld.name] = { present: false, value: fld.hasDefault ? fld.def : undefined };
284
+ }
285
+ } else if (fld.fkind === "trailing") {
286
+ _consumeTrailing(fld, kids, idx, fields, ctx);
287
+ idx = kids.length;
288
+ }
289
+ }
290
+
291
+ // Every child must be consumed by a field. A leftover element (no trailing
292
+ // field ran to absorb it) is malformed — dropping it silently would let a
293
+ // closed sequence of optional fields accept a duplicate/extra element.
294
+ if (idx < kids.length) {
295
+ _fail(ctx, schema.code, (schema.what || "value") + " has an unexpected element after its last field");
296
+ }
297
+
298
+ var match = { kind: "seq", node: node, fields: fields };
299
+ for (var c = 0; c < schema.checks.length; c++) schema.checks[c](match, ctx);
300
+ if (schema.build) match.result = schema.build(match, ctx);
301
+ return match;
302
+ }
303
+
304
+ function _consumeTrailing(fld, kids, start, fields, ctx) {
305
+ var byTag = {};
306
+ for (var m = 0; m < fld.members.length; m++) byTag[fld.members[m].tag] = fld.members[m];
307
+ // The monotonic-order sentinel must start below the lowest accepted tag.
308
+ // Context tag numbers are non-negative, so -1 is below any member tag when
309
+ // minTag is absent — otherwise a trailing block whose first member is [0]
310
+ // would reject that field (0 <= last==0) as repeated/out-of-order.
311
+ var last = fld.minTag != null ? fld.minTag - 1 : -1;
312
+ for (var i = start; i < kids.length; i++) {
313
+ var t = kids[i];
314
+ if (t.tagClass !== "context" || (fld.minTag != null && t.tagNumber < fld.minTag) || (fld.maxTag != null && t.tagNumber > fld.maxTag) || !byTag[t.tagNumber]) {
315
+ _fail(ctx, fld.unexpectedCode, "unexpected trailing field [" + (t.tagClass === "context" ? t.tagNumber : t.tagClass) + "]");
316
+ }
317
+ if (t.tagNumber <= last) _fail(ctx, fld.orderCode, "trailing field [" + t.tagNumber + "] is repeated or out of order");
318
+ last = t.tagNumber;
319
+ var member = byTag[t.tagNumber];
320
+ var inner = t;
321
+ if (member.explicit) {
322
+ inner = _explicitInner(t, t.tagNumber, ctx, member.emptyCode);
323
+ }
324
+ fields[member.name] = { node: t, present: true, value: walk(member.schema, inner, ctx) };
325
+ }
326
+ for (var n = 0; n < fld.members.length; n++) {
327
+ if (!fields[fld.members[n].name]) fields[fld.members[n].name] = { present: false, value: undefined };
328
+ }
329
+ }
330
+
331
+ module.exports = {
332
+ // structural
333
+ seq: seq, field: field, optional: optional, explicit: explicit, trailing: trailing,
334
+ seqOf: seqOf, setOf: setOf, setOfUnique: setOfUnique, choice: choice,
335
+ // leaves
336
+ oidLeaf: oidLeaf, integerLeaf: integerLeaf, boolean: boolean, octetString: octetString,
337
+ bitString: bitString, any: any, decode: decode, time: time,
338
+ // engine
339
+ walk: walk,
340
+ };
package/lib/x509.js CHANGED
@@ -31,14 +31,13 @@
31
31
 
32
32
  var constants = require("./constants");
33
33
  var asn1 = require("./asn1-der");
34
+ var schema = require("./asn1-schema");
34
35
  var oid = require("./oid");
35
36
  var frameworkError = require("./framework-error");
36
37
 
37
38
  var CertificateError = frameworkError.CertificateError;
38
39
  var PemError = frameworkError.PemError;
39
40
 
40
- var TAGS = asn1.TAGS;
41
-
42
41
  // Distinguished-name attribute short labels (RFC 4514 §3 + common use).
43
42
  var DN_SHORT = {
44
43
  commonName: "CN",
@@ -109,126 +108,236 @@ function pemEncode(der, label) {
109
108
 
110
109
  // ---- helpers ---------------------------------------------------------
111
110
 
112
- function _algId(node) {
113
- if (node.tagClass !== "universal" || node.tagNumber !== TAGS.SEQUENCE || !node.children.length) {
114
- throw new CertificateError("x509/bad-algorithm-identifier", "AlgorithmIdentifier must be a non-empty SEQUENCE");
115
- }
116
- var dotted = asn1.read.oid(node.children[0]);
117
- return {
118
- oid: dotted,
119
- name: oid.name(dotted) || null,
120
- parameters: node.children.length > 1 ? node.children[1].bytes : null,
121
- };
111
+ // The x509 error namespace the schema engine walks under: prefix names the
112
+ // error family and E constructs the typed CertificateError. The shared PKIX
113
+ // sub-schemas below are ns-parameterized FACTORIES so crl.js / cms.js can later
114
+ // reuse them while emitting their own crl/*, cms/* codes (hoist to a shared
115
+ // lib/pkix-schema.js when that second consumer lands).
116
+ var NS = { prefix: "x509", E: function (code, message) { return new CertificateError(code, message); }, oid: oid };
117
+
118
+ // AlgorithmIdentifier ::= SEQUENCE { algorithm OBJECT IDENTIFIER, parameters ANY OPTIONAL }
119
+ function algorithmIdentifier(ns) {
120
+ return schema.seq([
121
+ schema.field("algorithm", schema.oidLeaf()),
122
+ schema.optional("parameters", schema.any(), { whenAny: true }),
123
+ ], {
124
+ assert: "sequence", arity: { min: 1 }, code: ns.prefix + "/bad-algorithm-identifier", what: "AlgorithmIdentifier",
125
+ build: function (m, ctx) {
126
+ var dotted = m.fields.algorithm.value;
127
+ return { oid: dotted, name: ctx.oid.name(dotted) || null, parameters: m.fields.parameters.present ? m.fields.parameters.node.bytes : null };
128
+ },
129
+ });
122
130
  }
131
+ var ALGORITHM_IDENTIFIER = algorithmIdentifier(NS);
132
+
133
+ function _algId(node) { return schema.walk(ALGORITHM_IDENTIFIER, node, NS).result; }
123
134
 
124
- function _attrValueToString(node) {
125
- try { return asn1.read.string(node); }
126
- catch (e) {
127
- // A malformed KNOWN string type (invalid UTF-8, a non-IA5 byte, a
128
- // PrintableString character outside its set, ...) is a malformed
129
- // certificate and must fail closed do NOT hex-encode it away, or the
130
- // decoder's strict string validation is silently bypassed on the DN path.
131
- // Those surface as asn1/bad-* content errors; raise a CertificateError so
132
- // parse fails closed on the documented x509/* contract.
133
- //
134
- // A value that simply is not a decodable primitive string is NOT malformed
135
- // and must stay representable: an ANY-typed non-string tag
136
- // (asn1/expected-string) or a constructed universal type such as a SEQUENCE
137
- // (asn1/expected-primitive). Per RFC 4514 §2.4 it is rendered as "#" plus
138
- // the hex of its FULL DER encoding (tag + length + content node.bytes),
139
- // so a constructed value round-trips intact rather than being dropped or
140
- // rejecting the whole certificate.
141
- if (!e || (e.code !== "asn1/expected-string" && e.code !== "asn1/expected-primitive")) {
142
- throw new CertificateError("x509/bad-atv", "malformed string in attribute value: " + ((e && e.message) || String(e)));
135
+ // attrValueToString(ns): the AttributeValue decode-leaf. A malformed KNOWN
136
+ // string type (invalid UTF-8, a non-IA5 byte, a PrintableString character
137
+ // outside its set, ...) surfaces as an asn1/bad-* content error and must fail
138
+ // the certificate closed do NOT hex-encode it away, or the decoder's strict
139
+ // string validation is silently bypassed on the DN path. A value that is simply
140
+ // not a decodable primitive string is NOT malformed and stays representable: an
141
+ // ANY-typed non-string tag (asn1/expected-string) or a constructed universal
142
+ // type such as a SEQUENCE (asn1/expected-primitive) renders per RFC 4514 §2.4 as
143
+ // "#" plus the hex of its FULL DER encoding (node.bytes), round-tripping intact.
144
+ function attrValueToString(ns) {
145
+ return schema.decode(function (node) {
146
+ try { return asn1.read.string(node); }
147
+ catch (e) {
148
+ if (!e || (e.code !== "asn1/expected-string" && e.code !== "asn1/expected-primitive")) {
149
+ throw ns.E(ns.prefix + "/bad-atv", "malformed string in attribute value: " + ((e && e.message) || String(e)));
150
+ }
151
+ return "#" + node.bytes.toString("hex");
143
152
  }
144
- return "#" + node.bytes.toString("hex");
145
- }
153
+ });
146
154
  }
147
155
 
148
156
  function _escapeDnValue(v) {
149
157
  return v.replace(/([,+"\\<>;])/g, "\\$1");
150
158
  }
151
159
 
152
- function _parseName(node) {
153
- if (node.tagClass !== "universal" || node.tagNumber !== TAGS.SEQUENCE) {
154
- throw new CertificateError("x509/bad-name", "Name must be an RDNSequence (SEQUENCE)");
155
- }
156
- var rdns = [];
157
- var parts = [];
158
- for (var i = 0; i < node.children.length; i++) {
159
- var rdn = node.children[i];
160
- if (rdn.tagClass !== "universal" || rdn.tagNumber !== TAGS.SET) {
161
- throw new CertificateError("x509/bad-rdn", "RelativeDistinguishedName must be a SET");
162
- }
163
- var atvs = [];
164
- var atvParts = [];
165
- for (var j = 0; j < rdn.children.length; j++) {
166
- var atv = rdn.children[j];
167
- if (!atv.children || atv.children.length < 2) {
168
- throw new CertificateError("x509/bad-atv", "AttributeTypeAndValue must be a SEQUENCE of {type, value}");
169
- }
170
- var typeOid = asn1.read.oid(atv.children[0]);
171
- var typeName = oid.name(typeOid);
172
- var value = _attrValueToString(atv.children[1]);
173
- atvs.push({ type: typeOid, name: typeName || null, value: value });
174
- var label = (typeName && DN_SHORT[typeName]) || typeName || typeOid;
175
- atvParts.push(label + "=" + _escapeDnValue(value));
176
- }
177
- rdns.push(atvs);
178
- parts.push(atvParts.join("+"));
179
- }
180
- return { rdns: rdns, dn: parts.join(", ") };
160
+ // Name ::= RDNSequence ::= SEQUENCE OF RelativeDistinguishedName; RDN ::= SET OF
161
+ // AttributeTypeAndValue ::= SEQUENCE { type OID, value ANY }. The atv asserts
162
+ // bare-constructed (min 2) matching the historical guard that never checked
163
+ // the SEQUENCE tag — and repeated RDN attribute types stay legal (no uniqueness).
164
+ function attributeTypeAndValue(ns) {
165
+ return schema.seq([
166
+ schema.field("type", schema.oidLeaf()),
167
+ schema.field("value", attrValueToString(ns)),
168
+ ], {
169
+ assert: "constructed", arity: { min: 2 }, code: ns.prefix + "/bad-atv", what: "AttributeTypeAndValue",
170
+ build: function (m, ctx) {
171
+ var typeOid = m.fields.type.value;
172
+ return { type: typeOid, name: ctx.oid.name(typeOid) || null, value: m.fields.value.value };
173
+ },
174
+ });
181
175
  }
182
-
183
- function _parseValidityTime(node) {
184
- if (node.tagClass !== "universal" || (node.tagNumber !== TAGS.UTC_TIME && node.tagNumber !== TAGS.GENERALIZED_TIME)) {
185
- throw new CertificateError("x509/bad-time", "Validity time must be UTCTime or GeneralizedTime");
186
- }
187
- return asn1.read.time(node);
176
+ function relativeDistinguishedName(ns) {
177
+ return schema.setOf(attributeTypeAndValue(ns), { assert: "set", code: ns.prefix + "/bad-rdn", what: "RelativeDistinguishedName" });
178
+ }
179
+ function name(ns) {
180
+ return schema.seqOf(relativeDistinguishedName(ns), {
181
+ assert: "sequence", code: ns.prefix + "/bad-name", what: "Name",
182
+ build: function (m) {
183
+ var rdns = [], parts = [];
184
+ m.items.forEach(function (rdnItem) {
185
+ var atvs = [], atvParts = [];
186
+ rdnItem.value.items.forEach(function (atvItem) {
187
+ var a = atvItem.value.result; // atvItem.value = the atv seq-match; .value = its build result
188
+ atvs.push(a);
189
+ var label = (a.name && DN_SHORT[a.name]) || a.name || a.type;
190
+ atvParts.push(label + "=" + _escapeDnValue(a.value));
191
+ });
192
+ rdns.push(atvs);
193
+ parts.push(atvParts.join("+"));
194
+ });
195
+ return { rdns: rdns, dn: parts.join(", ") };
196
+ },
197
+ });
188
198
  }
199
+ var NAME = name(NS);
189
200
 
190
- function _parseExtensions(seqNode) {
191
- // RFC 5280 §4.1.2.9 — Extensions ::= SEQUENCE SIZE (1..MAX) OF Extension.
192
- // The wrapped value must be a SEQUENCE (not a primitive, which would read
193
- // seqNode.children as null and throw a raw TypeError) and must carry at
194
- // least one extension.
195
- if (seqNode.tagClass !== "universal" || seqNode.tagNumber !== TAGS.SEQUENCE || !seqNode.children) {
196
- throw new CertificateError("x509/bad-extensions", "extensions [3] must wrap a SEQUENCE");
197
- }
198
- if (seqNode.children.length === 0) {
199
- throw new CertificateError("x509/bad-extensions", "extensions SEQUENCE must contain at least one extension");
200
- }
201
- var out = [];
202
- var seen = new Set();
203
- for (var i = 0; i < seqNode.children.length; i++) {
204
- var ext = seqNode.children[i];
201
+ function _parseName(node) { return schema.walk(NAME, node, NS).result; }
202
+
203
+ // Validity ::= SEQUENCE { notBefore Time, notAfter Time }. The historical guard
204
+ // checked only children.length===2 (not the SEQUENCE tag) -> assert "constructed".
205
+ var VALIDITY = schema.seq([
206
+ schema.field("notBefore", schema.time(NS)),
207
+ schema.field("notAfter", schema.time(NS)),
208
+ ], {
209
+ assert: "constructed", arity: { exact: 2 }, code: "x509/bad-validity", what: "Validity",
210
+ build: function (m) { return { notBefore: m.fields.notBefore.value, notAfter: m.fields.notAfter.value }; },
211
+ });
212
+
213
+ // SubjectPublicKeyInfo ::= SEQUENCE { algorithm AlgorithmIdentifier, subjectPublicKey BIT STRING }.
214
+ var SPKI = schema.seq([
215
+ schema.field("algorithm", ALGORITHM_IDENTIFIER),
216
+ schema.field("subjectPublicKey", schema.bitString()),
217
+ ], {
218
+ assert: "constructed", arity: { exact: 2 }, code: "x509/bad-spki", what: "SubjectPublicKeyInfo",
219
+ build: function (m) {
220
+ return {
221
+ algorithm: m.fields.algorithm.value.result, // algorithm field = ALGORITHM_IDENTIFIER seq-match; .value = its build
222
+ publicKey: { unusedBits: m.fields.subjectPublicKey.value.unusedBits, bytes: m.fields.subjectPublicKey.value.bytes },
223
+ bytes: m.node.bytes,
224
+ };
225
+ },
226
+ });
227
+
228
+ // Extension ::= SEQUENCE { extnID OID, critical BOOLEAN DEFAULT FALSE,
229
+ // extnValue OCTET STRING }. `critical` is a universal BOOLEAN present-by-count
230
+ // (not context-tagged), so the per-extension decode handles the 2-vs-3-child
231
+ // shape directly; the seqOf centralizes the SEQUENCE / SIZE(1..MAX) assertion
232
+ // and the RFC 5280 §4.2 per-OID uniqueness.
233
+ function extension(ns) {
234
+ return schema.decode(function (ext) {
205
235
  if (!ext.children || ext.children.length < 2) {
206
- throw new CertificateError("x509/bad-extension", "Extension must be a SEQUENCE");
236
+ throw ns.E(ns.prefix + "/bad-extension", "Extension must be a SEQUENCE");
207
237
  }
208
238
  var extnID = asn1.read.oid(ext.children[0]);
209
- // RFC 5280 §4.2 — a certificate MUST NOT include more than one
210
- // instance of a particular extension.
211
- if (seen.has(extnID)) {
212
- throw new CertificateError("x509/duplicate-extension", "certificate repeats extension " + extnID);
239
+ var critical = false, valueNode;
240
+ if (ext.children.length === 3) { critical = asn1.read.boolean(ext.children[1]); valueNode = ext.children[2]; }
241
+ else { valueNode = ext.children[1]; }
242
+ return { oid: extnID, name: ns.oid.name(extnID) || null, critical: critical, value: asn1.read.octetString(valueNode) };
243
+ });
244
+ }
245
+ function extensions(ns) {
246
+ return schema.seqOf(extension(ns), {
247
+ assert: "sequence", min: 1, code: ns.prefix + "/bad-extensions", what: "Extensions",
248
+ unique: function (item) { return item.value.oid; }, dupCode: ns.prefix + "/duplicate-extension",
249
+ build: function (m) { return m.items.map(function (it) { return it.value; }); },
250
+ });
251
+ }
252
+ var EXTENSIONS = extensions(NS);
253
+
254
+ function _parseExtensions(seqNode) { return schema.walk(EXTENSIONS, seqNode, NS).result; }
255
+
256
+ // Version ::= INTEGER { v1(0), v2(1), v3(2) }, [0] EXPLICIT DEFAULT v1. Read as a
257
+ // BigInt so an out-of-range value can't coerce to a float; reject an explicitly-
258
+ // encoded v1 (DER forbids encoding the DEFAULT).
259
+ function readVersion(ns) {
260
+ return schema.decode(function (n) {
261
+ var v = asn1.read.integer(n);
262
+ if (v === 0n) throw ns.E(ns.prefix + "/bad-version", "DER forbids explicitly encoding the default version v1");
263
+ if (v === 1n) return 2;
264
+ if (v === 2n) return 3;
265
+ throw ns.E(ns.prefix + "/bad-version", "unsupported certificate version " + v.toString());
266
+ });
267
+ }
268
+
269
+ // TBSCertificate ::= SEQUENCE { version [0] EXPLICIT DEFAULT v1, serialNumber
270
+ // INTEGER, signature AlgorithmIdentifier, issuer Name, validity Validity,
271
+ // subject Name, subjectPublicKeyInfo SubjectPublicKeyInfo, issuerUniqueID [1]
272
+ // IMPLICIT OPTIONAL, subjectUniqueID [2] IMPLICIT OPTIONAL, extensions [3]
273
+ // EXPLICIT OPTIONAL }. The trailing fields are at-most-once, in increasing tag
274
+ // order (the engine enforces it); only extensions are surfaced.
275
+ var CERTIFICATE_TBS = schema.seq([
276
+ schema.optional("version", readVersion(NS), { tag: 0, explicit: true, emptyCode: "x509/bad-version", default: 1 }),
277
+ schema.field("serialNumber", schema.integerLeaf()),
278
+ schema.field("signature", ALGORITHM_IDENTIFIER),
279
+ schema.field("issuer", NAME),
280
+ schema.field("validity", VALIDITY),
281
+ schema.field("subject", NAME),
282
+ schema.field("subjectPublicKeyInfo", SPKI),
283
+ schema.trailing([
284
+ { tag: 1, name: "issuerUniqueID", schema: schema.any() },
285
+ { tag: 2, name: "subjectUniqueID", schema: schema.any() },
286
+ { tag: 3, name: "extensions", schema: EXTENSIONS, explicit: true, emptyCode: "x509/bad-extensions" },
287
+ ], { minTag: 1, maxTag: 3, unexpectedCode: "x509/bad-tbs", orderCode: "x509/bad-tbs" }),
288
+ ], { assert: "constructed", code: "x509/bad-tbs", what: "tbsCertificate" });
289
+
290
+ // Certificate ::= SEQUENCE { tbsCertificate, signatureAlgorithm, signatureValue
291
+ // BIT STRING }. The build runs the RFC 5280 cross-field checks (§4.1.1.2 sig-alg
292
+ // agreement, §4.1.2.4 non-empty issuer, §4.1.2.9 extensions-only-in-v3) after the
293
+ // structural walk, then assembles the public parse result. Raw-byte accessors
294
+ // (serialNumberHex, tbsBytes, sig-alg agreement) read off the match-tree nodes.
295
+ var CERTIFICATE = schema.seq([
296
+ schema.field("tbsCertificate", CERTIFICATE_TBS),
297
+ schema.field("signatureAlgorithm", ALGORITHM_IDENTIFIER),
298
+ schema.field("signatureValue", schema.bitString()),
299
+ ], {
300
+ assert: "sequence", arity: { exact: 3 }, code: "x509/not-a-certificate", what: "Certificate",
301
+ build: function (m, ctx) {
302
+ var tbs = m.fields.tbsCertificate.value; // CERTIFICATE_TBS seq-match (no build)
303
+
304
+ // RFC 5280 §4.1.1.2 — outer signatureAlgorithm MUST equal tbsCertificate.signature.
305
+ if (!m.fields.signatureAlgorithm.node.bytes.equals(tbs.fields.signature.node.bytes)) {
306
+ throw ctx.E("x509/bad-signature-algorithm", "signatureAlgorithm must match tbsCertificate.signature (RFC 5280 §4.1.1.2)");
213
307
  }
214
- seen.add(extnID);
215
- var critical = false;
216
- var valueNode;
217
- if (ext.children.length === 3) {
218
- critical = asn1.read.boolean(ext.children[1]);
219
- valueNode = ext.children[2];
220
- } else {
221
- valueNode = ext.children[1];
308
+
309
+ var version = tbs.fields.version.value; // 1 (default) | 2 | 3
310
+ var issuer = tbs.fields.issuer.value.result;
311
+ // RFC 5280 §4.1.2.4 issuer MUST be non-empty (an empty subject is permitted, §4.1.2.6).
312
+ if (!issuer.rdns.length) {
313
+ throw ctx.E("x509/bad-issuer", "issuer must be a non-empty distinguished name");
222
314
  }
223
- out.push({
224
- oid: extnID,
225
- name: oid.name(extnID) || null,
226
- critical: critical,
227
- value: asn1.read.octetString(valueNode),
228
- });
229
- }
230
- return out;
231
- }
315
+
316
+ var extField = tbs.fields.extensions;
317
+ var hasExtensions = !!(extField && extField.present);
318
+ // RFC 5280 §4.1.2.9 — extensions appear only in a v3 certificate.
319
+ if (hasExtensions && version !== 3) {
320
+ throw ctx.E("x509/bad-version", "extensions are only permitted in a v3 certificate");
321
+ }
322
+
323
+ var serialNode = tbs.fields.serialNumber.node;
324
+ var sigBits = m.fields.signatureValue.value; // { unusedBits, bytes }
325
+ return {
326
+ version: version,
327
+ serialNumber: tbs.fields.serialNumber.value,
328
+ serialNumberHex: serialNode.content.toString("hex"),
329
+ signatureAlgorithm: m.fields.signatureAlgorithm.value.result,
330
+ tbsSignatureAlgorithm: tbs.fields.signature.value.result,
331
+ issuer: issuer,
332
+ subject: tbs.fields.subject.value.result,
333
+ validity: tbs.fields.validity.value.result,
334
+ subjectPublicKeyInfo: tbs.fields.subjectPublicKeyInfo.value.result,
335
+ extensions: hasExtensions ? extField.value.result : [],
336
+ tbsBytes: tbs.node.bytes,
337
+ signatureValue: { unusedBits: sigBits.unusedBits, bytes: sigBits.bytes },
338
+ };
339
+ },
340
+ });
232
341
 
233
342
  // ---- parse -----------------------------------------------------------
234
343
 
@@ -274,134 +383,7 @@ function parse(input) {
274
383
  } catch (e) {
275
384
  throw new CertificateError("x509/bad-der", "certificate DER did not decode: " + e.message, e);
276
385
  }
277
- if (root.tagClass !== "universal" || root.tagNumber !== TAGS.SEQUENCE || !root.children || root.children.length !== 3) {
278
- throw new CertificateError("x509/not-a-certificate", "Certificate must be a SEQUENCE of {tbsCertificate, signatureAlgorithm, signature}");
279
- }
280
-
281
- var tbs = root.children[0];
282
- var outerSigAlg = root.children[1];
283
- var sigValueNode = root.children[2];
284
- if (!tbs.children || !tbs.children.length) {
285
- throw new CertificateError("x509/bad-tbs", "tbsCertificate is empty");
286
- }
287
-
288
- var idx = 0;
289
- var version;
290
- var first = tbs.children[0];
291
- if (first.tagClass === "context" && first.tagNumber === 0) {
292
- if (!first.children || !first.children.length) throw new CertificateError("x509/bad-version", "version [0] must wrap an INTEGER");
293
- // RFC 5280 §4.1.2.1 — version is INTEGER { v1(0), v2(1), v3(2) }; read
294
- // it as a BigInt so an out-of-range value can't be coerced to a float,
295
- // and reject an explicitly-encoded v1 (DER forbids encoding the DEFAULT).
296
- var versionValue = asn1.read.integer(first.children[0]);
297
- if (versionValue === 0n) {
298
- throw new CertificateError("x509/bad-version", "DER forbids explicitly encoding the default version v1");
299
- } else if (versionValue === 1n) {
300
- version = 2;
301
- } else if (versionValue === 2n) {
302
- version = 3;
303
- } else {
304
- throw new CertificateError("x509/bad-version", "unsupported certificate version " + versionValue.toString());
305
- }
306
- idx = 1;
307
- } else {
308
- version = 1;
309
- }
310
-
311
- // Six positional fields follow any [0] version; a shorter tbs would read
312
- // `undefined` and throw a raw TypeError on the first property access.
313
- if (tbs.children.length < idx + 6) {
314
- throw new CertificateError("x509/bad-tbs", "tbsCertificate is too short");
315
- }
316
-
317
- var serialNode = tbs.children[idx++];
318
- var serialNumber = asn1.read.integer(serialNode);
319
- var innerSigAlg = tbs.children[idx++];
320
- var issuer = _parseName(tbs.children[idx++]);
321
- var validityNode = tbs.children[idx++];
322
- var subject = _parseName(tbs.children[idx++]);
323
- var spkiNode = tbs.children[idx++];
324
-
325
- // RFC 5280 §4.1.1.2 — the outer signatureAlgorithm MUST contain the same
326
- // AlgorithmIdentifier (OID and parameters) as tbsCertificate.signature. A
327
- // mismatch is a signature-algorithm-substitution vector, so compare the full
328
- // DER of both fields rather than surfacing two disagreeing algorithms.
329
- if (!outerSigAlg.bytes.equals(innerSigAlg.bytes)) {
330
- throw new CertificateError("x509/bad-signature-algorithm", "signatureAlgorithm must match tbsCertificate.signature (RFC 5280 §4.1.1.2)");
331
- }
332
-
333
- // RFC 5280 §4.1.2.4 — the issuer field MUST contain a non-empty distinguished
334
- // name. (An empty subject is permitted when a subjectAltName carries the
335
- // identity — §4.1.2.6 — so only the issuer is required non-empty here.)
336
- if (!issuer.rdns.length) {
337
- throw new CertificateError("x509/bad-issuer", "issuer must be a non-empty distinguished name");
338
- }
339
-
340
- if (!validityNode.children || validityNode.children.length !== 2) {
341
- throw new CertificateError("x509/bad-validity", "Validity must be a SEQUENCE of {notBefore, notAfter}");
342
- }
343
- var validity = {
344
- notBefore: _parseValidityTime(validityNode.children[0]),
345
- notAfter: _parseValidityTime(validityNode.children[1]),
346
- };
347
-
348
- if (!spkiNode.children || spkiNode.children.length !== 2) {
349
- throw new CertificateError("x509/bad-spki", "SubjectPublicKeyInfo must be a SEQUENCE of {algorithm, subjectPublicKey}");
350
- }
351
- var spkiBits = asn1.read.bitString(spkiNode.children[1]);
352
- var subjectPublicKeyInfo = {
353
- algorithm: _algId(spkiNode.children[0]),
354
- publicKey: { unusedBits: spkiBits.unusedBits, bytes: spkiBits.bytes },
355
- bytes: spkiNode.bytes,
356
- };
357
-
358
- // Remaining tbs children: optional issuerUniqueID [1], subjectUniqueID
359
- // [2], and extensions [3] EXPLICIT. Only extensions are surfaced. RFC 5280
360
- // §4.1 fixes these as the trailing fields, each at most once and in strictly
361
- // increasing tag order; a repeated or out-of-order tag (or an unknown /
362
- // non-context field) is malformed. Without the monotonic guard a second [3]
363
- // silently overwrites the first, hiding its extensions and splitting
364
- // duplicate extension OIDs across two wrappers past the per-sequence check.
365
- var extensions = [];
366
- var hasExtensions = false;
367
- var lastTrailingTag = 0;
368
- for (; idx < tbs.children.length; idx++) {
369
- var t = tbs.children[idx];
370
- if (t.tagClass !== "context" || t.tagNumber < 1 || t.tagNumber > 3) {
371
- throw new CertificateError("x509/bad-tbs", "unexpected field after subjectPublicKeyInfo; tbsCertificate allows only issuerUniqueID [1], subjectUniqueID [2], extensions [3]");
372
- }
373
- if (t.tagNumber <= lastTrailingTag) {
374
- throw new CertificateError("x509/bad-tbs", "tbsCertificate trailing field [" + t.tagNumber + "] is repeated or out of order");
375
- }
376
- lastTrailingTag = t.tagNumber;
377
- if (t.tagNumber === 3) {
378
- hasExtensions = true;
379
- if (!t.children || !t.children.length) throw new CertificateError("x509/bad-extensions", "extensions [3] must wrap a SEQUENCE");
380
- extensions = _parseExtensions(t.children[0]);
381
- }
382
- }
383
-
384
- // RFC 5280 §4.1.2.9 — the extensions field appears only in a v3 cert.
385
- if (hasExtensions && version !== 3) {
386
- throw new CertificateError("x509/bad-version", "extensions are only permitted in a v3 certificate");
387
- }
388
-
389
- var sigBits = asn1.read.bitString(sigValueNode);
390
-
391
- return {
392
- version: version,
393
- serialNumber: serialNumber,
394
- serialNumberHex: serialNode.content.toString("hex"),
395
- signatureAlgorithm: _algId(outerSigAlg),
396
- tbsSignatureAlgorithm: _algId(innerSigAlg),
397
- issuer: issuer,
398
- subject: subject,
399
- validity: validity,
400
- subjectPublicKeyInfo: subjectPublicKeyInfo,
401
- extensions: extensions,
402
- tbsBytes: tbs.bytes,
403
- signatureValue: { unusedBits: sigBits.unusedBits, bytes: sigBits.bytes },
404
- };
386
+ return schema.walk(CERTIFICATE, root, NS).result;
405
387
  }
406
388
 
407
389
  module.exports = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:d25ccb6d-af2d-4b1f-b300-f756dc788762",
5
+ "serialNumber": "urn:uuid:8ff5f15e-e999-4db6-bf14-09273902a9c2",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-04T19:57:57.512Z",
8
+ "timestamp": "2026-07-04T22:46:29.855Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/pki@0.1.4",
22
+ "bom-ref": "@blamejs/pki@0.1.6",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.1.4",
25
+ "version": "0.1.6",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
29
- "purl": "pkg:npm/%40blamejs/pki@0.1.4",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.1.6",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/pki@0.1.4",
57
+ "ref": "@blamejs/pki@0.1.6",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]