@blamejs/pki 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/x509.js DELETED
@@ -1,411 +0,0 @@
1
- // SPDX-License-Identifier: Apache-2.0
2
- // Copyright (c) blamejs contributors
3
- "use strict";
4
- /**
5
- * @module pki.x509
6
- * @nav Certificates
7
- * @title X.509
8
- * @order 100
9
- * @featured true
10
- * @slug x509
11
- *
12
- * @intro
13
- * X.509 certificate handling per RFC 5280. The seed surface is
14
- * `parse` — turn a DER or PEM certificate into a structured,
15
- * fully-decoded object: version, serial, signature algorithm, issuer
16
- * and subject distinguished names, validity window (as real `Date`s),
17
- * subject public-key info, and the extension list. The parser composes
18
- * the strict DER codec and the OID registry, so every field is
19
- * validated on the way in and every algorithm / attribute / extension
20
- * is named where the registry knows it.
21
- *
22
- * The raw `tbsCertificate` bytes are returned alongside the parsed
23
- * fields so a signature-verification layer can hash exactly the bytes
24
- * that were signed rather than re-encoding and hoping for round-trip
25
- * fidelity.
26
- *
27
- * @card
28
- * Parse DER / PEM X.509 certificates into structured, validated fields
29
- * with named algorithms, extensions, and real-`Date` validity windows.
30
- */
31
-
32
- var constants = require("./constants");
33
- var asn1 = require("./asn1-der");
34
- var oid = require("./oid");
35
- var frameworkError = require("./framework-error");
36
-
37
- var CertificateError = frameworkError.CertificateError;
38
- var PemError = frameworkError.PemError;
39
-
40
- var TAGS = asn1.TAGS;
41
-
42
- // Distinguished-name attribute short labels (RFC 4514 §3 + common use).
43
- var DN_SHORT = {
44
- commonName: "CN",
45
- countryName: "C",
46
- localityName: "L",
47
- stateOrProvinceName: "ST",
48
- streetAddress: "STREET",
49
- organizationName: "O",
50
- organizationalUnitName: "OU",
51
- domainComponent: "DC",
52
- surname: "SN",
53
- givenName: "GN",
54
- serialNumber: "SERIALNUMBER",
55
- emailAddress: "emailAddress",
56
- };
57
-
58
- // ---- PEM -------------------------------------------------------------
59
-
60
- var PEM_RE = /-----BEGIN ([A-Z0-9 ]+)-----([\s\S]*?)-----END \1-----/;
61
-
62
- /**
63
- * @primitive pki.x509.pemDecode
64
- * @signature pki.x509.pemDecode(text, label?) -> Buffer
65
- * @since 0.1.0
66
- * @status stable
67
- * @spec RFC 7468, RFC 5280
68
- * @related pki.x509.pemEncode
69
- *
70
- * Extract the DER bytes from a PEM block. With `label` given (e.g.
71
- * `"CERTIFICATE"`) the block type must match; without it, the first block
72
- * is taken. Throws `PemError` on a missing / mismatched envelope or a
73
- * non-base64 body.
74
- *
75
- * @example
76
- * var der = pki.x509.pemDecode(pemText, "CERTIFICATE");
77
- */
78
- function pemDecode(text, label) {
79
- if (Buffer.isBuffer(text)) text = text.toString("latin1");
80
- if (typeof text !== "string") throw new PemError("pem/bad-input", "pemDecode expects a string or Buffer");
81
- if (text.length > constants.LIMITS.PEM_MAX_BYTES) throw new PemError("pem/too-large", "PEM input exceeds size cap");
82
- var m = PEM_RE.exec(text);
83
- if (!m) throw new PemError("pem/no-block", "no PEM block found");
84
- if (label && m[1] !== label) throw new PemError("pem/label-mismatch", "expected " + JSON.stringify(label) + " block, got " + JSON.stringify(m[1]));
85
- var b64 = m[2].replace(/[\r\n\t ]+/g, "");
86
- if (!/^[A-Za-z0-9+/]*={0,2}$/.test(b64)) throw new PemError("pem/bad-base64", "PEM body is not valid base64");
87
- return Buffer.from(b64, "base64");
88
- }
89
-
90
- /**
91
- * @primitive pki.x509.pemEncode
92
- * @signature pki.x509.pemEncode(der, label) -> string
93
- * @since 0.1.0
94
- * @status stable
95
- * @spec RFC 7468, RFC 5280
96
- * @related pki.x509.pemDecode
97
- *
98
- * Wrap DER bytes in a PEM envelope with 64-column base64 lines.
99
- *
100
- * @example
101
- * var pem = pki.x509.pemEncode(der, "CERTIFICATE");
102
- */
103
- function pemEncode(der, label) {
104
- if (typeof label !== "string" || label.length === 0) throw new PemError("pem/bad-label", "pemEncode requires a label");
105
- var buf = Buffer.isBuffer(der) ? der : Buffer.from(der);
106
- var b64 = buf.toString("base64").replace(/(.{64})/g, "$1\n").replace(/\n$/, "");
107
- return "-----BEGIN " + label + "-----\n" + b64 + "\n-----END " + label + "-----\n";
108
- }
109
-
110
- // ---- helpers ---------------------------------------------------------
111
-
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
- };
122
- }
123
-
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)));
143
- }
144
- return "#" + node.bytes.toString("hex");
145
- }
146
- }
147
-
148
- function _escapeDnValue(v) {
149
- return v.replace(/([,+"\\<>;])/g, "\\$1");
150
- }
151
-
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(", ") };
181
- }
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);
188
- }
189
-
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];
205
- if (!ext.children || ext.children.length < 2) {
206
- throw new CertificateError("x509/bad-extension", "Extension must be a SEQUENCE");
207
- }
208
- 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);
213
- }
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];
222
- }
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
- }
232
-
233
- // ---- parse -----------------------------------------------------------
234
-
235
- /**
236
- * @primitive pki.x509.parse
237
- * @signature pki.x509.parse(input) -> certificate
238
- * @since 0.1.0
239
- * @status stable
240
- * @spec RFC 5280, X.509
241
- * @defends malformed-certificate-parse (CWE-20)
242
- * @related pki.asn1.decode, pki.oid.name
243
- *
244
- * Parse a DER `Buffer` or a PEM string/Buffer into a structured
245
- * certificate: `{ version, serialNumber, serialNumberHex,
246
- * signatureAlgorithm, issuer, subject, validity, subjectPublicKeyInfo,
247
- * extensions, tbsBytes, signatureValue }`. Distinguished names come back
248
- * both as a rendered `dn` string and as structured `rdns`; the validity
249
- * window is real `Date`s; `tbsBytes` is the exact signed byte range for a
250
- * downstream verifier.
251
- *
252
- * Throws `CertificateError` when the bytes are not a well-formed
253
- * certificate and `Asn1Error` when the underlying DER is malformed.
254
- *
255
- * @example
256
- * var cert = pki.x509.parse(pemString);
257
- * cert.subject.dn; // "CN=example.com, O=Example"
258
- * cert.validity.notAfter; // Date
259
- * cert.signatureAlgorithm.name; // "sha256WithRSAEncryption"
260
- */
261
- function parse(input) {
262
- var der;
263
- if (typeof input === "string" || (Buffer.isBuffer(input) && input.length >= 5 && input.toString("latin1", 0, 5) === "-----")) {
264
- der = pemDecode(input, "CERTIFICATE");
265
- } else if (Buffer.isBuffer(input) || input instanceof Uint8Array) {
266
- der = Buffer.isBuffer(input) ? input : Buffer.from(input);
267
- } else {
268
- throw new CertificateError("x509/bad-input", "parse expects a DER Buffer or a PEM string");
269
- }
270
-
271
- var root;
272
- try {
273
- root = asn1.decode(der);
274
- } catch (e) {
275
- throw new CertificateError("x509/bad-der", "certificate DER did not decode: " + e.message, e);
276
- }
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
- };
405
- }
406
-
407
- module.exports = {
408
- parse: parse,
409
- pemDecode: pemDecode,
410
- pemEncode: pemEncode,
411
- };