@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/lib/x509.js ADDED
@@ -0,0 +1,320 @@
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
+ * @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
+ * @related pki.x509.pemDecode
95
+ *
96
+ * Wrap DER bytes in a PEM envelope with 64-column base64 lines.
97
+ *
98
+ * @example
99
+ * var pem = pki.x509.pemEncode(der, "CERTIFICATE");
100
+ */
101
+ function pemEncode(der, label) {
102
+ if (typeof label !== "string" || label.length === 0) throw new PemError("pem/bad-label", "pemEncode requires a label");
103
+ var buf = Buffer.isBuffer(der) ? der : Buffer.from(der);
104
+ var b64 = buf.toString("base64").replace(/(.{64})/g, "$1\n").replace(/\n$/, "");
105
+ return "-----BEGIN " + label + "-----\n" + b64 + "\n-----END " + label + "-----\n";
106
+ }
107
+
108
+ // ---- helpers ---------------------------------------------------------
109
+
110
+ function _algId(node) {
111
+ if (node.tagClass !== "universal" || node.tagNumber !== TAGS.SEQUENCE || !node.children.length) {
112
+ throw new CertificateError("x509/bad-algorithm-identifier", "AlgorithmIdentifier must be a non-empty SEQUENCE");
113
+ }
114
+ var dotted = asn1.read.oid(node.children[0]);
115
+ return {
116
+ oid: dotted,
117
+ name: oid.name(dotted) || null,
118
+ parameters: node.children.length > 1 ? node.children[1].bytes : null,
119
+ };
120
+ }
121
+
122
+ function _attrValueToString(node) {
123
+ try { return asn1.read.string(node); }
124
+ catch (_e) {
125
+ // Not a recognized string type (e.g. an ANY-typed value) — surface
126
+ // the raw bytes as hex so the DN is still representable.
127
+ return "#" + (node.content ? node.content.toString("hex") : node.bytes.toString("hex"));
128
+ }
129
+ }
130
+
131
+ function _escapeDnValue(v) {
132
+ return v.replace(/([,+"\\<>;])/g, "\\$1");
133
+ }
134
+
135
+ function _parseName(node) {
136
+ if (node.tagClass !== "universal" || node.tagNumber !== TAGS.SEQUENCE) {
137
+ throw new CertificateError("x509/bad-name", "Name must be an RDNSequence (SEQUENCE)");
138
+ }
139
+ var rdns = [];
140
+ var parts = [];
141
+ for (var i = 0; i < node.children.length; i++) {
142
+ var rdn = node.children[i];
143
+ if (rdn.tagClass !== "universal" || rdn.tagNumber !== TAGS.SET) {
144
+ throw new CertificateError("x509/bad-rdn", "RelativeDistinguishedName must be a SET");
145
+ }
146
+ var atvs = [];
147
+ var atvParts = [];
148
+ for (var j = 0; j < rdn.children.length; j++) {
149
+ var atv = rdn.children[j];
150
+ if (!atv.children || atv.children.length < 2) {
151
+ throw new CertificateError("x509/bad-atv", "AttributeTypeAndValue must be a SEQUENCE of {type, value}");
152
+ }
153
+ var typeOid = asn1.read.oid(atv.children[0]);
154
+ var typeName = oid.name(typeOid);
155
+ var value = _attrValueToString(atv.children[1]);
156
+ atvs.push({ type: typeOid, name: typeName || null, value: value });
157
+ var label = (typeName && DN_SHORT[typeName]) || typeName || typeOid;
158
+ atvParts.push(label + "=" + _escapeDnValue(value));
159
+ }
160
+ rdns.push(atvs);
161
+ parts.push(atvParts.join("+"));
162
+ }
163
+ return { rdns: rdns, dn: parts.join(", ") };
164
+ }
165
+
166
+ function _parseValidityTime(node) {
167
+ if (node.tagClass !== "universal" || (node.tagNumber !== TAGS.UTC_TIME && node.tagNumber !== TAGS.GENERALIZED_TIME)) {
168
+ throw new CertificateError("x509/bad-time", "Validity time must be UTCTime or GeneralizedTime");
169
+ }
170
+ return asn1.read.time(node);
171
+ }
172
+
173
+ function _parseExtensions(seqNode) {
174
+ var out = [];
175
+ for (var i = 0; i < seqNode.children.length; i++) {
176
+ var ext = seqNode.children[i];
177
+ if (!ext.children || ext.children.length < 2) {
178
+ throw new CertificateError("x509/bad-extension", "Extension must be a SEQUENCE");
179
+ }
180
+ var extnID = asn1.read.oid(ext.children[0]);
181
+ var critical = false;
182
+ var valueNode;
183
+ if (ext.children.length === 3) {
184
+ critical = asn1.read.boolean(ext.children[1]);
185
+ valueNode = ext.children[2];
186
+ } else {
187
+ valueNode = ext.children[1];
188
+ }
189
+ out.push({
190
+ oid: extnID,
191
+ name: oid.name(extnID) || null,
192
+ critical: critical,
193
+ value: asn1.read.octetString(valueNode),
194
+ });
195
+ }
196
+ return out;
197
+ }
198
+
199
+ // ---- parse -----------------------------------------------------------
200
+
201
+ /**
202
+ * @primitive pki.x509.parse
203
+ * @signature pki.x509.parse(input) -> certificate
204
+ * @since 0.1.0
205
+ * @status stable
206
+ * @related pki.asn1.decode, pki.oid.name
207
+ *
208
+ * Parse a DER `Buffer` or a PEM string/Buffer into a structured
209
+ * certificate: `{ version, serialNumber, serialNumberHex,
210
+ * signatureAlgorithm, issuer, subject, validity, subjectPublicKeyInfo,
211
+ * extensions, tbsBytes, signatureValue }`. Distinguished names come back
212
+ * both as a rendered `dn` string and as structured `rdns`; the validity
213
+ * window is real `Date`s; `tbsBytes` is the exact signed byte range for a
214
+ * downstream verifier.
215
+ *
216
+ * Throws `CertificateError` when the bytes are not a well-formed
217
+ * certificate and `Asn1Error` when the underlying DER is malformed.
218
+ *
219
+ * @example
220
+ * var cert = pki.x509.parse(pemString);
221
+ * cert.subject.dn; // "CN=example.com, O=Example"
222
+ * cert.validity.notAfter; // Date
223
+ * cert.signatureAlgorithm.name; // "sha256WithRSAEncryption"
224
+ */
225
+ function parse(input) {
226
+ var der;
227
+ if (typeof input === "string" || (Buffer.isBuffer(input) && input.length >= 5 && input.toString("latin1", 0, 5) === "-----")) {
228
+ der = pemDecode(input, "CERTIFICATE");
229
+ } else if (Buffer.isBuffer(input) || input instanceof Uint8Array) {
230
+ der = Buffer.isBuffer(input) ? input : Buffer.from(input);
231
+ } else {
232
+ throw new CertificateError("x509/bad-input", "parse expects a DER Buffer or a PEM string");
233
+ }
234
+
235
+ var root;
236
+ try {
237
+ root = asn1.decode(der);
238
+ } catch (e) {
239
+ throw new CertificateError("x509/bad-der", "certificate DER did not decode: " + e.message, e);
240
+ }
241
+ if (root.tagClass !== "universal" || root.tagNumber !== TAGS.SEQUENCE || !root.children || root.children.length !== 3) {
242
+ throw new CertificateError("x509/not-a-certificate", "Certificate must be a SEQUENCE of {tbsCertificate, signatureAlgorithm, signature}");
243
+ }
244
+
245
+ var tbs = root.children[0];
246
+ var outerSigAlg = root.children[1];
247
+ var sigValueNode = root.children[2];
248
+ if (!tbs.children || tbs.children.length < 6) {
249
+ throw new CertificateError("x509/bad-tbs", "tbsCertificate is too short");
250
+ }
251
+
252
+ var idx = 0;
253
+ var version = 1;
254
+ var first = tbs.children[0];
255
+ if (first.tagClass === "context" && first.tagNumber === 0) {
256
+ if (!first.children || !first.children.length) throw new CertificateError("x509/bad-version", "version [0] must wrap an INTEGER");
257
+ version = Number(asn1.read.integer(first.children[0])) + 1;
258
+ idx = 1;
259
+ }
260
+
261
+ var serialNode = tbs.children[idx++];
262
+ var serialNumber = asn1.read.integer(serialNode);
263
+ var innerSigAlg = tbs.children[idx++];
264
+ var issuer = _parseName(tbs.children[idx++]);
265
+ var validityNode = tbs.children[idx++];
266
+ var subject = _parseName(tbs.children[idx++]);
267
+ var spkiNode = tbs.children[idx++];
268
+
269
+ if (!validityNode.children || validityNode.children.length !== 2) {
270
+ throw new CertificateError("x509/bad-validity", "Validity must be a SEQUENCE of {notBefore, notAfter}");
271
+ }
272
+ var validity = {
273
+ notBefore: _parseValidityTime(validityNode.children[0]),
274
+ notAfter: _parseValidityTime(validityNode.children[1]),
275
+ };
276
+
277
+ if (!spkiNode.children || spkiNode.children.length !== 2) {
278
+ throw new CertificateError("x509/bad-spki", "SubjectPublicKeyInfo must be a SEQUENCE of {algorithm, subjectPublicKey}");
279
+ }
280
+ var spkiBits = asn1.read.bitString(spkiNode.children[1]);
281
+ var subjectPublicKeyInfo = {
282
+ algorithm: _algId(spkiNode.children[0]),
283
+ publicKey: { unusedBits: spkiBits.unusedBits, bytes: spkiBits.bytes },
284
+ bytes: spkiNode.bytes,
285
+ };
286
+
287
+ // Remaining tbs children: optional issuerUniqueID [1], subjectUniqueID
288
+ // [2], and extensions [3] EXPLICIT. Only extensions are surfaced.
289
+ var extensions = [];
290
+ for (; idx < tbs.children.length; idx++) {
291
+ var t = tbs.children[idx];
292
+ if (t.tagClass === "context" && t.tagNumber === 3) {
293
+ if (!t.children || !t.children.length) throw new CertificateError("x509/bad-extensions", "extensions [3] must wrap a SEQUENCE");
294
+ extensions = _parseExtensions(t.children[0]);
295
+ }
296
+ }
297
+
298
+ var sigBits = asn1.read.bitString(sigValueNode);
299
+
300
+ return {
301
+ version: version,
302
+ serialNumber: serialNumber,
303
+ serialNumberHex: serialNode.content.toString("hex"),
304
+ signatureAlgorithm: _algId(outerSigAlg),
305
+ tbsSignatureAlgorithm: _algId(innerSigAlg),
306
+ issuer: issuer,
307
+ subject: subject,
308
+ validity: validity,
309
+ subjectPublicKeyInfo: subjectPublicKeyInfo,
310
+ extensions: extensions,
311
+ tbsBytes: tbs.bytes,
312
+ signatureValue: { unusedBits: sigBits.unusedBits, bytes: sigBits.bytes },
313
+ };
314
+ }
315
+
316
+ module.exports = {
317
+ parse: parse,
318
+ pemDecode: pemDecode,
319
+ pemEncode: pemEncode,
320
+ };
package/package.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "@blamejs/pki",
3
+ "version": "0.1.1",
4
+ "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
5
+ "license": "Apache-2.0",
6
+ "author": "blamejs contributors",
7
+ "homepage": "https://pkijs.com",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/blamejs/pki.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/blamejs/pki/issues"
14
+ },
15
+ "main": "index.js",
16
+ "bin": {
17
+ "pki": "bin/pki.js"
18
+ },
19
+ "keywords": [
20
+ "pki",
21
+ "x509",
22
+ "asn1",
23
+ "der",
24
+ "oid",
25
+ "cms",
26
+ "pkcs7",
27
+ "ocsp",
28
+ "crl",
29
+ "csr",
30
+ "pkcs10",
31
+ "pkcs8",
32
+ "pkcs12",
33
+ "tsp",
34
+ "timestamp",
35
+ "certificate",
36
+ "post-quantum",
37
+ "pqc",
38
+ "ml-dsa",
39
+ "ml-kem",
40
+ "slh-dsa",
41
+ "composite-signatures",
42
+ "zero-dependencies",
43
+ "vendored-dependencies",
44
+ "pure-javascript",
45
+ "supply-chain",
46
+ "security-first",
47
+ "owns-its-stack"
48
+ ],
49
+ "engines": {
50
+ "node": ">=24.18.0"
51
+ },
52
+ "files": [
53
+ "index.js",
54
+ "bin/",
55
+ "lib/",
56
+ "CHANGELOG.md",
57
+ "LICENSE",
58
+ "MIGRATING.md",
59
+ "NOTICE",
60
+ "README.md",
61
+ "sbom.cdx.json"
62
+ ],
63
+ "scripts": {
64
+ "test": "node test/smoke.js",
65
+ "lint": "eslint --max-warnings 0 .",
66
+ "fuzz": "npm ci --prefix fuzz && npx --prefix fuzz jazzer fuzz/asn1-der.fuzz.js -- -max_total_time=60",
67
+ "gates": "node test/layer-0-primitives/codebase-patterns.test.js && node scripts/validate-source-comment-blocks.js && node scripts/check-api-snapshot.js",
68
+ "prepack": "node scripts/check-pack-against-gitignore.js",
69
+ "check:vendor-currency": "node scripts/check-vendor-currency.js"
70
+ },
71
+ "devDependencies": {
72
+ "esbuild": "0.28.1"
73
+ }
74
+ }
package/sbom.cdx.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
+ "bomFormat": "CycloneDX",
4
+ "specVersion": "1.5",
5
+ "serialNumber": "urn:uuid:3eef8bae-7aaa-4ecf-a97b-3774a378db59",
6
+ "version": 1,
7
+ "metadata": {
8
+ "timestamp": "2026-07-04T12:48:34.391Z",
9
+ "lifecycles": [
10
+ {
11
+ "phase": "build"
12
+ }
13
+ ],
14
+ "tools": [
15
+ {
16
+ "vendor": "npm",
17
+ "name": "cli",
18
+ "version": "11.16.0"
19
+ }
20
+ ],
21
+ "component": {
22
+ "bom-ref": "@blamejs/pki@0.1.1",
23
+ "type": "application",
24
+ "name": "pki",
25
+ "version": "0.1.1",
26
+ "scope": "required",
27
+ "author": "blamejs contributors",
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.1",
30
+ "properties": [],
31
+ "externalReferences": [
32
+ {
33
+ "type": "vcs",
34
+ "url": "git+https://github.com/blamejs/pki.git"
35
+ },
36
+ {
37
+ "type": "website",
38
+ "url": "https://pkijs.com"
39
+ },
40
+ {
41
+ "type": "issue-tracker",
42
+ "url": "https://github.com/blamejs/pki/issues"
43
+ }
44
+ ],
45
+ "licenses": [
46
+ {
47
+ "license": {
48
+ "id": "Apache-2.0"
49
+ }
50
+ }
51
+ ]
52
+ }
53
+ },
54
+ "components": [],
55
+ "dependencies": [
56
+ {
57
+ "ref": "@blamejs/pki@0.1.1",
58
+ "dependsOn": []
59
+ }
60
+ ]
61
+ }