@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/CHANGELOG.md +41 -0
- package/README.md +13 -9
- package/bin/pki.js +2 -1
- package/index.js +8 -7
- package/lib/asn1-der.js +29 -11
- package/lib/constants.js +1 -1
- package/lib/framework-error.js +15 -0
- package/lib/oid.js +8 -3
- package/lib/schema-all.js +139 -0
- package/lib/schema-crl.js +251 -0
- package/lib/schema-csr.js +203 -0
- package/lib/{asn1-schema.js → schema-engine.js} +49 -15
- package/lib/schema-pkix.js +320 -0
- package/lib/schema-x509.js +243 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
- package/lib/x509.js +0 -393
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,47 @@ All notable changes to `@blamejs/pki` are documented here. The format
|
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
|
|
5
5
|
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## v0.1.8 — 2026-07-05
|
|
8
|
+
|
|
9
|
+
A PKCS#10 certification-request parser joins the pki.schema family.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- pki.schema.csr.parse — a PKCS#10 CertificationRequest parser per RFC 2986. It turns a DER Buffer or a 'CERTIFICATE REQUEST' PEM string into a structured object: version, subject distinguished name, subjectPublicKeyInfo, the requested attributes (each with its type OID, resolved name, and raw-DER values), and the signatureAlgorithm / signatureValue over the CertificationRequestInfo — with the raw certificationRequestInfoBytes returned for signature verification. It composes the shared schema engine and PKIX sub-schemas (AlgorithmIdentifier, Name, SubjectPublicKeyInfo), so a certification request inherits the identical fail-closed structural rules and a malformed request throws a typed CsrError (csr/*); a leaf-level codec fault surfaces as asn1/*. The version must be v1 (INTEGER 0), the [0] IMPLICIT attributes element is mandatory, and each attribute's values SET must be non-empty. pki.schema.parse now detects and routes certification requests, and pki.schema.all() lists it alongside crl and x509. pki.schema.csr.pemDecode / pemEncode handle the PEM envelope.
|
|
14
|
+
- pki.asn1.read.enumerated — reads an ENUMERATED value from a decoded node (the same content rules as an INTEGER), the counterpart to the now-strict pki.asn1.read.integer.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- C.TIME.ms is renamed to C.TIME.milliseconds, so every C.TIME duration helper now reads as a full word (milliseconds, seconds, minutes, hours, days, weeks). The behaviour is unchanged — it still returns an integer millisecond count.
|
|
19
|
+
|
|
20
|
+
### Security
|
|
21
|
+
|
|
22
|
+
- pki.asn1.read.integer now rejects an ENUMERATED-tagged node. INTEGER and ENUMERATED share DER content encoding, so an INTEGER-pinned field — a certificate or certification-request version, a serial number, or a cRLNumber — mis-encoded as ENUMERATED was previously decoded as though it were the INTEGER, a type confusion that let malformed DER parse where a conformant reader rejects it. read.integer is now strict on the tag, and ENUMERATED values are read with the new pki.asn1.read.enumerated. Certificate, CRL, and certification-request parsing reject these inputs fail-closed.
|
|
23
|
+
- SubjectPublicKeyInfo is now required to be a universal SEQUENCE across the certificate and certification-request parsers — a context-tagged or SET-tagged constructed node carrying a well-formed algorithm and key is no longer accepted as an SPKI.
|
|
24
|
+
- SET OF components are now required to be in ascending DER order (X.690 §11.6) wherever the schema declares a SET OF — a relative distinguished name, and a certification request's attributes and attribute values. A non-canonical, unsorted encoding is rejected fail-closed.
|
|
25
|
+
|
|
26
|
+
### Migration
|
|
27
|
+
|
|
28
|
+
- Replace C.TIME.ms(n) with C.TIME.milliseconds(n). The other C.TIME and C.BYTES helpers are unchanged.
|
|
29
|
+
|
|
30
|
+
## v0.1.7 — 2026-07-04
|
|
31
|
+
|
|
32
|
+
A unified pki.schema family: the structure-schema engine, the X.509 parser, a new CRL parser, and a detect-and-route orchestrator.
|
|
33
|
+
|
|
34
|
+
### Added
|
|
35
|
+
|
|
36
|
+
- pki.schema.crl.parse — an X.509 CRL (CertificateList) parser per RFC 5280 §5. It turns a DER Buffer or an 'X509 CRL' PEM string into a structured object: version, issuer distinguished name, thisUpdate / nextUpdate as real Dates, the ordered list of revoked certificates (serial number + hex + revocation date + entry extensions), and the CRL extensions — with the cRLNumber, reasonCode, and invalidityDate values decoded and the raw tbsCertList bytes returned for signature verification. It composes the same schema engine and shared PKIX sub-schemas (AlgorithmIdentifier, Name, Extension) as the certificate parser, so the CertificateList inherits the identical fail-closed structural rules (bounds-checked positional reads, the signature-algorithm agreement, non-empty issuer, extension uniqueness, the v2-only version rule).
|
|
37
|
+
- pki.schema.parse — a detect-and-route entry point: hand it DER or PEM and it identifies which registered PKI format the bytes encode (certificate vs CRL) and routes to that member's parser. pki.schema.all() enumerates the registered formats.
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- The schema engine and the per-format parsers are reorganized under one pki.schema namespace. pki.x509.parse is now pki.schema.x509.parse (and .pemDecode / .pemEncode likewise), and the structure-schema engine pki.asn1.schema is now pki.schema.engine. pki.asn1 remains the strict DER codec (decode / encode / build / read / TAGS). This is a breaking rename with no compatibility shim; see MIGRATING. The schema engine also gained a universal-tag optional-field recognizer, which the CRL's bare version / nextUpdate / revokedCertificates fields require.
|
|
42
|
+
|
|
43
|
+
### Migration
|
|
44
|
+
|
|
45
|
+
- Replace pki.x509.parse(...) with pki.schema.x509.parse(...); pki.x509.pemDecode / pemEncode become pki.schema.x509.pemDecode / pemEncode.
|
|
46
|
+
- Replace pki.asn1.schema (the structure-schema engine) with pki.schema.engine. pki.asn1 is unchanged for the DER codec (pki.asn1.decode / encode / build / read / TAGS).
|
|
47
|
+
|
|
7
48
|
## v0.1.6 — 2026-07-04
|
|
8
49
|
|
|
9
50
|
A declarative ASN.1 structure-schema engine; the X.509 parser is rebuilt on it.
|
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ var pki = require("@blamejs/pki");
|
|
|
63
63
|
|
|
64
64
|
### Parse an X.509 certificate
|
|
65
65
|
|
|
66
|
-
`pki.x509.parse` accepts a DER `Buffer` or a PEM string/Buffer and returns a
|
|
66
|
+
`pki.schema.x509.parse` accepts a DER `Buffer` or a PEM string/Buffer and returns a
|
|
67
67
|
fully-decoded, validated certificate — distinguished names rendered and
|
|
68
68
|
structured, the validity window as real `Date`s, algorithms and extensions
|
|
69
69
|
named through the OID registry, and the exact signed `tbsBytes` for a downstream
|
|
@@ -74,7 +74,7 @@ var pki = require("@blamejs/pki");
|
|
|
74
74
|
var fs = require("node:fs");
|
|
75
75
|
|
|
76
76
|
var pem = fs.readFileSync("cert.pem", "utf8");
|
|
77
|
-
var cert = pki.x509.parse(pem);
|
|
77
|
+
var cert = pki.schema.x509.parse(pem);
|
|
78
78
|
|
|
79
79
|
cert.subject.dn; // "CN=example.com, O=Example Org, C=US"
|
|
80
80
|
cert.issuer.dn; // "CN=example.com, O=Example Org, C=US"
|
|
@@ -94,7 +94,7 @@ Malformed bytes throw a typed error rather than returning a half-parsed object:
|
|
|
94
94
|
|
|
95
95
|
```js
|
|
96
96
|
try {
|
|
97
|
-
pki.x509.parse(Buffer.from([0x30, 0x03, 0x02, 0x01, 0x00]));
|
|
97
|
+
pki.schema.x509.parse(Buffer.from([0x30, 0x03, 0x02, 0x01, 0x00]));
|
|
98
98
|
} catch (e) {
|
|
99
99
|
e.constructor.name; // "CertificateError"
|
|
100
100
|
e.code; // e.g. "x509/not-a-certificate" — stable domain/reason string
|
|
@@ -104,8 +104,8 @@ try {
|
|
|
104
104
|
### Convert PEM ↔ DER
|
|
105
105
|
|
|
106
106
|
```js
|
|
107
|
-
var der = pki.x509.pemDecode(pem, "CERTIFICATE"); // Buffer of DER bytes
|
|
108
|
-
var out = pki.x509.pemEncode(der, "CERTIFICATE"); // 64-column PEM string
|
|
107
|
+
var der = pki.schema.x509.pemDecode(pem, "CERTIFICATE"); // Buffer of DER bytes
|
|
108
|
+
var out = pki.schema.x509.pemEncode(der, "CERTIFICATE"); // 64-column PEM string
|
|
109
109
|
```
|
|
110
110
|
|
|
111
111
|
### Decode and build ASN.1 / DER directly
|
|
@@ -180,7 +180,7 @@ var ok = await subtle.verify({ name: "ML-DSA-65" }, kp.publicKey, sig, data); /
|
|
|
180
180
|
// same call shape, and every key it exports is OpenSSL/NSS-interoperable.
|
|
181
181
|
```
|
|
182
182
|
|
|
183
|
-
## What ships today
|
|
183
|
+
## What ships today
|
|
184
184
|
|
|
185
185
|
The core codec and certificate-reading surface are here and stable. Everything
|
|
186
186
|
is callable today; nothing below is a stub.
|
|
@@ -190,9 +190,13 @@ is callable today; nothing below is a stub.
|
|
|
190
190
|
| `pki.asn1` | Strict, bounded DER codec — `decode` (zero-copy node tree), `encode`, `build.*` canonical-DER value builders, `read.*` typed readers, `TAGS`, OID-content encode/decode |
|
|
191
191
|
| `pki.oid` | Two-way OID ↔ name registry — `name`, `byName`, `register`, `toArcs`/`fromArcs`, `toDER`/`fromDER`; seeded with RFC 5280 + NIST PQC arcs |
|
|
192
192
|
| `pki.webcrypto` | A W3C WebCrypto (`SubtleCrypto`) engine over `node:crypto` — `sign`/`verify`/`encrypt`/`decrypt`/`deriveBits`/`digest`/`generateKey`/`importKey`/`exportKey` across RSA, ECDSA, ECDH, Ed25519/Ed448, AES, HMAC, HKDF, PBKDF2, SHA — **and** post-quantum ML-DSA-44/65/87 and SLH-DSA signatures, plus ML-KEM-512/768/1024 key generation (KEM encapsulation on the roadmap). Zero-dependency, OpenSSL-interoperable |
|
|
193
|
-
| `pki.
|
|
193
|
+
| `pki.schema` | The schema family — `parse` detects which PKI format DER / PEM encodes and routes to the right parser, `all` enumerates the registered formats, and the engine + per-format members are grouped here |
|
|
194
|
+
| `pki.schema.x509` | Parse DER / PEM certificates into structured, validated fields — `parse`, `pemDecode`, `pemEncode` |
|
|
195
|
+
| `pki.schema.crl` | Parse DER / PEM X.509 CRLs per RFC 5280 §5 — revoked serials with real-`Date` revocation times, named + partly-decoded extensions, fail-closed — `parse`, `pemDecode` |
|
|
196
|
+
| `pki.schema.csr` | Parse DER / PEM PKCS#10 certification requests per RFC 2986 — subject DN, public key, requested attributes, signature, fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
|
197
|
+
| `pki.schema.engine` | The declarative ASN.1 structure-schema engine every format parser composes — `walk` plus the schema combinators |
|
|
194
198
|
| `pki.C` / `pki.constants` | Version-stable constants — functional scale helpers (`C.TIME.*`, `C.BYTES.*`), codec `LIMITS`, `version` |
|
|
195
|
-
| `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError` / `Asn1Error` / `OidError` / `PemError` / `CertificateError`, each carrying a stable `code` in `domain/reason` form |
|
|
199
|
+
| `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError` / `Asn1Error` / `OidError` / `PemError` / `CertificateError` / `CrlError` / `CsrError`, each carrying a stable `code` in `domain/reason` form |
|
|
196
200
|
| `pki` CLI | `pki version`, `pki oid <dotted\|name>`, `pki parse <cert>` |
|
|
197
201
|
|
|
198
202
|
### CLI
|
|
@@ -206,7 +210,7 @@ pki parse cert.pem # structured JSON summary of a certific
|
|
|
206
210
|
|
|
207
211
|
### What's coming
|
|
208
212
|
|
|
209
|
-
Certificate build/sign/verify,
|
|
213
|
+
Certificate build/sign/verify, certification-path
|
|
210
214
|
validation, CMS (SignedData / EnvelopedData / EncryptedData / AuthenticatedData),
|
|
211
215
|
OCSP, RFC 3161 timestamping, PKCS#8 / SPKI / PBES2 / PKCS#12, and the
|
|
212
216
|
post-quantum certificate and CMS surface (ML-DSA / ML-KEM / SLH-DSA and hybrid
|
package/bin/pki.js
CHANGED
|
@@ -44,7 +44,7 @@ function cmdParse(file) {
|
|
|
44
44
|
var bytes;
|
|
45
45
|
try { bytes = fs.readFileSync(file); } catch (e) { return fail("cannot read " + file + ": " + e.message); }
|
|
46
46
|
var cert;
|
|
47
|
-
try { cert = pki.x509.parse(bytes); } catch (e) { return fail(e.code + ": " + e.message); }
|
|
47
|
+
try { cert = pki.schema.x509.parse(bytes); } catch (e) { return fail(e.code + ": " + e.message); }
|
|
48
48
|
var view = {
|
|
49
49
|
version: cert.version,
|
|
50
50
|
serialNumber: cert.serialNumberHex,
|
|
@@ -74,3 +74,4 @@ function main(argv) {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
main(process.argv.slice(2));
|
|
77
|
+
|
package/index.js
CHANGED
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
* (PkiError taxonomy), asn1 (strict DER codec), oid
|
|
14
14
|
* (OID ↔ name registry), webcrypto (W3C SubtleCrypto
|
|
15
15
|
* engine over node:crypto — PQC-first, classical-capable)
|
|
16
|
-
*
|
|
16
|
+
* Schema: schema (the structure-schema engine + per-format parsers:
|
|
17
|
+
* schema.engine, schema.x509, schema.parse detect-and-route)
|
|
17
18
|
*
|
|
18
19
|
* The surface grows per ROADMAP.md — CMS, OCSP, CRL, CSR, PKCS#8/#12,
|
|
19
20
|
* timestamping, path validation, and the post-quantum algorithm set are
|
|
@@ -25,10 +26,9 @@
|
|
|
25
26
|
var constants = require("./lib/constants");
|
|
26
27
|
var errors = require("./lib/framework-error");
|
|
27
28
|
var asn1 = require("./lib/asn1-der");
|
|
28
|
-
var asn1Schema = require("./lib/asn1-schema");
|
|
29
29
|
var oid = require("./lib/oid");
|
|
30
30
|
var webcrypto = require("./lib/webcrypto");
|
|
31
|
-
var
|
|
31
|
+
var schema = require("./lib/schema-all");
|
|
32
32
|
|
|
33
33
|
module.exports = {
|
|
34
34
|
version: constants.version,
|
|
@@ -36,11 +36,12 @@ module.exports = {
|
|
|
36
36
|
C: constants,
|
|
37
37
|
constants: constants,
|
|
38
38
|
errors: errors,
|
|
39
|
-
// `asn1
|
|
40
|
-
|
|
41
|
-
asn1: Object.assign({}, asn1, { schema: asn1Schema }),
|
|
39
|
+
// `asn1` is the strict DER codec (decode/encode/build/read/TAGS).
|
|
40
|
+
asn1: asn1,
|
|
42
41
|
oid: oid,
|
|
43
|
-
|
|
42
|
+
// `schema` is the family: the L2 structure-schema engine (schema.engine) and
|
|
43
|
+
// the per-format parsers (schema.x509, …) with detect-and-route schema.parse.
|
|
44
|
+
schema: schema,
|
|
44
45
|
// A ready W3C Crypto instance (globalThis.crypto shape) + the classes
|
|
45
46
|
// for constructing more. PQC-first, classical-capable, zero-dep.
|
|
46
47
|
webcrypto: webcrypto.webcrypto,
|
package/lib/asn1-der.js
CHANGED
|
@@ -273,15 +273,15 @@ function readBoolean(node) {
|
|
|
273
273
|
throw new Asn1Error("asn1/bad-boolean", "DER BOOLEAN must be 0x00 or 0xFF, got 0x" + v.toString(16));
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
_expectPrimitive(node,
|
|
276
|
+
// INTEGER and ENUMERATED share DER content encoding (a two's-complement big-endian
|
|
277
|
+
// value) but are DISTINCT universal types. This decodes that shared content — the
|
|
278
|
+
// caller asserts the tag first. `typeName` names the type in error messages; the
|
|
279
|
+
// stable error codes stay the integer family (a caller switching on `code` treats
|
|
280
|
+
// both alike since the encoding is identical).
|
|
281
|
+
function _readIntegerLikeContent(node, typeName, who) {
|
|
282
|
+
_expectPrimitive(node, who);
|
|
283
283
|
var c = node.content;
|
|
284
|
-
if (c.length === 0) throw new Asn1Error("asn1/bad-integer", "
|
|
284
|
+
if (c.length === 0) throw new Asn1Error("asn1/bad-integer", typeName + " must have at least 1 content octet");
|
|
285
285
|
// Defense-in-depth: refuse an over-cap magnitude before touching BigInt.
|
|
286
286
|
// A one-shot hex parse is linear, but the length still bounds worst-case
|
|
287
287
|
// work — reject a hostile length prefix up front. The cap bounds the
|
|
@@ -290,17 +290,34 @@ function readInteger(node) {
|
|
|
290
290
|
// modulus is 16384 magnitude bytes plus that sign pad).
|
|
291
291
|
if (c.length > constants.LIMITS.DER_MAX_INTEGER_BYTES + 1) {
|
|
292
292
|
throw new Asn1Error("asn1/integer-too-large",
|
|
293
|
-
"
|
|
293
|
+
typeName + " content " + c.length + " bytes exceeds cap " + (constants.LIMITS.DER_MAX_INTEGER_BYTES + 1));
|
|
294
294
|
}
|
|
295
295
|
if (c.length > 1) {
|
|
296
|
-
if (c[0] === 0x00 && (c[1] & 0x80) === 0) throw new Asn1Error("asn1/non-minimal-integer", "non-minimal positive
|
|
297
|
-
if (c[0] === 0xff && (c[1] & 0x80) !== 0) throw new Asn1Error("asn1/non-minimal-integer", "non-minimal negative
|
|
296
|
+
if (c[0] === 0x00 && (c[1] & 0x80) === 0) throw new Asn1Error("asn1/non-minimal-integer", "non-minimal positive " + typeName);
|
|
297
|
+
if (c[0] === 0xff && (c[1] & 0x80) !== 0) throw new Asn1Error("asn1/non-minimal-integer", "non-minimal negative " + typeName);
|
|
298
298
|
}
|
|
299
299
|
var neg = (c[0] & 0x80) !== 0;
|
|
300
300
|
var mag = c.length ? BigInt("0x" + Buffer.from(c).toString("hex")) : 0n;
|
|
301
301
|
return neg ? mag - (1n << BigInt(c.length * 8)) : mag;
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
+
// read.integer is STRICT on the tag: an ENUMERATED node (which shares INTEGER's
|
|
305
|
+
// content encoding) is NOT accepted here. Coercing an ENUMERATED to an INTEGER is a
|
|
306
|
+
// type confusion — an INTEGER-pinned field (a certificate/CSR version, a serial
|
|
307
|
+
// number, a cRLNumber) encoded as ENUMERATED would slip past a lenient reader.
|
|
308
|
+
// Read ENUMERATED values with read.enumerated.
|
|
309
|
+
function readInteger(node) {
|
|
310
|
+
_expectUniversal(node, TAGS.INTEGER, "readInteger");
|
|
311
|
+
return _readIntegerLikeContent(node, "INTEGER", "readInteger");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// read.enumerated reads an ENUMERATED value; its content rules are identical to
|
|
315
|
+
// INTEGER (non-empty, minimally encoded, magnitude within the cap).
|
|
316
|
+
function readEnumerated(node) {
|
|
317
|
+
_expectUniversal(node, TAGS.ENUMERATED, "readEnumerated");
|
|
318
|
+
return _readIntegerLikeContent(node, "ENUMERATED", "readEnumerated");
|
|
319
|
+
}
|
|
320
|
+
|
|
304
321
|
function readBitString(node) {
|
|
305
322
|
_expectUniversal(node, TAGS.BIT_STRING, "readBitString");
|
|
306
323
|
_expectPrimitive(node, "readBitString");
|
|
@@ -754,6 +771,7 @@ module.exports = {
|
|
|
754
771
|
read: {
|
|
755
772
|
boolean: readBoolean,
|
|
756
773
|
integer: readInteger,
|
|
774
|
+
enumerated: readEnumerated,
|
|
757
775
|
bitString: readBitString,
|
|
758
776
|
octetString: readOctetString,
|
|
759
777
|
nullValue: readNull,
|
package/lib/constants.js
CHANGED
|
@@ -64,7 +64,7 @@ var DAYS_PER_WEEK = 7;
|
|
|
64
64
|
* // -> 31536000000
|
|
65
65
|
*/
|
|
66
66
|
var TIME = {
|
|
67
|
-
|
|
67
|
+
milliseconds: function (n) { return Math.round(_positive(n, "C.TIME.milliseconds")); },
|
|
68
68
|
seconds: function (n) { return Math.round(_positive(n, "C.TIME.seconds") * MS_PER_SECOND); },
|
|
69
69
|
minutes: function (n) { return Math.round(_positive(n, "C.TIME.minutes") * SECONDS_PER_MINUTE * MS_PER_SECOND); },
|
|
70
70
|
hours: function (n) { return Math.round(_positive(n, "C.TIME.hours") * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MS_PER_SECOND); },
|
package/lib/framework-error.js
CHANGED
|
@@ -121,6 +121,18 @@ var PemError = defineClass("PemError");
|
|
|
121
121
|
// version).
|
|
122
122
|
var CertificateError = defineClass("CertificateError", { withCause: true });
|
|
123
123
|
|
|
124
|
+
// CrlError — a byte sequence that is not a well-formed X.509 CRL
|
|
125
|
+
// (CertificateList / TBSCertList — RFC 5280 §5).
|
|
126
|
+
var CrlError = defineClass("CrlError", { withCause: true });
|
|
127
|
+
|
|
128
|
+
// SchemaError — the schema-family orchestrator's own errors (input that matches
|
|
129
|
+
// no registered format / does not decode). (code, message) shape like the rest.
|
|
130
|
+
var SchemaError = defineClass("SchemaError", { withCause: true });
|
|
131
|
+
|
|
132
|
+
// CsrError — a byte sequence that is not a well-formed PKCS#10 CertificationRequest
|
|
133
|
+
// (RFC 2986).
|
|
134
|
+
var CsrError = defineClass("CsrError", { withCause: true });
|
|
135
|
+
|
|
124
136
|
module.exports = {
|
|
125
137
|
PkiError: PkiError,
|
|
126
138
|
defineClass: defineClass,
|
|
@@ -129,4 +141,7 @@ module.exports = {
|
|
|
129
141
|
OidError: OidError,
|
|
130
142
|
PemError: PemError,
|
|
131
143
|
CertificateError: CertificateError,
|
|
144
|
+
CrlError: CrlError,
|
|
145
|
+
SchemaError: SchemaError,
|
|
146
|
+
CsrError: CsrError,
|
|
132
147
|
};
|
package/lib/oid.js
CHANGED
|
@@ -54,8 +54,13 @@ var FAMILIES = {
|
|
|
54
54
|
// RFC 5280 certificate extensions.
|
|
55
55
|
certExtension: { base: [2, 5, 29], of: {
|
|
56
56
|
subjectKeyIdentifier: 14, keyUsage: 15, subjectAltName: 17, issuerAltName: 18,
|
|
57
|
-
basicConstraints: 19,
|
|
58
|
-
|
|
57
|
+
basicConstraints: 19,
|
|
58
|
+
// CRL + CRL-entry extensions (RFC 5280 §5.2, §5.3)
|
|
59
|
+
cRLNumber: 20, reasonCode: 21, invalidityDate: 24, deltaCRLIndicator: 27,
|
|
60
|
+
issuingDistributionPoint: 28, certificateIssuer: 29,
|
|
61
|
+
nameConstraints: 30, cRLDistributionPoints: 31,
|
|
62
|
+
certificatePolicies: 32, authorityKeyIdentifier: 35, extKeyUsage: 37,
|
|
63
|
+
freshestCRL: 46 } },
|
|
59
64
|
|
|
60
65
|
// PKIX private extensions (authorityInfoAccess et al).
|
|
61
66
|
pkixAccess: { base: [1, 3, 6, 1, 5, 5, 7, 1], of: { authorityInfoAccess: 1 } },
|
|
@@ -66,7 +71,7 @@ var FAMILIES = {
|
|
|
66
71
|
sha384WithRSAEncryption: 12, sha512WithRSAEncryption: 13 } },
|
|
67
72
|
|
|
68
73
|
// PKCS#9 attribute types.
|
|
69
|
-
pkcs9: { base: [1, 2, 840, 113549, 1, 9], of: { emailAddress: 1 } },
|
|
74
|
+
pkcs9: { base: [1, 2, 840, 113549, 1, 9], of: { emailAddress: 1, challengePassword: 7, extensionRequest: 14 } },
|
|
70
75
|
|
|
71
76
|
// ANSI X9.62 EC public key, named curve, and ECDSA signatures.
|
|
72
77
|
ansiX962: { base: [1, 2, 840, 10045], of: {
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
/**
|
|
5
|
+
* @module pki.schema
|
|
6
|
+
* @nav Schema
|
|
7
|
+
* @title Schema
|
|
8
|
+
* @order 10
|
|
9
|
+
* @featured true
|
|
10
|
+
* @slug schema
|
|
11
|
+
*
|
|
12
|
+
* @intro
|
|
13
|
+
* The schema family: a declarative ASN.1 structure-schema engine and the
|
|
14
|
+
* per-format parsers built on it. Every format (X.509 certificates, CRLs,
|
|
15
|
+
* PKCS#10 certification requests, and — as they land — CMS / PKCS#8) is a
|
|
16
|
+
* member that COMPOSES the
|
|
17
|
+
* shared engine and the shared PKIX sub-schemas (AlgorithmIdentifier, Name,
|
|
18
|
+
* Extension), so a structural rule — bounds-checked positional reads,
|
|
19
|
+
* optional / tagged field ordering, SET-OF uniqueness, fail-closed typed
|
|
20
|
+
* errors — is defined once in the engine and no format can reintroduce the
|
|
21
|
+
* class of bug it prevents.
|
|
22
|
+
*
|
|
23
|
+
* `parse` is the orchestrator: hand it DER (or PEM) and it detects which
|
|
24
|
+
* format the bytes encode and routes to that member's parser. Each member is
|
|
25
|
+
* also reachable directly (`pki.schema.x509.parse`, `pki.schema.crl.parse`),
|
|
26
|
+
* and `all()` enumerates the registered formats.
|
|
27
|
+
*
|
|
28
|
+
* @card
|
|
29
|
+
* One declarative schema engine; every PKI format (X.509, CRL, …) is a
|
|
30
|
+
* member composed on it. Detect-and-parse DER, or call a format directly.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
var engine = require("./schema-engine");
|
|
34
|
+
var pkix = require("./schema-pkix");
|
|
35
|
+
var x509 = require("./schema-x509");
|
|
36
|
+
var crl = require("./schema-crl");
|
|
37
|
+
var csr = require("./schema-csr");
|
|
38
|
+
var frameworkError = require("./framework-error");
|
|
39
|
+
|
|
40
|
+
var SchemaError = frameworkError.SchemaError;
|
|
41
|
+
var PemError = frameworkError.PemError;
|
|
42
|
+
|
|
43
|
+
// The shared parse-entry opts for the orchestrator: label-agnostic PEM unwrap
|
|
44
|
+
// (any block type routes), the schema/* error family. Detection needs DER, so
|
|
45
|
+
// the coerced DER is what gets routed to the matched format.
|
|
46
|
+
var ENTRY = { pemLabel: null, PemError: PemError, ErrorClass: SchemaError, prefix: "schema", what: "input" };
|
|
47
|
+
|
|
48
|
+
// REGISTRY — each format is declarative data: a name, a `detect(root)` that
|
|
49
|
+
// recognizes the decoded DER root as this format's outer structure, and the
|
|
50
|
+
// member's own `parse`. Adding a format is a table entry, not new dispatch
|
|
51
|
+
// logic. Order matters: the most specific detector wins, so a new member is
|
|
52
|
+
// inserted ahead of any more-permissive one.
|
|
53
|
+
var FORMATS = [
|
|
54
|
+
{
|
|
55
|
+
// CertificationRequest ::= SEQUENCE { certificationRequestInfo,
|
|
56
|
+
// signatureAlgorithm, signature } — the same outer 3-element shape,
|
|
57
|
+
// distinguished by a CertificationRequestInfo of EXACTLY four children
|
|
58
|
+
// ending in the IMPLICIT [0] attributes element. Checked first because that
|
|
59
|
+
// detector is the most specific and mutually exclusive with the others.
|
|
60
|
+
name: "csr",
|
|
61
|
+
module: csr,
|
|
62
|
+
detect: csr.matches,
|
|
63
|
+
parse: function (input) { return csr.parse(input); },
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
// CertificateList ::= SEQUENCE { tbsCertList, signatureAlgorithm,
|
|
67
|
+
// signatureValue } — the same outer shape as a certificate, distinguished
|
|
68
|
+
// by its tbsCertList (a bare Time at the certificate's Validity position).
|
|
69
|
+
name: "crl",
|
|
70
|
+
module: crl,
|
|
71
|
+
detect: crl.matches,
|
|
72
|
+
parse: function (input) { return crl.parse(input); },
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
name: "x509",
|
|
76
|
+
module: x509,
|
|
77
|
+
// Certificate — identified by a Validity (SEQUENCE of two Times) inside the
|
|
78
|
+
// tbs, so a CSR / other 3-element signed envelope is NOT misclassified as a
|
|
79
|
+
// certificate (it falls through to schema/unknown-format).
|
|
80
|
+
detect: x509.matches,
|
|
81
|
+
parse: function (input) { return x509.parse(input); },
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @primitive pki.schema.all
|
|
87
|
+
* @signature pki.schema.all() -> string[]
|
|
88
|
+
* @since 0.1.7
|
|
89
|
+
* @status experimental
|
|
90
|
+
* @spec RFC 5280
|
|
91
|
+
* @related pki.schema.parse
|
|
92
|
+
*
|
|
93
|
+
* The names of every registered format, in detection order.
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* pki.schema.all(); // → ["csr", "crl", "x509"]
|
|
97
|
+
*/
|
|
98
|
+
function all() { return FORMATS.map(function (f) { return f.name; }); }
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @primitive pki.schema.parse
|
|
102
|
+
* @signature pki.schema.parse(input) -> parsed
|
|
103
|
+
* @since 0.1.7
|
|
104
|
+
* @status experimental
|
|
105
|
+
* @spec RFC 5280
|
|
106
|
+
* @related pki.schema.x509, pki.schema.all
|
|
107
|
+
*
|
|
108
|
+
* Detect which PKI format `input` (a DER `Buffer` or a PEM string) encodes and
|
|
109
|
+
* route to that format's parser, returning the same structured object the
|
|
110
|
+
* format's own `parse` returns. Throws `SchemaError("schema/unknown-format")` when
|
|
111
|
+
* the bytes match no registered format; the underlying decode / structural
|
|
112
|
+
* errors of the matched format propagate unchanged.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* var parsed = pki.schema.parse(der); // cert → the pki.schema.x509 shape
|
|
116
|
+
*/
|
|
117
|
+
function parse(input) {
|
|
118
|
+
// Coerce + decode via the shared parse-entry, then route the COERCED DER (a
|
|
119
|
+
// PEM string/Buffer is already unwrapped, so the matched format parses DER
|
|
120
|
+
// directly and never re-treats the armor as DER).
|
|
121
|
+
var der = pkix.coerceToDer(input, ENTRY);
|
|
122
|
+
var root = pkix.decodeRoot(der, ENTRY);
|
|
123
|
+
for (var i = 0; i < FORMATS.length; i++) {
|
|
124
|
+
if (FORMATS[i].detect(root)) return FORMATS[i].parse(der);
|
|
125
|
+
}
|
|
126
|
+
throw new SchemaError("schema/unknown-format", "input does not match any registered PKI format (" + all().join(", ") + ")");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Curated public surface: each format exposes only its operator primitives. The
|
|
130
|
+
// `matches` detector is internal dispatch infrastructure (used by FORMATS above),
|
|
131
|
+
// not an operator API, so it is NOT re-exported here.
|
|
132
|
+
module.exports = {
|
|
133
|
+
engine: engine,
|
|
134
|
+
x509: { parse: x509.parse, pemDecode: x509.pemDecode, pemEncode: x509.pemEncode },
|
|
135
|
+
crl: { parse: crl.parse, pemDecode: crl.pemDecode },
|
|
136
|
+
csr: { parse: csr.parse, pemDecode: csr.pemDecode, pemEncode: csr.pemEncode },
|
|
137
|
+
all: all,
|
|
138
|
+
parse: parse,
|
|
139
|
+
};
|