@blamejs/pki 0.1.1
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 +32 -0
- package/LICENSE +201 -0
- package/MIGRATING.md +9 -0
- package/NOTICE +21 -0
- package/README.md +252 -0
- package/bin/pki.js +76 -0
- package/index.js +50 -0
- package/lib/asn1-der.js +576 -0
- package/lib/constants.js +115 -0
- package/lib/framework-error.js +130 -0
- package/lib/oid.js +219 -0
- package/lib/vendor/MANIFEST.json +4 -0
- package/lib/vendor/README.md +41 -0
- package/lib/webcrypto.js +542 -0
- package/lib/x509.js +320 -0
- package/package.json +74 -0
- package/sbom.cdx.json +61 -0
package/lib/asn1-der.js
ADDED
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
/**
|
|
5
|
+
* @module pki.asn1
|
|
6
|
+
* @nav Core
|
|
7
|
+
* @title ASN.1 / DER
|
|
8
|
+
* @order 30
|
|
9
|
+
* @featured true
|
|
10
|
+
* @slug asn1
|
|
11
|
+
*
|
|
12
|
+
* @intro
|
|
13
|
+
* A strict DER (Distinguished Encoding Rules) codec — the byte layer
|
|
14
|
+
* every X.509 / PKCS / CMS structure is built on. The decoder is
|
|
15
|
+
* fail-closed: it rejects the BER shapes DER forbids (indefinite
|
|
16
|
+
* length, non-minimal length or integer encodings, trailing garbage,
|
|
17
|
+
* constructed strings) and refuses input past a size or nesting cap
|
|
18
|
+
* before it walks a single byte, so a hostile length prefix can't turn
|
|
19
|
+
* into a decoder denial-of-service.
|
|
20
|
+
*
|
|
21
|
+
* `decode(bytes)` returns a navigable node tree; the `read.*` helpers
|
|
22
|
+
* turn a node into a JS value (BigInt, dotted OID, Date, string); the
|
|
23
|
+
* `build.*` helpers construct canonical DER from JS values. Because DER
|
|
24
|
+
* is canonical, each value the `build.*` helpers emit has exactly one
|
|
25
|
+
* valid encoding — byte-identical to any other conformant DER encoder's
|
|
26
|
+
* output — and decoding it reproduces the value it was built from.
|
|
27
|
+
*
|
|
28
|
+
* @card
|
|
29
|
+
* Strict, fail-closed DER decode / encode with a navigable node tree
|
|
30
|
+
* and typed readers + builders.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
var constants = require("./constants");
|
|
34
|
+
var frameworkError = require("./framework-error");
|
|
35
|
+
|
|
36
|
+
var Asn1Error = frameworkError.Asn1Error;
|
|
37
|
+
var OidError = frameworkError.OidError;
|
|
38
|
+
|
|
39
|
+
// ---- Tag constants (universal class) --------------------------------
|
|
40
|
+
|
|
41
|
+
var TAGS = {
|
|
42
|
+
BOOLEAN: 0x01,
|
|
43
|
+
INTEGER: 0x02,
|
|
44
|
+
BIT_STRING: 0x03,
|
|
45
|
+
OCTET_STRING: 0x04,
|
|
46
|
+
NULL: 0x05,
|
|
47
|
+
OBJECT_IDENTIFIER: 0x06,
|
|
48
|
+
ENUMERATED: 0x0a,
|
|
49
|
+
UTF8_STRING: 0x0c,
|
|
50
|
+
SEQUENCE: 0x10,
|
|
51
|
+
SET: 0x11,
|
|
52
|
+
PRINTABLE_STRING: 0x13,
|
|
53
|
+
TELETEX_STRING: 0x14,
|
|
54
|
+
IA5_STRING: 0x16,
|
|
55
|
+
UTC_TIME: 0x17,
|
|
56
|
+
GENERALIZED_TIME: 0x18,
|
|
57
|
+
VISIBLE_STRING: 0x1a,
|
|
58
|
+
UNIVERSAL_STRING: 0x1c,
|
|
59
|
+
BMP_STRING: 0x1e,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
var CLASS_UNIVERSAL = 0x00;
|
|
63
|
+
var CLASS_APPLICATION = 0x40;
|
|
64
|
+
var CLASS_CONTEXT = 0x80;
|
|
65
|
+
var CLASS_PRIVATE = 0xc0;
|
|
66
|
+
var CONSTRUCTED_BIT = 0x20;
|
|
67
|
+
|
|
68
|
+
function _className(bits) {
|
|
69
|
+
switch (bits) {
|
|
70
|
+
case CLASS_UNIVERSAL: return "universal";
|
|
71
|
+
case CLASS_APPLICATION: return "application";
|
|
72
|
+
case CLASS_CONTEXT: return "context";
|
|
73
|
+
case CLASS_PRIVATE: return "private";
|
|
74
|
+
default: return "universal";
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function _asBuffer(input, who) {
|
|
79
|
+
if (Buffer.isBuffer(input)) return input;
|
|
80
|
+
if (input instanceof Uint8Array) return Buffer.from(input);
|
|
81
|
+
throw new Asn1Error("asn1/not-buffer", who + ": expected a Buffer / Uint8Array");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---- Decoder --------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @primitive pki.asn1.decode
|
|
88
|
+
* @signature pki.asn1.decode(bytes, opts?) -> node
|
|
89
|
+
* @since 0.1.0
|
|
90
|
+
* @status stable
|
|
91
|
+
* @related pki.asn1.encode
|
|
92
|
+
*
|
|
93
|
+
* Parse DER into a node tree. Each node is
|
|
94
|
+
* `{ tagClass, constructed, tagNumber, header, length, content, children,
|
|
95
|
+
* bytes }` — `content` is the primitive value slice, `children` the
|
|
96
|
+
* decoded sub-nodes of a constructed node, and `bytes` the full TLV slice
|
|
97
|
+
* (all zero-copy views over the input).
|
|
98
|
+
*
|
|
99
|
+
* Throws `Asn1Error` on any non-DER shape: indefinite length, a
|
|
100
|
+
* non-minimal length or a length that overruns the buffer, trailing bytes
|
|
101
|
+
* after the top-level value (unless `allowTrailing`), or exceeding the
|
|
102
|
+
* size / depth caps.
|
|
103
|
+
*
|
|
104
|
+
* @opts
|
|
105
|
+
* maxBytes: number, // default: C.LIMITS.DER_MAX_BYTES (16 MiB)
|
|
106
|
+
* maxDepth: number, // default: C.LIMITS.DER_MAX_DEPTH (64)
|
|
107
|
+
* allowTrailing: boolean, // default: false — allow bytes after the top TLV
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* var node = pki.asn1.decode(der);
|
|
111
|
+
* node.tagNumber === pki.asn1.TAGS.SEQUENCE;
|
|
112
|
+
*/
|
|
113
|
+
function decode(input, opts) {
|
|
114
|
+
opts = opts || {};
|
|
115
|
+
var buf = _asBuffer(input, "decode");
|
|
116
|
+
var maxBytes = typeof opts.maxBytes === "number" ? opts.maxBytes : constants.LIMITS.DER_MAX_BYTES;
|
|
117
|
+
var maxDepth = typeof opts.maxDepth === "number" ? opts.maxDepth : constants.LIMITS.DER_MAX_DEPTH;
|
|
118
|
+
if (buf.length > maxBytes) {
|
|
119
|
+
throw new Asn1Error("asn1/too-large", "input " + buf.length + " bytes exceeds cap " + maxBytes);
|
|
120
|
+
}
|
|
121
|
+
var r = _decodeTLV(buf, 0, buf.length, 0, maxDepth);
|
|
122
|
+
if (!opts.allowTrailing && r.end !== buf.length) {
|
|
123
|
+
throw new Asn1Error("asn1/trailing-bytes", (buf.length - r.end) + " trailing byte(s) after the top-level value");
|
|
124
|
+
}
|
|
125
|
+
return r.node;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function _decodeTLV(buf, start, limit, depth, maxDepth) {
|
|
129
|
+
if (depth > maxDepth) {
|
|
130
|
+
throw new Asn1Error("asn1/too-deep", "nesting exceeds depth cap " + maxDepth);
|
|
131
|
+
}
|
|
132
|
+
var p = start;
|
|
133
|
+
if (p >= limit) throw new Asn1Error("asn1/truncated", "expected an identifier octet");
|
|
134
|
+
var first = buf[p]; p += 1;
|
|
135
|
+
var tagClassBits = first & 0xc0;
|
|
136
|
+
var constructed = (first & CONSTRUCTED_BIT) !== 0;
|
|
137
|
+
var tagNumber = first & 0x1f;
|
|
138
|
+
|
|
139
|
+
if (tagNumber === 0x1f) {
|
|
140
|
+
// High-tag-number form: base-128, minimal (no leading 0x80).
|
|
141
|
+
tagNumber = 0;
|
|
142
|
+
var seen = 0;
|
|
143
|
+
for (;;) {
|
|
144
|
+
if (p >= limit) throw new Asn1Error("asn1/truncated", "truncated high-tag-number");
|
|
145
|
+
var tb = buf[p]; p += 1;
|
|
146
|
+
if (seen === 0 && tb === 0x80) {
|
|
147
|
+
throw new Asn1Error("asn1/non-minimal-tag", "leading 0x80 in high-tag-number form");
|
|
148
|
+
}
|
|
149
|
+
tagNumber = (tagNumber * 128) + (tb & 0x7f);
|
|
150
|
+
seen += 1;
|
|
151
|
+
if (seen > 4) throw new Asn1Error("asn1/tag-too-large", "high-tag-number too large");
|
|
152
|
+
if ((tb & 0x80) === 0) break;
|
|
153
|
+
}
|
|
154
|
+
if (tagNumber < 0x1f) {
|
|
155
|
+
throw new Asn1Error("asn1/non-minimal-tag", "high-tag-number form used for a low tag");
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (p >= limit) throw new Asn1Error("asn1/truncated", "expected a length octet");
|
|
160
|
+
var lenByte = buf[p]; p += 1;
|
|
161
|
+
var length;
|
|
162
|
+
if (lenByte < 0x80) {
|
|
163
|
+
length = lenByte;
|
|
164
|
+
} else if (lenByte === 0x80) {
|
|
165
|
+
throw new Asn1Error("asn1/indefinite-length", "indefinite length is not valid DER");
|
|
166
|
+
} else {
|
|
167
|
+
var numLenBytes = lenByte & 0x7f;
|
|
168
|
+
if (numLenBytes > 6) throw new Asn1Error("asn1/length-too-large", "length uses more than 6 octets");
|
|
169
|
+
if (p + numLenBytes > limit) throw new Asn1Error("asn1/truncated", "truncated long-form length");
|
|
170
|
+
if (buf[p] === 0x00) throw new Asn1Error("asn1/non-minimal-length", "leading zero in long-form length");
|
|
171
|
+
length = 0;
|
|
172
|
+
for (var i = 0; i < numLenBytes; i++) length = (length * 256) + buf[p + i];
|
|
173
|
+
p += numLenBytes;
|
|
174
|
+
if (length < 0x80) throw new Asn1Error("asn1/non-minimal-length", "long form used for a length < 128");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
var contentStart = p;
|
|
178
|
+
var contentEnd = contentStart + length;
|
|
179
|
+
if (contentEnd > limit) throw new Asn1Error("asn1/truncated", "content length overruns the buffer");
|
|
180
|
+
|
|
181
|
+
var node = {
|
|
182
|
+
tagClass: _className(tagClassBits),
|
|
183
|
+
constructed: constructed,
|
|
184
|
+
tagNumber: tagNumber,
|
|
185
|
+
length: length,
|
|
186
|
+
header: { start: start, end: contentStart },
|
|
187
|
+
contentStart: contentStart,
|
|
188
|
+
contentEnd: contentEnd,
|
|
189
|
+
content: null,
|
|
190
|
+
children: null,
|
|
191
|
+
bytes: buf.subarray(start, contentEnd),
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
if (constructed) {
|
|
195
|
+
node.children = [];
|
|
196
|
+
var cp = contentStart;
|
|
197
|
+
while (cp < contentEnd) {
|
|
198
|
+
var child = _decodeTLV(buf, cp, contentEnd, depth + 1, maxDepth);
|
|
199
|
+
node.children.push(child.node);
|
|
200
|
+
cp = child.end;
|
|
201
|
+
}
|
|
202
|
+
} else {
|
|
203
|
+
node.content = buf.subarray(contentStart, contentEnd);
|
|
204
|
+
}
|
|
205
|
+
return { node: node, end: contentEnd };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ---- Node assertions ------------------------------------------------
|
|
209
|
+
|
|
210
|
+
function _expectUniversal(node, tag, who) {
|
|
211
|
+
if (node.tagClass !== "universal" || node.tagNumber !== tag) {
|
|
212
|
+
throw new Asn1Error("asn1/unexpected-tag", who + ": expected universal tag " + tag +
|
|
213
|
+
", got " + node.tagClass + "/" + node.tagNumber);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function _expectPrimitive(node, who) {
|
|
218
|
+
if (node.constructed) throw new Asn1Error("asn1/expected-primitive", who + ": expected a primitive encoding");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ---- Typed readers --------------------------------------------------
|
|
222
|
+
|
|
223
|
+
function readBoolean(node) {
|
|
224
|
+
_expectUniversal(node, TAGS.BOOLEAN, "readBoolean");
|
|
225
|
+
_expectPrimitive(node, "readBoolean");
|
|
226
|
+
if (node.content.length !== 1) throw new Asn1Error("asn1/bad-boolean", "BOOLEAN content must be 1 octet");
|
|
227
|
+
var v = node.content[0];
|
|
228
|
+
if (v === 0x00) return false;
|
|
229
|
+
if (v === 0xff) return true;
|
|
230
|
+
throw new Asn1Error("asn1/bad-boolean", "DER BOOLEAN must be 0x00 or 0xFF, got 0x" + v.toString(16));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function readInteger(node) {
|
|
234
|
+
if (node.tagClass === "universal" && node.tagNumber === TAGS.ENUMERATED) {
|
|
235
|
+
// ENUMERATED shares INTEGER's content encoding.
|
|
236
|
+
} else {
|
|
237
|
+
_expectUniversal(node, TAGS.INTEGER, "readInteger");
|
|
238
|
+
}
|
|
239
|
+
_expectPrimitive(node, "readInteger");
|
|
240
|
+
var c = node.content;
|
|
241
|
+
if (c.length === 0) throw new Asn1Error("asn1/bad-integer", "INTEGER must have at least 1 content octet");
|
|
242
|
+
if (c.length > 1) {
|
|
243
|
+
if (c[0] === 0x00 && (c[1] & 0x80) === 0) throw new Asn1Error("asn1/non-minimal-integer", "non-minimal positive INTEGER");
|
|
244
|
+
if (c[0] === 0xff && (c[1] & 0x80) !== 0) throw new Asn1Error("asn1/non-minimal-integer", "non-minimal negative INTEGER");
|
|
245
|
+
}
|
|
246
|
+
var neg = (c[0] & 0x80) !== 0;
|
|
247
|
+
var v = 0n;
|
|
248
|
+
for (var i = 0; i < c.length; i++) v = (v << 8n) | BigInt(c[i]);
|
|
249
|
+
if (neg) v -= (1n << BigInt(8 * c.length));
|
|
250
|
+
return v;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function readBitString(node) {
|
|
254
|
+
_expectUniversal(node, TAGS.BIT_STRING, "readBitString");
|
|
255
|
+
_expectPrimitive(node, "readBitString");
|
|
256
|
+
var c = node.content;
|
|
257
|
+
if (c.length === 0) throw new Asn1Error("asn1/bad-bit-string", "BIT STRING must have >= 1 content octet");
|
|
258
|
+
var unusedBits = c[0];
|
|
259
|
+
if (unusedBits > 7) throw new Asn1Error("asn1/bad-bit-string", "unused-bit count " + unusedBits + " > 7");
|
|
260
|
+
if (unusedBits > 0 && c.length === 1) throw new Asn1Error("asn1/bad-bit-string", "unused bits declared over an empty body");
|
|
261
|
+
return { unusedBits: unusedBits, bytes: c.subarray(1) };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function readOctetString(node) {
|
|
265
|
+
_expectUniversal(node, TAGS.OCTET_STRING, "readOctetString");
|
|
266
|
+
_expectPrimitive(node, "readOctetString");
|
|
267
|
+
return node.content;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function readNull(node) {
|
|
271
|
+
_expectUniversal(node, TAGS.NULL, "readNull");
|
|
272
|
+
_expectPrimitive(node, "readNull");
|
|
273
|
+
if (node.content.length !== 0) throw new Asn1Error("asn1/bad-null", "NULL must have empty content");
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* @primitive pki.asn1.readOid
|
|
279
|
+
* @signature pki.asn1.read.oid(node) -> "1.2.840.113549.1.1.11"
|
|
280
|
+
* @since 0.1.0
|
|
281
|
+
* @status stable
|
|
282
|
+
* @related pki.oid.name
|
|
283
|
+
*
|
|
284
|
+
* Decode an OBJECT IDENTIFIER node to its dotted-decimal string, enforcing
|
|
285
|
+
* the minimal base-128 sub-identifier encoding DER requires.
|
|
286
|
+
*
|
|
287
|
+
* @example
|
|
288
|
+
* pki.asn1.read.oid(node); // -> "2.5.4.3"
|
|
289
|
+
*/
|
|
290
|
+
function readOid(node) {
|
|
291
|
+
_expectUniversal(node, TAGS.OBJECT_IDENTIFIER, "readOid");
|
|
292
|
+
_expectPrimitive(node, "readOid");
|
|
293
|
+
return decodeOidContent(node.content);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function decodeOidContent(buf) {
|
|
297
|
+
if (buf.length === 0) throw new OidError("oid/empty", "OBJECT IDENTIFIER content is empty");
|
|
298
|
+
var arcs = [];
|
|
299
|
+
var value = 0n;
|
|
300
|
+
var started = false;
|
|
301
|
+
for (var i = 0; i < buf.length; i++) {
|
|
302
|
+
var b = buf[i];
|
|
303
|
+
if (!started && b === 0x80) throw new OidError("oid/non-minimal", "non-minimal sub-identifier (leading 0x80)");
|
|
304
|
+
value = (value << 7n) | BigInt(b & 0x7f);
|
|
305
|
+
started = true;
|
|
306
|
+
if ((b & 0x80) === 0) { arcs.push(value); value = 0n; started = false; }
|
|
307
|
+
}
|
|
308
|
+
if (started) throw new OidError("oid/truncated", "OBJECT IDENTIFIER ends mid sub-identifier");
|
|
309
|
+
var first = arcs[0];
|
|
310
|
+
var a1, a2;
|
|
311
|
+
if (first < 40n) { a1 = 0n; a2 = first; }
|
|
312
|
+
else if (first < 80n) { a1 = 1n; a2 = first - 40n; }
|
|
313
|
+
else { a1 = 2n; a2 = first - 80n; }
|
|
314
|
+
var out = [a1.toString(), a2.toString()];
|
|
315
|
+
for (var j = 1; j < arcs.length; j++) out.push(arcs[j].toString());
|
|
316
|
+
return out.join(".");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function _decodeText(buf, encoding) {
|
|
320
|
+
return buf.toString(encoding);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function readString(node) {
|
|
324
|
+
if (node.tagClass !== "universal") throw new Asn1Error("asn1/expected-string", "readString: not a universal string type");
|
|
325
|
+
_expectPrimitive(node, "readString");
|
|
326
|
+
switch (node.tagNumber) {
|
|
327
|
+
case TAGS.UTF8_STRING: return _decodeText(node.content, "utf8");
|
|
328
|
+
case TAGS.PRINTABLE_STRING: return _decodeText(node.content, "latin1");
|
|
329
|
+
case TAGS.IA5_STRING: return _decodeText(node.content, "latin1");
|
|
330
|
+
case TAGS.TELETEX_STRING: return _decodeText(node.content, "latin1");
|
|
331
|
+
case TAGS.VISIBLE_STRING: return _decodeText(node.content, "latin1");
|
|
332
|
+
case TAGS.BMP_STRING: return _decodeUtf16be(node.content);
|
|
333
|
+
case TAGS.UNIVERSAL_STRING: return _decodeUtf32be(node.content);
|
|
334
|
+
default:
|
|
335
|
+
throw new Asn1Error("asn1/expected-string", "readString: tag " + node.tagNumber + " is not a known string type");
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function _decodeUtf16be(buf) {
|
|
340
|
+
if (buf.length % 2 !== 0) throw new Asn1Error("asn1/bad-bmp-string", "BMPString length must be even");
|
|
341
|
+
var swapped = Buffer.from(buf);
|
|
342
|
+
swapped.swap16();
|
|
343
|
+
return swapped.toString("utf16le");
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function _decodeUtf32be(buf) {
|
|
347
|
+
if (buf.length % 4 !== 0) throw new Asn1Error("asn1/bad-universal-string", "UniversalString length must be a multiple of 4");
|
|
348
|
+
var out = "";
|
|
349
|
+
for (var i = 0; i < buf.length; i += 4) {
|
|
350
|
+
var cp = (buf[i] * 0x1000000) + (buf[i + 1] << 16) + (buf[i + 2] << 8) + buf[i + 3];
|
|
351
|
+
out += String.fromCodePoint(cp);
|
|
352
|
+
}
|
|
353
|
+
return out;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
var UTC_RE = /^(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})Z$/;
|
|
357
|
+
var GEN_RE = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})Z$/;
|
|
358
|
+
|
|
359
|
+
function readTime(node) {
|
|
360
|
+
if (node.tagClass !== "universal") throw new Asn1Error("asn1/expected-time", "readTime: not a universal time type");
|
|
361
|
+
_expectPrimitive(node, "readTime");
|
|
362
|
+
var s = node.content.toString("latin1");
|
|
363
|
+
var m, year;
|
|
364
|
+
if (node.tagNumber === TAGS.UTC_TIME) {
|
|
365
|
+
m = UTC_RE.exec(s);
|
|
366
|
+
if (!m) throw new Asn1Error("asn1/bad-utctime", "UTCTime must be YYMMDDHHMMSSZ, got " + JSON.stringify(s));
|
|
367
|
+
year = parseInt(m[1], 10);
|
|
368
|
+
// RFC 5280 §4.1.2.5.1 — YY < 50 => 20YY, else 19YY.
|
|
369
|
+
year += (year < 50) ? 2000 : 1900;
|
|
370
|
+
} else if (node.tagNumber === TAGS.GENERALIZED_TIME) {
|
|
371
|
+
m = GEN_RE.exec(s);
|
|
372
|
+
if (!m) throw new Asn1Error("asn1/bad-generalizedtime", "GeneralizedTime must be YYYYMMDDHHMMSSZ, got " + JSON.stringify(s));
|
|
373
|
+
year = parseInt(m[1], 10);
|
|
374
|
+
} else {
|
|
375
|
+
throw new Asn1Error("asn1/expected-time", "readTime: tag " + node.tagNumber + " is not a time type");
|
|
376
|
+
}
|
|
377
|
+
var month = parseInt(m[node.tagNumber === TAGS.UTC_TIME ? 2 : 2], 10);
|
|
378
|
+
var day = parseInt(m[3], 10);
|
|
379
|
+
var hour = parseInt(m[4], 10);
|
|
380
|
+
var min = parseInt(m[5], 10);
|
|
381
|
+
var sec = parseInt(m[6], 10);
|
|
382
|
+
var t = Date.UTC(year, month - 1, day, hour, min, sec);
|
|
383
|
+
if (isNaN(t)) throw new Asn1Error("asn1/bad-time", "unparseable time " + JSON.stringify(s));
|
|
384
|
+
return new Date(t);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// ---- Encoder / builders ---------------------------------------------
|
|
388
|
+
|
|
389
|
+
function encodeLength(n) {
|
|
390
|
+
if (typeof n !== "number" || !isFinite(n) || n < 0 || Math.floor(n) !== n) {
|
|
391
|
+
throw new Asn1Error("asn1/bad-length", "length must be a non-negative integer");
|
|
392
|
+
}
|
|
393
|
+
if (n < 0x80) return Buffer.from([n]);
|
|
394
|
+
var bytes = [];
|
|
395
|
+
var v = n;
|
|
396
|
+
while (v > 0) { bytes.unshift(v & 0xff); v = Math.floor(v / 256); }
|
|
397
|
+
if (bytes.length > 126) throw new Asn1Error("asn1/length-too-large", "length needs more than 126 octets");
|
|
398
|
+
return Buffer.concat([Buffer.from([0x80 | bytes.length]), Buffer.from(bytes)]);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function encodeIdentifier(classBits, constructed, tagNumber) {
|
|
402
|
+
var lead = classBits | (constructed ? CONSTRUCTED_BIT : 0);
|
|
403
|
+
if (tagNumber < 0x1f) return Buffer.from([lead | tagNumber]);
|
|
404
|
+
var body = [];
|
|
405
|
+
var v = tagNumber;
|
|
406
|
+
do { body.unshift(v & 0x7f); v = Math.floor(v / 128); } while (v > 0);
|
|
407
|
+
for (var i = 0; i < body.length - 1; i++) body[i] |= 0x80;
|
|
408
|
+
return Buffer.concat([Buffer.from([lead | 0x1f]), Buffer.from(body)]);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* @primitive pki.asn1.encode
|
|
413
|
+
* @signature pki.asn1.encode(classBits, constructed, tagNumber, content) -> Buffer
|
|
414
|
+
* @since 0.1.0
|
|
415
|
+
* @status stable
|
|
416
|
+
* @related pki.asn1.decode
|
|
417
|
+
*
|
|
418
|
+
* Low-level TLV encoder — prepend the identifier + DER length to a content
|
|
419
|
+
* buffer. Most callers use the higher-level `build.*` helpers; this is the
|
|
420
|
+
* escape hatch for context-tagged and implicitly-tagged constructions.
|
|
421
|
+
*
|
|
422
|
+
* @example
|
|
423
|
+
* pki.asn1.encode(0x00, false, pki.asn1.TAGS.NULL, Buffer.alloc(0));
|
|
424
|
+
*/
|
|
425
|
+
function encodeTLV(classBits, constructed, tagNumber, content) {
|
|
426
|
+
var body = Buffer.isBuffer(content) ? content : Buffer.from(content || []);
|
|
427
|
+
var id = encodeIdentifier(classBits, constructed, tagNumber);
|
|
428
|
+
return Buffer.concat([id, encodeLength(body.length), body]);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function _universal(tagNumber, constructed, content) {
|
|
432
|
+
return encodeTLV(CLASS_UNIVERSAL, constructed, tagNumber, content);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function intToDer(v) {
|
|
436
|
+
if (typeof v === "number") {
|
|
437
|
+
if (!Number.isSafeInteger(v)) throw new Asn1Error("asn1/bad-integer", "unsafe integer; pass a BigInt");
|
|
438
|
+
v = BigInt(v);
|
|
439
|
+
}
|
|
440
|
+
if (typeof v !== "bigint") throw new Asn1Error("asn1/bad-integer", "integer must be number or BigInt");
|
|
441
|
+
if (v === 0n) return Buffer.from([0]);
|
|
442
|
+
var bytes = [];
|
|
443
|
+
if (v > 0n) {
|
|
444
|
+
var t = v;
|
|
445
|
+
while (t > 0n) { bytes.unshift(Number(t & 0xffn)); t >>= 8n; }
|
|
446
|
+
if (bytes[0] & 0x80) bytes.unshift(0x00);
|
|
447
|
+
} else {
|
|
448
|
+
var len = 1;
|
|
449
|
+
while (v < -(1n << BigInt(8 * len - 1))) len += 1;
|
|
450
|
+
var tc = (1n << BigInt(8 * len)) + v;
|
|
451
|
+
for (var i = len - 1; i >= 0; i--) { bytes[i] = Number(tc & 0xffn); tc >>= 8n; }
|
|
452
|
+
}
|
|
453
|
+
return Buffer.from(bytes);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function encodeOidContent(dotted) {
|
|
457
|
+
if (typeof dotted !== "string" || dotted.length === 0) throw new OidError("oid/bad-input", "OID must be a dotted-decimal string");
|
|
458
|
+
var parts = dotted.split(".");
|
|
459
|
+
if (parts.length < 2) throw new OidError("oid/too-short", "OID needs at least two arcs");
|
|
460
|
+
var arcs = [];
|
|
461
|
+
for (var i = 0; i < parts.length; i++) {
|
|
462
|
+
if (!/^\d+$/.test(parts[i])) throw new OidError("oid/bad-arc", "arc " + JSON.stringify(parts[i]) + " is not a non-negative integer");
|
|
463
|
+
arcs.push(BigInt(parts[i]));
|
|
464
|
+
}
|
|
465
|
+
var a1 = arcs[0], a2 = arcs[1];
|
|
466
|
+
if (a1 > 2n) throw new OidError("oid/bad-arc", "first arc must be 0, 1, or 2");
|
|
467
|
+
if (a1 < 2n && a2 >= 40n) throw new OidError("oid/bad-arc", "second arc must be < 40 when the first arc is 0 or 1");
|
|
468
|
+
var subids = [a1 * 40n + a2].concat(arcs.slice(2));
|
|
469
|
+
var out = [];
|
|
470
|
+
for (var s = 0; s < subids.length; s++) {
|
|
471
|
+
var body = [];
|
|
472
|
+
var v = subids[s];
|
|
473
|
+
do { body.unshift(Number(v & 0x7fn)); v >>= 7n; } while (v > 0n);
|
|
474
|
+
for (var k = 0; k < body.length - 1; k++) body[k] |= 0x80;
|
|
475
|
+
for (var j = 0; j < body.length; j++) out.push(body[j]);
|
|
476
|
+
}
|
|
477
|
+
return Buffer.from(out);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
var PRINTABLE_RE = /^[A-Za-z0-9 '()+,\-./:=?]*$/;
|
|
481
|
+
|
|
482
|
+
function _fmtTwo(n) { return (n < 10 ? "0" : "") + n; }
|
|
483
|
+
|
|
484
|
+
function _generalizedTimeString(date) {
|
|
485
|
+
return "" + date.getUTCFullYear() +
|
|
486
|
+
_fmtTwo(date.getUTCMonth() + 1) + _fmtTwo(date.getUTCDate()) +
|
|
487
|
+
_fmtTwo(date.getUTCHours()) + _fmtTwo(date.getUTCMinutes()) + _fmtTwo(date.getUTCSeconds()) + "Z";
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function _utcTimeString(date) {
|
|
491
|
+
var yy = date.getUTCFullYear() % 100;
|
|
492
|
+
return _fmtTwo(yy) +
|
|
493
|
+
_fmtTwo(date.getUTCMonth() + 1) + _fmtTwo(date.getUTCDate()) +
|
|
494
|
+
_fmtTwo(date.getUTCHours()) + _fmtTwo(date.getUTCMinutes()) + _fmtTwo(date.getUTCSeconds()) + "Z";
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* @primitive pki.asn1.build
|
|
499
|
+
* @signature pki.asn1.build.sequence([ ...tlvBuffers ]) -> Buffer
|
|
500
|
+
* @since 0.1.0
|
|
501
|
+
* @status stable
|
|
502
|
+
* @related pki.asn1.encode
|
|
503
|
+
*
|
|
504
|
+
* Canonical-DER value builders. Each returns the full TLV Buffer for one
|
|
505
|
+
* value; `sequence` / `set` / `setOf` take arrays of already-built child
|
|
506
|
+
* TLVs. `setOf` sorts its members by their DER encoding as X.690 requires.
|
|
507
|
+
*
|
|
508
|
+
* @example
|
|
509
|
+
* var rdn = pki.asn1.build.sequence([
|
|
510
|
+
* pki.asn1.build.oid("2.5.4.3"),
|
|
511
|
+
* pki.asn1.build.utf8("example.com"),
|
|
512
|
+
* ]);
|
|
513
|
+
*/
|
|
514
|
+
var build = {
|
|
515
|
+
boolean: function (v) { return _universal(TAGS.BOOLEAN, false, Buffer.from([v ? 0xff : 0x00])); },
|
|
516
|
+
integer: function (v) { return _universal(TAGS.INTEGER, false, Buffer.isBuffer(v) ? v : intToDer(v)); },
|
|
517
|
+
enumerated: function (v) { return _universal(TAGS.ENUMERATED, false, Buffer.isBuffer(v) ? v : intToDer(v)); },
|
|
518
|
+
nullValue: function () { return _universal(TAGS.NULL, false, Buffer.alloc(0)); },
|
|
519
|
+
oid: function (dotted) { return _universal(TAGS.OBJECT_IDENTIFIER, false, encodeOidContent(dotted)); },
|
|
520
|
+
octetString: function (buf) { return _universal(TAGS.OCTET_STRING, false, _asBuffer(buf, "build.octetString")); },
|
|
521
|
+
bitString: function (buf, unusedBits) {
|
|
522
|
+
var u = unusedBits || 0;
|
|
523
|
+
if (u < 0 || u > 7) throw new Asn1Error("asn1/bad-bit-string", "unusedBits must be 0..7");
|
|
524
|
+
return _universal(TAGS.BIT_STRING, false, Buffer.concat([Buffer.from([u]), _asBuffer(buf, "build.bitString")]));
|
|
525
|
+
},
|
|
526
|
+
utf8: function (s) { return _universal(TAGS.UTF8_STRING, false, Buffer.from(String(s), "utf8")); },
|
|
527
|
+
ia5: function (s) { return _universal(TAGS.IA5_STRING, false, Buffer.from(String(s), "latin1")); },
|
|
528
|
+
printable: function (s) {
|
|
529
|
+
s = String(s);
|
|
530
|
+
if (!PRINTABLE_RE.test(s)) throw new Asn1Error("asn1/bad-printable-string", "value has characters outside the PrintableString set");
|
|
531
|
+
return _universal(TAGS.PRINTABLE_STRING, false, Buffer.from(s, "latin1"));
|
|
532
|
+
},
|
|
533
|
+
utcTime: function (date) { return _universal(TAGS.UTC_TIME, false, Buffer.from(_utcTimeString(date), "latin1")); },
|
|
534
|
+
generalizedTime: function (date) { return _universal(TAGS.GENERALIZED_TIME, false, Buffer.from(_generalizedTimeString(date), "latin1")); },
|
|
535
|
+
sequence: function (children) { return _universal(TAGS.SEQUENCE, true, Buffer.concat(_asBufferArray(children, "build.sequence"))); },
|
|
536
|
+
set: function (children) { return _universal(TAGS.SET, true, Buffer.concat(_asBufferArray(children, "build.set"))); },
|
|
537
|
+
setOf: function (children) {
|
|
538
|
+
var arr = _asBufferArray(children, "build.setOf").slice();
|
|
539
|
+
arr.sort(Buffer.compare);
|
|
540
|
+
return _universal(TAGS.SET, true, Buffer.concat(arr));
|
|
541
|
+
},
|
|
542
|
+
// Context-tagged constructions for [n] EXPLICIT / IMPLICIT tagging.
|
|
543
|
+
explicit: function (tagNumber, inner) { return encodeTLV(CLASS_CONTEXT, true, tagNumber, _asBuffer(inner, "build.explicit")); },
|
|
544
|
+
contextPrimitive: function (tagNumber, content) { return encodeTLV(CLASS_CONTEXT, false, tagNumber, _asBuffer(content, "build.contextPrimitive")); },
|
|
545
|
+
contextConstructed: function (tagNumber, content) { return encodeTLV(CLASS_CONTEXT, true, tagNumber, _asBuffer(content, "build.contextConstructed")); },
|
|
546
|
+
raw: function (buf) { return _asBuffer(buf, "build.raw"); },
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
function _asBufferArray(arr, who) {
|
|
550
|
+
if (!Array.isArray(arr)) throw new Asn1Error("asn1/bad-children", who + ": expected an array of TLV buffers");
|
|
551
|
+
return arr.map(function (b) { return _asBuffer(b, who); });
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
module.exports = {
|
|
555
|
+
TAGS: TAGS,
|
|
556
|
+
decode: decode,
|
|
557
|
+
// `encode` is the documented public name for the low-level TLV encoder;
|
|
558
|
+
// `encodeTLV` remains as the descriptive alias.
|
|
559
|
+
encode: encodeTLV,
|
|
560
|
+
encodeTLV: encodeTLV,
|
|
561
|
+
encodeLength: encodeLength,
|
|
562
|
+
encodeIdentifier: encodeIdentifier,
|
|
563
|
+
decodeOidContent: decodeOidContent,
|
|
564
|
+
encodeOidContent: encodeOidContent,
|
|
565
|
+
build: build,
|
|
566
|
+
read: {
|
|
567
|
+
boolean: readBoolean,
|
|
568
|
+
integer: readInteger,
|
|
569
|
+
bitString: readBitString,
|
|
570
|
+
octetString: readOctetString,
|
|
571
|
+
nullValue: readNull,
|
|
572
|
+
oid: readOid,
|
|
573
|
+
string: readString,
|
|
574
|
+
time: readTime,
|
|
575
|
+
},
|
|
576
|
+
};
|
package/lib/constants.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
/**
|
|
5
|
+
* @module pki.C
|
|
6
|
+
* @nav Core
|
|
7
|
+
* @title Constants
|
|
8
|
+
* @order 10
|
|
9
|
+
* @featured true
|
|
10
|
+
* @slug constants
|
|
11
|
+
*
|
|
12
|
+
* @intro
|
|
13
|
+
* Version-stable constant namespace for the toolkit. Scale values are
|
|
14
|
+
* FUNCTIONS, not pre-baked discrete numbers: `C.TIME.days(30)` reads
|
|
15
|
+
* at the call site and computes at boot, so a caller never hand-writes
|
|
16
|
+
* `30 * 24 * 60 * 60 * 1000` (a raw-literal the codebase gate refuses)
|
|
17
|
+
* and a reviewer never has to decode it.
|
|
18
|
+
*
|
|
19
|
+
* Every scale helper is config-time / entry-point validation: it THROWS
|
|
20
|
+
* `ConstantsError` on a non-finite or negative argument, so an operator
|
|
21
|
+
* catches the typo at boot rather than shipping a silently-wrong window.
|
|
22
|
+
*
|
|
23
|
+
* @card
|
|
24
|
+
* Functional scale helpers (`C.TIME.*`, `C.BYTES.*`) plus the toolkit
|
|
25
|
+
* version and shared codec limits.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
var frameworkError = require("./framework-error");
|
|
29
|
+
|
|
30
|
+
var ConstantsError = frameworkError.ConstantsError;
|
|
31
|
+
|
|
32
|
+
// _positive(n, who) — the shared guard every scale helper runs. Config-
|
|
33
|
+
// time tier: a bad scale argument is an authoring bug, so it throws.
|
|
34
|
+
function _positive(n, who) {
|
|
35
|
+
if (typeof n !== "number" || !isFinite(n) || n < 0) {
|
|
36
|
+
throw new ConstantsError(
|
|
37
|
+
"constants/bad-scale",
|
|
38
|
+
who + ": expected a finite number >= 0, got " + String(n)
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
return n;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
var MS_PER_SECOND = 1000;
|
|
45
|
+
var SECONDS_PER_MINUTE = 60;
|
|
46
|
+
var MINUTES_PER_HOUR = 60;
|
|
47
|
+
var HOURS_PER_DAY = 24;
|
|
48
|
+
var DAYS_PER_WEEK = 7;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @primitive pki.C.TIME
|
|
52
|
+
* @signature C.TIME.days(n) -> milliseconds
|
|
53
|
+
* @since 0.1.0
|
|
54
|
+
* @status stable
|
|
55
|
+
*
|
|
56
|
+
* Duration helpers. Each returns an integer count of milliseconds so the
|
|
57
|
+
* value drops straight into `setTimeout`, a validity window, or an OCSP
|
|
58
|
+
* `nextUpdate` computation. Composing reads naturally:
|
|
59
|
+
* `C.TIME.days(365)` for a one-year certificate lifetime.
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* var oneYear = pki.C.TIME.days(365);
|
|
63
|
+
* // -> 31536000000
|
|
64
|
+
*/
|
|
65
|
+
var TIME = {
|
|
66
|
+
ms: function (n) { return Math.round(_positive(n, "C.TIME.ms")); },
|
|
67
|
+
seconds: function (n) { return Math.round(_positive(n, "C.TIME.seconds") * MS_PER_SECOND); },
|
|
68
|
+
minutes: function (n) { return Math.round(_positive(n, "C.TIME.minutes") * SECONDS_PER_MINUTE * MS_PER_SECOND); },
|
|
69
|
+
hours: function (n) { return Math.round(_positive(n, "C.TIME.hours") * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MS_PER_SECOND); },
|
|
70
|
+
days: function (n) { return Math.round(_positive(n, "C.TIME.days") * HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MS_PER_SECOND); },
|
|
71
|
+
weeks: function (n) { return Math.round(_positive(n, "C.TIME.weeks") * DAYS_PER_WEEK * HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MS_PER_SECOND); },
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
var BYTES_PER_KIB = 1024;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @primitive pki.C.BYTES
|
|
78
|
+
* @signature C.BYTES.mib(n) -> bytes
|
|
79
|
+
* @since 0.1.0
|
|
80
|
+
* @status stable
|
|
81
|
+
*
|
|
82
|
+
* Binary-magnitude size helpers. Each returns an integer byte count for
|
|
83
|
+
* codec limits (max DER input, max PEM block) so a size bound reads as
|
|
84
|
+
* `C.BYTES.mib(16)` instead of `16 * 1024 * 1024`.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* var cap = pki.C.BYTES.mib(16);
|
|
88
|
+
* // -> 16777216
|
|
89
|
+
*/
|
|
90
|
+
var BYTES = {
|
|
91
|
+
b: function (n) { return Math.round(_positive(n, "C.BYTES.b")); },
|
|
92
|
+
kib: function (n) { return Math.round(_positive(n, "C.BYTES.kib") * BYTES_PER_KIB); },
|
|
93
|
+
mib: function (n) { return Math.round(_positive(n, "C.BYTES.mib") * BYTES_PER_KIB * BYTES_PER_KIB); },
|
|
94
|
+
gib: function (n) { return Math.round(_positive(n, "C.BYTES.gib") * BYTES_PER_KIB * BYTES_PER_KIB * BYTES_PER_KIB); },
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// LIMITS — shared codec ceilings. A DER document larger than this, or
|
|
98
|
+
// nested deeper than this, is refused before the parser walks it: an
|
|
99
|
+
// unbounded length prefix or a pathologically-nested SEQUENCE is a
|
|
100
|
+
// classic decoder-DoS, so the bound is a fail-closed default rather
|
|
101
|
+
// than a tunable an operator must remember to set.
|
|
102
|
+
var LIMITS = {
|
|
103
|
+
DER_MAX_BYTES: BYTES.mib(16),
|
|
104
|
+
DER_MAX_DEPTH: 64,
|
|
105
|
+
PEM_MAX_BYTES: BYTES.mib(16),
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
var VERSION = "0.1.0";
|
|
109
|
+
|
|
110
|
+
module.exports = {
|
|
111
|
+
TIME: TIME,
|
|
112
|
+
BYTES: BYTES,
|
|
113
|
+
LIMITS: LIMITS,
|
|
114
|
+
version: VERSION,
|
|
115
|
+
};
|