@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.
@@ -0,0 +1,130 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ /**
5
+ * @module pki.errors
6
+ * @nav Core
7
+ * @title Errors
8
+ * @order 20
9
+ * @slug errors
10
+ *
11
+ * @intro
12
+ * Error taxonomy for the toolkit. Every error the library throws
13
+ * extends `PkiError`, so a consumer needs a single
14
+ * `err instanceof pki.errors.PkiError` check instead of sniffing per-
15
+ * module boolean flags, and every error carries a stable shape:
16
+ * `{ name, code, message, permanent, isPkiError: true }`.
17
+ *
18
+ * `code` is a stable, greppable `domain/reason` string
19
+ * (`asn1/indefinite-length`, `x509/not-a-certificate`) — safe to switch
20
+ * on and safe to log. Because every failure here is a deterministic
21
+ * verdict on the bytes in hand (a malformed length, an unknown OID
22
+ * shape, a truncated certificate), errors are `permanent: true` — the
23
+ * same input will never parse on retry.
24
+ *
25
+ * @card
26
+ * `PkiError` base class + `defineClass` factory + the per-domain error
27
+ * classes the toolkit throws.
28
+ */
29
+
30
+ /**
31
+ * @primitive pki.errors.PkiError
32
+ * @signature new PkiError(message, code)
33
+ * @since 0.1.0
34
+ * @status stable
35
+ *
36
+ * Base class every toolkit error extends. Provides the unified
37
+ * `instanceof` check plus the `{ name, code, isPkiError }` shape.
38
+ *
39
+ * @example
40
+ * try { pki.asn1.decode(bytes); }
41
+ * catch (e) {
42
+ * if (e instanceof pki.errors.PkiError) console.error(e.code);
43
+ * }
44
+ */
45
+ class PkiError extends Error {
46
+ constructor(message, code) {
47
+ super(message);
48
+ this.name = "PkiError";
49
+ this.code = code || "pki/invalid";
50
+ this.isPkiError = true;
51
+ this.permanent = true;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * @primitive pki.errors.defineClass
57
+ * @signature pki.errors.defineClass(name, opts?) -> constructor
58
+ * @since 0.1.0
59
+ * @status stable
60
+ *
61
+ * Factory that produces a `PkiError` subclass with the standard shape —
62
+ * eliminating the per-domain boilerplate. The returned constructor takes
63
+ * `(code, message)`, stamps `name`, sets an `is<Name>` flag, and exposes
64
+ * a `.factory` static for the common `var _err = XxxError.factory` shape.
65
+ *
66
+ * @opts
67
+ * withCause: boolean, // default: false — constructor becomes (code, message, cause)
68
+ *
69
+ * @example
70
+ * var MyError = pki.errors.defineClass("MyError");
71
+ * throw new MyError("my/bad-input", "explanation");
72
+ */
73
+ function defineClass(name, opts) {
74
+ if (typeof name !== "string" || name.length === 0) {
75
+ throw new TypeError("defineClass: name must be a non-empty string");
76
+ }
77
+ opts = opts || {};
78
+ var withCause = !!opts.withCause;
79
+ var flagKey = "is" + name;
80
+
81
+ var GeneratedError = class extends PkiError {
82
+ constructor(code, message, arg3) {
83
+ super(message, code);
84
+ this.name = name;
85
+ this[flagKey] = true;
86
+ if (withCause && arg3 !== undefined) this.cause = arg3;
87
+ }
88
+ };
89
+ Object.defineProperty(GeneratedError, "name", { value: name, configurable: true });
90
+ GeneratedError.factory = function (code, message, arg3) {
91
+ return new GeneratedError(code, message, arg3);
92
+ };
93
+ return GeneratedError;
94
+ }
95
+
96
+ // ---- Per-domain error classes ----
97
+
98
+ // ConstantsError — a bad scale argument to C.TIME.* / C.BYTES.* (an
99
+ // authoring bug caught at config time).
100
+ var ConstantsError = defineClass("ConstantsError");
101
+
102
+ // Asn1Error — malformed / non-canonical DER: truncated TLV, indefinite
103
+ // length, non-minimal length or integer, leftover trailing bytes, depth
104
+ // or size cap exceeded. DER is a canonical encoding, so anything the
105
+ // decoder rejects is permanently invalid.
106
+ var Asn1Error = defineClass("Asn1Error");
107
+
108
+ // OidError — a malformed object-identifier: fewer than two arcs, a first
109
+ // arc outside 0..2, a second arc >= 40 under arcs 0/1, or a non-minimal
110
+ // base-128 sub-identifier.
111
+ var OidError = defineClass("OidError");
112
+
113
+ // PemError — a malformed PEM envelope: missing / mismatched BEGIN/END
114
+ // markers, a bad label, or non-base64 body.
115
+ var PemError = defineClass("PemError");
116
+
117
+ // CertificateError — a byte sequence that is not a well-formed X.509
118
+ // certificate (wrong outer structure, unparseable field, unsupported
119
+ // version).
120
+ var CertificateError = defineClass("CertificateError", { withCause: true });
121
+
122
+ module.exports = {
123
+ PkiError: PkiError,
124
+ defineClass: defineClass,
125
+ ConstantsError: ConstantsError,
126
+ Asn1Error: Asn1Error,
127
+ OidError: OidError,
128
+ PemError: PemError,
129
+ CertificateError: CertificateError,
130
+ };
package/lib/oid.js ADDED
@@ -0,0 +1,219 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ /**
5
+ * @module pki.oid
6
+ * @nav Core
7
+ * @title Object Identifiers
8
+ * @order 40
9
+ * @slug oid
10
+ *
11
+ * @intro
12
+ * The object-identifier registry: a two-way map between dotted-decimal
13
+ * OID strings and their human names, plus arc conversion and DER
14
+ * encode/decode convenience. Every algorithm, attribute type, and
15
+ * extension in PKI is named by an OID, and resolving them through one
16
+ * registry — rather than scattering magic dotted strings across the
17
+ * codebase — is what lets a new algorithm be a data entry instead of a
18
+ * code change.
19
+ *
20
+ * The seed set covers the RFC 5280 attribute types and extensions, the
21
+ * classical signature / public-key / digest algorithms, and the
22
+ * NIST-assigned post-quantum arcs (ML-DSA, ML-KEM, SLH-DSA). Operators
23
+ * extend it with `register`.
24
+ *
25
+ * @card
26
+ * Two-way OID ↔ name registry with arc conversion, seeded with the
27
+ * RFC 5280 and NIST post-quantum object identifiers.
28
+ */
29
+
30
+ var asn1 = require("./asn1-der");
31
+ var frameworkError = require("./framework-error");
32
+
33
+ var OidError = frameworkError.OidError;
34
+
35
+ // SEED — [dotted, canonicalName]. Kept as a flat table so it reads as
36
+ // data and diffs cleanly when an arc is added.
37
+ var SEED = [
38
+ // --- RFC 5280 attribute types (2.5.4.*) ---
39
+ ["2.5.4.3", "commonName"],
40
+ ["2.5.4.4", "surname"],
41
+ ["2.5.4.5", "serialNumber"],
42
+ ["2.5.4.6", "countryName"],
43
+ ["2.5.4.7", "localityName"],
44
+ ["2.5.4.8", "stateOrProvinceName"],
45
+ ["2.5.4.9", "streetAddress"],
46
+ ["2.5.4.10", "organizationName"],
47
+ ["2.5.4.11", "organizationalUnitName"],
48
+ ["2.5.4.12", "title"],
49
+ ["2.5.4.42", "givenName"],
50
+ ["0.9.2342.19200300.100.1.25", "domainComponent"],
51
+ ["1.2.840.113549.1.9.1", "emailAddress"],
52
+
53
+ // --- Public-key + signature algorithms ---
54
+ ["1.2.840.113549.1.1.1", "rsaEncryption"],
55
+ ["1.2.840.113549.1.1.10", "rsassaPss"],
56
+ ["1.2.840.113549.1.1.11", "sha256WithRSAEncryption"],
57
+ ["1.2.840.113549.1.1.12", "sha384WithRSAEncryption"],
58
+ ["1.2.840.113549.1.1.13", "sha512WithRSAEncryption"],
59
+ ["1.2.840.10045.2.1", "ecPublicKey"],
60
+ ["1.2.840.10045.4.3.2", "ecdsaWithSHA256"],
61
+ ["1.2.840.10045.4.3.3", "ecdsaWithSHA384"],
62
+ ["1.2.840.10045.4.3.4", "ecdsaWithSHA512"],
63
+ ["1.2.840.10045.3.1.7", "prime256v1"],
64
+ ["1.3.132.0.34", "secp384r1"],
65
+ ["1.3.132.0.35", "secp521r1"],
66
+ ["1.3.101.110", "X25519"],
67
+ ["1.3.101.111", "X448"],
68
+ ["1.3.101.112", "Ed25519"],
69
+ ["1.3.101.113", "Ed448"],
70
+
71
+ // --- Digests ---
72
+ ["2.16.840.1.101.3.4.2.1", "sha256"],
73
+ ["2.16.840.1.101.3.4.2.2", "sha384"],
74
+ ["2.16.840.1.101.3.4.2.3", "sha512"],
75
+ ["2.16.840.1.101.3.4.2.8", "sha3-256"],
76
+ ["2.16.840.1.101.3.4.2.10", "sha3-512"],
77
+ ["2.16.840.1.101.3.4.2.12", "shake256"],
78
+
79
+ // --- RFC 5280 certificate extensions (2.5.29.*) ---
80
+ ["2.5.29.14", "subjectKeyIdentifier"],
81
+ ["2.5.29.15", "keyUsage"],
82
+ ["2.5.29.17", "subjectAltName"],
83
+ ["2.5.29.18", "issuerAltName"],
84
+ ["2.5.29.19", "basicConstraints"],
85
+ ["2.5.29.30", "nameConstraints"],
86
+ ["2.5.29.31", "cRLDistributionPoints"],
87
+ ["2.5.29.32", "certificatePolicies"],
88
+ ["2.5.29.35", "authorityKeyIdentifier"],
89
+ ["2.5.29.37", "extKeyUsage"],
90
+ ["1.3.6.1.5.5.7.1.1", "authorityInfoAccess"],
91
+
92
+ // --- NIST post-quantum algorithms ---
93
+ // FIPS 204 ML-DSA (signature-algorithm arc 2.16.840.1.101.3.4.3.*).
94
+ ["2.16.840.1.101.3.4.3.17", "id-ml-dsa-44"],
95
+ ["2.16.840.1.101.3.4.3.18", "id-ml-dsa-65"],
96
+ ["2.16.840.1.101.3.4.3.19", "id-ml-dsa-87"],
97
+ // FIPS 203 ML-KEM (KEM arc 2.16.840.1.101.3.4.4.*).
98
+ ["2.16.840.1.101.3.4.4.1", "id-ml-kem-512"],
99
+ ["2.16.840.1.101.3.4.4.2", "id-ml-kem-768"],
100
+ ["2.16.840.1.101.3.4.4.3", "id-ml-kem-1024"],
101
+ // FIPS 205 SLH-DSA (a representative slice of the SHAKE-256 family).
102
+ ["2.16.840.1.101.3.4.3.20", "id-slh-dsa-sha2-128s"],
103
+ ["2.16.840.1.101.3.4.3.24", "id-slh-dsa-shake-128s"],
104
+ ["2.16.840.1.101.3.4.3.27", "id-slh-dsa-shake-256s"],
105
+ ];
106
+
107
+ var _byOid = new Map();
108
+ var _byName = new Map();
109
+
110
+ function _index(dotted, name) {
111
+ _byOid.set(dotted, name);
112
+ // First registration of a name wins as the canonical reverse entry.
113
+ if (!_byName.has(name)) _byName.set(name, dotted);
114
+ }
115
+
116
+ for (var _i = 0; _i < SEED.length; _i++) _index(SEED[_i][0], SEED[_i][1]);
117
+
118
+ function _assertDotted(dotted, who) {
119
+ if (typeof dotted !== "string" || !/^\d+(\.\d+)+$/.test(dotted)) {
120
+ throw new OidError("oid/bad-input", who + ": expected a dotted-decimal OID string");
121
+ }
122
+ }
123
+
124
+ /**
125
+ * @primitive pki.oid.name
126
+ * @signature pki.oid.name(dotted) -> string | undefined
127
+ * @since 0.1.0
128
+ * @status stable
129
+ * @related pki.oid.byName, pki.oid.register
130
+ *
131
+ * Resolve a dotted OID to its registered name. Returns `undefined` for an
132
+ * unregistered OID (a caller that needs the raw arc keeps the dotted
133
+ * string); throws `OidError` only when the argument isn't a dotted OID.
134
+ *
135
+ * @example
136
+ * pki.oid.name("1.2.840.113549.1.1.11"); // -> "sha256WithRSAEncryption"
137
+ */
138
+ function name(dotted) {
139
+ _assertDotted(dotted, "name");
140
+ return _byOid.get(dotted);
141
+ }
142
+
143
+ function byName(n) {
144
+ if (typeof n !== "string" || n.length === 0) throw new OidError("oid/bad-input", "byName: expected a name string");
145
+ return _byName.get(n);
146
+ }
147
+
148
+ function has(dotted) {
149
+ _assertDotted(dotted, "has");
150
+ return _byOid.has(dotted);
151
+ }
152
+
153
+ /**
154
+ * @primitive pki.oid.register
155
+ * @signature pki.oid.register(dotted, name) -> void
156
+ * @since 0.1.0
157
+ * @status stable
158
+ * @related pki.oid.name
159
+ *
160
+ * Add (or override) an OID → name mapping so an operator's private or
161
+ * newly-standardized arc resolves through the same registry as the seed
162
+ * set. A later registration of the same OID replaces the forward name;
163
+ * the reverse (name → OID) keeps the first registration as canonical.
164
+ *
165
+ * @example
166
+ * pki.oid.register("1.3.6.1.4.1.99999.1", "acmeWidgetPolicy");
167
+ */
168
+ function register(dotted, n) {
169
+ _assertDotted(dotted, "register");
170
+ if (typeof n !== "string" || n.length === 0) throw new OidError("oid/bad-input", "register: name must be a non-empty string");
171
+ _index(dotted, n);
172
+ }
173
+
174
+ function all() {
175
+ var out = {};
176
+ _byOid.forEach(function (v, k) { out[k] = v; });
177
+ return out;
178
+ }
179
+
180
+ // toArcs / fromArcs — dotted <-> numeric arc array. Arcs come back as
181
+ // Number where safe and BigInt where an arc exceeds 2^53 (rare, but a
182
+ // UUID-based OID arc can), so the round-trip never loses precision.
183
+ function toArcs(dotted) {
184
+ _assertDotted(dotted, "toArcs");
185
+ return dotted.split(".").map(function (p) {
186
+ var b = BigInt(p);
187
+ return b <= 9007199254740991n ? Number(b) : b;
188
+ });
189
+ }
190
+
191
+ function fromArcs(arcs) {
192
+ if (!Array.isArray(arcs) || arcs.length < 2) throw new OidError("oid/bad-input", "fromArcs: expected an array of >= 2 arcs");
193
+ return arcs.map(function (a) {
194
+ if (typeof a === "bigint") return a.toString();
195
+ if (typeof a === "number" && Number.isInteger(a) && a >= 0) return String(a);
196
+ throw new OidError("oid/bad-arc", "fromArcs: arc " + String(a) + " is not a non-negative integer");
197
+ }).join(".");
198
+ }
199
+
200
+ // DER convenience — thin pass-throughs to the codec so callers reach for
201
+ // one namespace when they have a dotted string in hand.
202
+ function toDER(dotted) { _assertDotted(dotted, "toDER"); return asn1.build.oid(dotted); }
203
+ function fromDER(input) {
204
+ var buf = Buffer.isBuffer(input) ? input : Buffer.from(input);
205
+ var node = asn1.decode(buf);
206
+ return asn1.read.oid(node);
207
+ }
208
+
209
+ module.exports = {
210
+ name: name,
211
+ byName: byName,
212
+ has: has,
213
+ register: register,
214
+ all: all,
215
+ toArcs: toArcs,
216
+ fromArcs: fromArcs,
217
+ toDER: toDER,
218
+ fromDER: fromDER,
219
+ };
@@ -0,0 +1,4 @@
1
+ {
2
+ "_comment": "Vendored dependencies — none currently. @blamejs/pki's cryptography runs entirely on Node's built-in node:crypto: the classical algorithm set (RSA, ECDSA, EdDSA, ECDH, AES, HMAC, HKDF, PBKDF2, the SHA family) AND the FIPS 203/204/205 post-quantum algorithms (ML-KEM, ML-DSA, SLH-DSA) via the platform OpenSSL 3.5 that the Node engine floor (>=24.18) ships. A built-in ships zero bytes and is OpenSSL-interoperable by construction, so no crypto bundle is vendored. A package is added here ONLY when a specific operation is confirmed missing from the engine floor; see lib/vendor/README.md for the policy.",
3
+ "packages": {}
4
+ }
@@ -0,0 +1,41 @@
1
+ # Vendored dependencies
2
+
3
+ `@blamejs/pki` ships with **zero npm runtime dependencies**, and it currently
4
+ vendors **nothing** — this directory holds only the manifest.
5
+
6
+ ## Native-first crypto
7
+
8
+ The toolkit's cryptography runs entirely on Node's built-in `node:crypto`. The
9
+ engine floor (Node `>=24.18`) links OpenSSL 3.5, which provides:
10
+
11
+ - the full classical set — RSA (PKCS#1 v1.5, PSS, OAEP), ECDSA, EdDSA
12
+ (Ed25519/Ed448), ECDH (incl. X25519/X448), AES (GCM/CBC/CTR/KW), HMAC, HKDF,
13
+ PBKDF2, and the SHA-1/2/3 family; and
14
+ - the FIPS post-quantum set — **ML-KEM** (FIPS 203), **ML-DSA** (FIPS 204), and
15
+ **SLH-DSA** (FIPS 205).
16
+
17
+ A platform built-in ships **zero bytes**, has **nothing to hash-pin or keep
18
+ current**, and is **OpenSSL/NSS-interoperable by construction** — which is what
19
+ the toolkit's interoperability gate needs. So there is no reason to vendor a
20
+ crypto library when the runtime already provides the primitive.
21
+
22
+ ## When something IS vendored
23
+
24
+ A package is added here **only** when a specific operation is confirmed missing
25
+ from the engine floor (for example, a pure-JS fallback for a post-quantum
26
+ operation whose JS binding is absent on a supported Node version, or a
27
+ cross-check reference used only in tests). When that happens:
28
+
29
+ - `MANIFEST.json` records the package's version, SPDX license, upstream author,
30
+ source URL, exported surface, bundler invocation, CPE, and pinned SHA-256, so
31
+ any tampering is detectable and `scripts/check-vendor-currency.js` can gate
32
+ version drift.
33
+ - The top-level `NOTICE` gains the component's attribution.
34
+ - The reason and the re-open condition are recorded alongside the entry — a
35
+ vendored dependency is a deliberate, justified exception to native-first, not
36
+ a default.
37
+
38
+ Bundles, when present, are produced with
39
+ `esbuild --format=cjs --minify --platform=node` against the pinned upstream
40
+ version; refreshing one recomputes its SHA-256 and updates the bytes and the
41
+ matching `hashes.server` entry in the same change.