@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.
@@ -0,0 +1,260 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ /**
5
+ * @module pki.schema.x509
6
+ * @nav Schema
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 asn1 = require("./asn1-der");
33
+ var schema = require("./schema-engine");
34
+ var oid = require("./oid");
35
+ var frameworkError = require("./framework-error");
36
+ var pkix = require("./schema-pkix");
37
+
38
+ var CertificateError = frameworkError.CertificateError;
39
+ var PemError = frameworkError.PemError;
40
+
41
+ // ---- PEM -------------------------------------------------------------
42
+
43
+ /**
44
+ * @primitive pki.schema.x509.pemDecode
45
+ * @signature pki.schema.x509.pemDecode(text, label?) -> Buffer
46
+ * @since 0.1.7
47
+ * @status stable
48
+ * @spec RFC 7468, RFC 5280
49
+ * @related pki.schema.x509.pemEncode
50
+ *
51
+ * Extract the DER bytes from a PEM block. With `label` given (e.g.
52
+ * `"CERTIFICATE"`) the block type must match; without it, the first block
53
+ * is taken. Throws `PemError` on a missing / mismatched envelope or a
54
+ * non-base64 body.
55
+ *
56
+ * @example
57
+ * var der = pki.schema.x509.pemDecode(pemText, "CERTIFICATE");
58
+ */
59
+ function pemDecode(text, label) { return pkix.pemDecode(text, label, PemError); }
60
+
61
+ /**
62
+ * @primitive pki.schema.x509.pemEncode
63
+ * @signature pki.schema.x509.pemEncode(der, label) -> string
64
+ * @since 0.1.7
65
+ * @status stable
66
+ * @spec RFC 7468, RFC 5280
67
+ * @related pki.schema.x509.pemDecode
68
+ *
69
+ * Wrap DER bytes in a PEM envelope with 64-column base64 lines.
70
+ *
71
+ * @example
72
+ * var pem = pki.schema.x509.pemEncode(der, "CERTIFICATE");
73
+ */
74
+ function pemEncode(der, label) { return pkix.pemEncode(der, label, PemError); }
75
+
76
+ // ---- helpers ---------------------------------------------------------
77
+
78
+ // The x509 error namespace the schema engine walks under: prefix names the
79
+ // error family and E constructs the typed CertificateError. The shared PKIX
80
+ // sub-schemas (AlgorithmIdentifier, Name, Extension) live in lib/pkix-schema.js
81
+ // as ns-parameterized factories so crl.js / cms.js reuse them while emitting
82
+ // their own crl/*, cms/* codes; here they are instantiated under this NS.
83
+ var NS = { prefix: "x509", E: function (code, message) { return new CertificateError(code, message); }, oid: oid };
84
+
85
+ var ALGORITHM_IDENTIFIER = pkix.algorithmIdentifier(NS);
86
+ var NAME = pkix.name(NS);
87
+
88
+ // Validity ::= SEQUENCE { notBefore Time, notAfter Time }. The historical guard
89
+ // checked only children.length===2 (not the SEQUENCE tag) -> assert "constructed".
90
+ var VALIDITY = schema.seq([
91
+ schema.field("notBefore", schema.time(NS)),
92
+ schema.field("notAfter", schema.time(NS)),
93
+ ], {
94
+ assert: "constructed", arity: { exact: 2 }, code: "x509/bad-validity", what: "Validity",
95
+ build: function (m) { return { notBefore: m.fields.notBefore.value, notAfter: m.fields.notAfter.value }; },
96
+ });
97
+
98
+ // SubjectPublicKeyInfo ::= SEQUENCE { algorithm AlgorithmIdentifier, subjectPublicKey BIT STRING }.
99
+ var SPKI = schema.seq([
100
+ schema.field("algorithm", ALGORITHM_IDENTIFIER),
101
+ schema.field("subjectPublicKey", schema.bitString()),
102
+ ], {
103
+ assert: "constructed", arity: { exact: 2 }, code: "x509/bad-spki", what: "SubjectPublicKeyInfo",
104
+ build: function (m) {
105
+ return {
106
+ algorithm: m.fields.algorithm.value.result, // algorithm field = ALGORITHM_IDENTIFIER seq-match; .value = its build
107
+ publicKey: { unusedBits: m.fields.subjectPublicKey.value.unusedBits, bytes: m.fields.subjectPublicKey.value.bytes },
108
+ bytes: m.node.bytes,
109
+ };
110
+ },
111
+ });
112
+
113
+ var EXTENSIONS = pkix.extensions(NS);
114
+
115
+ // Version ::= INTEGER { v1(0), v2(1), v3(2) }, [0] EXPLICIT DEFAULT v1. Read as a
116
+ // BigInt so an out-of-range value can't coerce to a float; reject an explicitly-
117
+ // encoded v1 (DER forbids encoding the DEFAULT).
118
+ function readVersion(ns) {
119
+ return schema.decode(function (n) {
120
+ var v = asn1.read.integer(n);
121
+ if (v === 0n) throw ns.E(ns.prefix + "/bad-version", "DER forbids explicitly encoding the default version v1");
122
+ if (v === 1n) return 2;
123
+ if (v === 2n) return 3;
124
+ throw ns.E(ns.prefix + "/bad-version", "unsupported certificate version " + v.toString());
125
+ });
126
+ }
127
+
128
+ // TBSCertificate ::= SEQUENCE { version [0] EXPLICIT DEFAULT v1, serialNumber
129
+ // INTEGER, signature AlgorithmIdentifier, issuer Name, validity Validity,
130
+ // subject Name, subjectPublicKeyInfo SubjectPublicKeyInfo, issuerUniqueID [1]
131
+ // IMPLICIT OPTIONAL, subjectUniqueID [2] IMPLICIT OPTIONAL, extensions [3]
132
+ // EXPLICIT OPTIONAL }. The trailing fields are at-most-once, in increasing tag
133
+ // order (the engine enforces it); only extensions are surfaced.
134
+ var CERTIFICATE_TBS = schema.seq([
135
+ schema.optional("version", readVersion(NS), { tag: 0, explicit: true, emptyCode: "x509/bad-version", default: 1 }),
136
+ schema.field("serialNumber", schema.integerLeaf()),
137
+ schema.field("signature", ALGORITHM_IDENTIFIER),
138
+ schema.field("issuer", NAME),
139
+ schema.field("validity", VALIDITY),
140
+ schema.field("subject", NAME),
141
+ schema.field("subjectPublicKeyInfo", SPKI),
142
+ schema.trailing([
143
+ { tag: 1, name: "issuerUniqueID", schema: schema.any() },
144
+ { tag: 2, name: "subjectUniqueID", schema: schema.any() },
145
+ { tag: 3, name: "extensions", schema: EXTENSIONS, explicit: true, emptyCode: "x509/bad-extensions" },
146
+ ], { minTag: 1, maxTag: 3, unexpectedCode: "x509/bad-tbs", orderCode: "x509/bad-tbs" }),
147
+ ], { assert: "constructed", code: "x509/bad-tbs", what: "tbsCertificate" });
148
+
149
+ // Certificate ::= SEQUENCE { tbsCertificate, signatureAlgorithm, signatureValue
150
+ // BIT STRING }. The build runs the RFC 5280 cross-field checks (§4.1.1.2 sig-alg
151
+ // agreement, §4.1.2.4 non-empty issuer, §4.1.2.9 extensions-only-in-v3) after the
152
+ // structural walk, then assembles the public parse result. Raw-byte accessors
153
+ // (serialNumberHex, tbsBytes, sig-alg agreement) read off the match-tree nodes.
154
+ var CERTIFICATE = schema.seq([
155
+ schema.field("tbsCertificate", CERTIFICATE_TBS),
156
+ schema.field("signatureAlgorithm", ALGORITHM_IDENTIFIER),
157
+ schema.field("signatureValue", schema.bitString()),
158
+ ], {
159
+ assert: "sequence", arity: { exact: 3 }, code: "x509/not-a-certificate", what: "Certificate",
160
+ build: function (m, ctx) {
161
+ var tbs = m.fields.tbsCertificate.value; // CERTIFICATE_TBS seq-match (no build)
162
+
163
+ // RFC 5280 §4.1.1.2 — outer signatureAlgorithm MUST equal tbsCertificate.signature.
164
+ if (!m.fields.signatureAlgorithm.node.bytes.equals(tbs.fields.signature.node.bytes)) {
165
+ throw ctx.E("x509/bad-signature-algorithm", "signatureAlgorithm must match tbsCertificate.signature (RFC 5280 §4.1.1.2)");
166
+ }
167
+
168
+ var version = tbs.fields.version.value; // 1 (default) | 2 | 3
169
+ var issuer = tbs.fields.issuer.value.result;
170
+ // RFC 5280 §4.1.2.4 — issuer MUST be non-empty (an empty subject is permitted, §4.1.2.6).
171
+ if (!issuer.rdns.length) {
172
+ throw ctx.E("x509/bad-issuer", "issuer must be a non-empty distinguished name");
173
+ }
174
+
175
+ var extField = tbs.fields.extensions;
176
+ var hasExtensions = !!(extField && extField.present);
177
+ // RFC 5280 §4.1.2.9 — extensions appear only in a v3 certificate.
178
+ if (hasExtensions && version !== 3) {
179
+ throw ctx.E("x509/bad-version", "extensions are only permitted in a v3 certificate");
180
+ }
181
+
182
+ var serialNode = tbs.fields.serialNumber.node;
183
+ var sigBits = m.fields.signatureValue.value; // { unusedBits, bytes }
184
+ return {
185
+ version: version,
186
+ serialNumber: tbs.fields.serialNumber.value,
187
+ serialNumberHex: serialNode.content.toString("hex"),
188
+ signatureAlgorithm: m.fields.signatureAlgorithm.value.result,
189
+ tbsSignatureAlgorithm: tbs.fields.signature.value.result,
190
+ issuer: issuer,
191
+ subject: tbs.fields.subject.value.result,
192
+ validity: tbs.fields.validity.value.result,
193
+ subjectPublicKeyInfo: tbs.fields.subjectPublicKeyInfo.value.result,
194
+ extensions: hasExtensions ? extField.value.result : [],
195
+ tbsBytes: tbs.node.bytes,
196
+ signatureValue: { unusedBits: sigBits.unusedBits, bytes: sigBits.bytes },
197
+ };
198
+ },
199
+ });
200
+
201
+ // ---- parse -----------------------------------------------------------
202
+
203
+ /**
204
+ * @primitive pki.schema.x509.parse
205
+ * @signature pki.schema.x509.parse(input) -> certificate
206
+ * @since 0.1.7
207
+ * @status stable
208
+ * @spec RFC 5280, X.509
209
+ * @defends malformed-certificate-parse (CWE-20)
210
+ * @related pki.asn1.decode, pki.oid.name
211
+ *
212
+ * Parse a DER `Buffer` or a PEM string/Buffer into a structured
213
+ * certificate: `{ version, serialNumber, serialNumberHex,
214
+ * signatureAlgorithm, issuer, subject, validity, subjectPublicKeyInfo,
215
+ * extensions, tbsBytes, signatureValue }`. Distinguished names come back
216
+ * both as a rendered `dn` string and as structured `rdns`; the validity
217
+ * window is real `Date`s; `tbsBytes` is the exact signed byte range for a
218
+ * downstream verifier.
219
+ *
220
+ * Throws `CertificateError` when the bytes are not a well-formed
221
+ * certificate and `Asn1Error` when the underlying DER is malformed.
222
+ *
223
+ * @example
224
+ * var cert = pki.schema.x509.parse(pemString);
225
+ * cert.subject.dn; // "CN=example.com, O=Example"
226
+ * cert.validity.notAfter; // Date
227
+ * cert.signatureAlgorithm.name; // "sha256WithRSAEncryption"
228
+ */
229
+ function parse(input) {
230
+ return pkix.runParse(input, { pemLabel: "CERTIFICATE", PemError: PemError, ErrorClass: CertificateError, prefix: "x509", what: "certificate", topSchema: CERTIFICATE, ns: NS });
231
+ }
232
+
233
+ // matches(root): does the decoded DER look like a Certificate? A CSR and a CRL
234
+ // share the outer SEQUENCE-of-3 envelope, so the discriminator is inside the
235
+ // tbs: a certificate has a Validity — a SEQUENCE of exactly two Time values — at
236
+ // position [serial, signature, issuer, VALIDITY] after the optional [0] version.
237
+ // A CSR's certificationRequestInfo has subjectPublicKeyInfo there (no Validity),
238
+ // and a CRL leads with a bare Time; neither matches. Used by the orchestrator.
239
+ function matches(root) {
240
+ var TAGS = asn1.TAGS;
241
+ if (!root || root.tagClass !== "universal" || root.tagNumber !== TAGS.SEQUENCE) return false;
242
+ if (!root.children || root.children.length !== 3) return false;
243
+ var tbs = root.children[0];
244
+ if (!tbs.children || tbs.tagClass !== "universal" || tbs.tagNumber !== TAGS.SEQUENCE) return false;
245
+ var kids = tbs.children;
246
+ var i = (kids[0] && kids[0].tagClass === "context" && kids[0].tagNumber === 0) ? 1 : 0; // optional [0] version
247
+ var validity = kids[i + 3]; // serial(i), signature(i+1), issuer(i+2), validity(i+3)
248
+ return !!validity && validity.tagClass === "universal" && validity.tagNumber === TAGS.SEQUENCE &&
249
+ !!validity.children && validity.children.length === 2 &&
250
+ validity.children.every(function (t) {
251
+ return t.tagClass === "universal" && (t.tagNumber === TAGS.UTC_TIME || t.tagNumber === TAGS.GENERALIZED_TIME);
252
+ });
253
+ }
254
+
255
+ module.exports = {
256
+ parse: parse,
257
+ pemDecode: pemDecode,
258
+ pemEncode: pemEncode,
259
+ matches: matches,
260
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
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:76df9bb3-81f7-4e82-8639-fd06f6b37a59",
5
+ "serialNumber": "urn:uuid:a50e8ea1-a357-4e08-b473-56bcf5e2659f",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-04T20:32:20.563Z",
8
+ "timestamp": "2026-07-05T03:52:05.348Z",
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.5",
22
+ "bom-ref": "@blamejs/pki@0.1.7",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.1.5",
25
+ "version": "0.1.7",
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.5",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.1.7",
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.5",
57
+ "ref": "@blamejs/pki@0.1.7",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]