@blamejs/pki 0.1.6 → 0.1.8

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,393 +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 schema = require("./asn1-schema");
35
- var oid = require("./oid");
36
- var frameworkError = require("./framework-error");
37
-
38
- var CertificateError = frameworkError.CertificateError;
39
- var PemError = frameworkError.PemError;
40
-
41
- // Distinguished-name attribute short labels (RFC 4514 §3 + common use).
42
- var DN_SHORT = {
43
- commonName: "CN",
44
- countryName: "C",
45
- localityName: "L",
46
- stateOrProvinceName: "ST",
47
- streetAddress: "STREET",
48
- organizationName: "O",
49
- organizationalUnitName: "OU",
50
- domainComponent: "DC",
51
- surname: "SN",
52
- givenName: "GN",
53
- serialNumber: "SERIALNUMBER",
54
- emailAddress: "emailAddress",
55
- };
56
-
57
- // ---- PEM -------------------------------------------------------------
58
-
59
- var PEM_RE = /-----BEGIN ([A-Z0-9 ]+)-----([\s\S]*?)-----END \1-----/;
60
-
61
- /**
62
- * @primitive pki.x509.pemDecode
63
- * @signature pki.x509.pemDecode(text, label?) -> Buffer
64
- * @since 0.1.0
65
- * @status stable
66
- * @spec RFC 7468, RFC 5280
67
- * @related pki.x509.pemEncode
68
- *
69
- * Extract the DER bytes from a PEM block. With `label` given (e.g.
70
- * `"CERTIFICATE"`) the block type must match; without it, the first block
71
- * is taken. Throws `PemError` on a missing / mismatched envelope or a
72
- * non-base64 body.
73
- *
74
- * @example
75
- * var der = pki.x509.pemDecode(pemText, "CERTIFICATE");
76
- */
77
- function pemDecode(text, label) {
78
- if (Buffer.isBuffer(text)) text = text.toString("latin1");
79
- if (typeof text !== "string") throw new PemError("pem/bad-input", "pemDecode expects a string or Buffer");
80
- if (text.length > constants.LIMITS.PEM_MAX_BYTES) throw new PemError("pem/too-large", "PEM input exceeds size cap");
81
- var m = PEM_RE.exec(text);
82
- if (!m) throw new PemError("pem/no-block", "no PEM block found");
83
- if (label && m[1] !== label) throw new PemError("pem/label-mismatch", "expected " + JSON.stringify(label) + " block, got " + JSON.stringify(m[1]));
84
- var b64 = m[2].replace(/[\r\n\t ]+/g, "");
85
- if (!/^[A-Za-z0-9+/]*={0,2}$/.test(b64)) throw new PemError("pem/bad-base64", "PEM body is not valid base64");
86
- return Buffer.from(b64, "base64");
87
- }
88
-
89
- /**
90
- * @primitive pki.x509.pemEncode
91
- * @signature pki.x509.pemEncode(der, label) -> string
92
- * @since 0.1.0
93
- * @status stable
94
- * @spec RFC 7468, RFC 5280
95
- * @related pki.x509.pemDecode
96
- *
97
- * Wrap DER bytes in a PEM envelope with 64-column base64 lines.
98
- *
99
- * @example
100
- * var pem = pki.x509.pemEncode(der, "CERTIFICATE");
101
- */
102
- function pemEncode(der, label) {
103
- if (typeof label !== "string" || label.length === 0) throw new PemError("pem/bad-label", "pemEncode requires a label");
104
- var buf = Buffer.isBuffer(der) ? der : Buffer.from(der);
105
- var b64 = buf.toString("base64").replace(/(.{64})/g, "$1\n").replace(/\n$/, "");
106
- return "-----BEGIN " + label + "-----\n" + b64 + "\n-----END " + label + "-----\n";
107
- }
108
-
109
- // ---- helpers ---------------------------------------------------------
110
-
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
- });
130
- }
131
- var ALGORITHM_IDENTIFIER = algorithmIdentifier(NS);
132
-
133
- function _algId(node) { return schema.walk(ALGORITHM_IDENTIFIER, node, NS).result; }
134
-
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");
152
- }
153
- });
154
- }
155
-
156
- function _escapeDnValue(v) {
157
- return v.replace(/([,+"\\<>;])/g, "\\$1");
158
- }
159
-
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
- });
175
- }
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
- });
198
- }
199
- var NAME = name(NS);
200
-
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) {
235
- if (!ext.children || ext.children.length < 2) {
236
- throw ns.E(ns.prefix + "/bad-extension", "Extension must be a SEQUENCE");
237
- }
238
- var extnID = asn1.read.oid(ext.children[0]);
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)");
307
- }
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");
314
- }
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
- });
341
-
342
- // ---- parse -----------------------------------------------------------
343
-
344
- /**
345
- * @primitive pki.x509.parse
346
- * @signature pki.x509.parse(input) -> certificate
347
- * @since 0.1.0
348
- * @status stable
349
- * @spec RFC 5280, X.509
350
- * @defends malformed-certificate-parse (CWE-20)
351
- * @related pki.asn1.decode, pki.oid.name
352
- *
353
- * Parse a DER `Buffer` or a PEM string/Buffer into a structured
354
- * certificate: `{ version, serialNumber, serialNumberHex,
355
- * signatureAlgorithm, issuer, subject, validity, subjectPublicKeyInfo,
356
- * extensions, tbsBytes, signatureValue }`. Distinguished names come back
357
- * both as a rendered `dn` string and as structured `rdns`; the validity
358
- * window is real `Date`s; `tbsBytes` is the exact signed byte range for a
359
- * downstream verifier.
360
- *
361
- * Throws `CertificateError` when the bytes are not a well-formed
362
- * certificate and `Asn1Error` when the underlying DER is malformed.
363
- *
364
- * @example
365
- * var cert = pki.x509.parse(pemString);
366
- * cert.subject.dn; // "CN=example.com, O=Example"
367
- * cert.validity.notAfter; // Date
368
- * cert.signatureAlgorithm.name; // "sha256WithRSAEncryption"
369
- */
370
- function parse(input) {
371
- var der;
372
- if (typeof input === "string" || (Buffer.isBuffer(input) && input.length >= 5 && input.toString("latin1", 0, 5) === "-----")) {
373
- der = pemDecode(input, "CERTIFICATE");
374
- } else if (Buffer.isBuffer(input) || input instanceof Uint8Array) {
375
- der = Buffer.isBuffer(input) ? input : Buffer.from(input);
376
- } else {
377
- throw new CertificateError("x509/bad-input", "parse expects a DER Buffer or a PEM string");
378
- }
379
-
380
- var root;
381
- try {
382
- root = asn1.decode(der);
383
- } catch (e) {
384
- throw new CertificateError("x509/bad-der", "certificate DER did not decode: " + e.message, e);
385
- }
386
- return schema.walk(CERTIFICATE, root, NS).result;
387
- }
388
-
389
- module.exports = {
390
- parse: parse,
391
- pemDecode: pemDecode,
392
- pemEncode: pemEncode,
393
- };