@blamejs/pki 0.1.5 → 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 +12 -0
- package/index.js +4 -1
- package/lib/asn1-schema.js +340 -0
- package/lib/x509.js +215 -233
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@ 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
|
+
|
|
7
19
|
## v0.1.5 — 2026-07-04
|
|
8
20
|
|
|
9
21
|
Container healthcheck honors WIKI_PORT; release-tooling supply-chain hardening.
|
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
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
145
|
-
}
|
|
153
|
+
});
|
|
146
154
|
}
|
|
147
155
|
|
|
148
156
|
function _escapeDnValue(v) {
|
|
149
157
|
return v.replace(/([,+"\\<>;])/g, "\\$1");
|
|
150
158
|
}
|
|
151
159
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
215
|
-
var
|
|
216
|
-
var
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
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
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:
|
|
5
|
+
"serialNumber": "urn:uuid:8ff5f15e-e999-4db6-bf14-09273902a9c2",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
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.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.1.6",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.1.
|
|
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.
|
|
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.
|
|
57
|
+
"ref": "@blamejs/pki@0.1.6",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|