@blamejs/pki 0.1.22 → 0.1.24

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.
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * @intro
13
13
  * X.509 certificate handling per RFC 5280. The seed surface is
14
- * `parse` turn a DER or PEM certificate into a structured,
14
+ * `parse` -- turn a DER or PEM certificate into a structured,
15
15
  * fully-decoded object: version, serial, signature algorithm, issuer
16
16
  * and subject distinguished names, validity window (as real `Date`s),
17
17
  * subject public-key info, and the extension list. The parser composes
@@ -48,15 +48,16 @@ var PemError = frameworkError.PemError;
48
48
  * @spec RFC 7468, RFC 5280
49
49
  * @related pki.schema.x509.pemEncode
50
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.
51
+ * Extract the DER bytes from a PEM block (default label `CERTIFICATE`, the
52
+ * RFC 7468 sec. 5 armor -- the canonical-label default every sibling format
53
+ * applies). Pass a `label` to enforce a different block type, or an explicit
54
+ * `null` to take the first block of any type. Throws `PemError` on a
55
+ * missing / mismatched envelope or a non-base64 body.
55
56
  *
56
57
  * @example
57
- * var der = pki.schema.x509.pemDecode(pemText, "CERTIFICATE");
58
+ * var der = pki.schema.x509.pemDecode(pemText);
58
59
  */
59
- function pemDecode(text, label) { return pkix.pemDecode(text, label, PemError); }
60
+ function pemDecode(text, label) { return pkix.pemDecode(text, label === null ? null : (label || "CERTIFICATE"), PemError); }
60
61
 
61
62
  /**
62
63
  * @primitive pki.schema.x509.pemEncode
@@ -85,17 +86,19 @@ var NS = pkix.makeNS("x509", CertificateError, oid);
85
86
  var ALGORITHM_IDENTIFIER = pkix.algorithmIdentifier(NS);
86
87
  var NAME = pkix.name(NS);
87
88
 
88
- // Validity ::= SEQUENCE { notBefore Time, notAfter Time }. The historical guard
89
- // checked only children.length===2 (not the SEQUENCE tag) -> assert "constructed".
89
+ // Validity ::= SEQUENCE { notBefore Time, notAfter Time } -- the outer tag is
90
+ // asserted (a SET-tagged body is a different ASN.1 type, not a Validity).
91
+ // pkix.time enforces the RFC 5280 sec. 4.1.2.5 encoding cutover on decode: a
92
+ // validity date through 2049 must be UTCTime, GeneralizedTime is 2050 onward.
90
93
  var VALIDITY = schema.seq([
91
- schema.field("notBefore", schema.time(NS)),
92
- schema.field("notAfter", schema.time(NS)),
94
+ schema.field("notBefore", pkix.time(NS)),
95
+ schema.field("notAfter", pkix.time(NS)),
93
96
  ], {
94
- assert: "constructed", arity: { exact: 2 }, code: "x509/bad-validity", what: "Validity",
97
+ assert: "sequence", arity: { exact: 2 }, code: "x509/bad-validity", what: "Validity",
95
98
  build: function (m) { return { notBefore: m.fields.notBefore.value, notAfter: m.fields.notAfter.value }; },
96
99
  });
97
100
 
98
- // SubjectPublicKeyInfo the shared pkix.spki factory under the x509 NS.
101
+ // SubjectPublicKeyInfo -- the shared pkix.spki factory under the x509 NS.
99
102
  var SPKI = pkix.spki(NS);
100
103
 
101
104
  var EXTENSIONS = pkix.extensions(NS);
@@ -103,7 +106,7 @@ var EXTENSIONS = pkix.extensions(NS);
103
106
  // Version ::= INTEGER { v1(0), v2(1), v3(2) }, [0] EXPLICIT DEFAULT v1. Read as a
104
107
  // BigInt so an out-of-range value can't coerce to a float; reject an explicitly-
105
108
  // encoded v1 (DER forbids encoding the DEFAULT).
106
- // version ::= INTEGER { v1(0), v2(1), v3(2) } inside a [0] EXPLICIT DEFAULT v1
109
+ // version ::= INTEGER { v1(0), v2(1), v3(2) } inside a [0] EXPLICIT DEFAULT v1 --
107
110
  // DER forbids encoding the default, so 0 is rejected; 1->v2, 2->v3.
108
111
  var CERTIFICATE_VERSION = pkix.versionReader(NS, { "1": 2, "2": 3 });
109
112
 
@@ -122,19 +125,21 @@ var CERTIFICATE_TBS = schema.seq([
122
125
  schema.field("subject", NAME),
123
126
  schema.field("subjectPublicKeyInfo", SPKI),
124
127
  schema.trailing([
125
- { tag: 1, name: "issuerUniqueID", schema: schema.any() },
126
- { tag: 2, name: "subjectUniqueID", schema: schema.any() },
128
+ // RFC 5280 sec. 4.1.2.8 -- the unique identifiers are [n] IMPLICIT BIT
129
+ // STRING (context PRIMITIVE, unusedBits validated), never an EXPLICIT wrap.
130
+ { tag: 1, name: "issuerUniqueID", schema: schema.implicitBitString(1) },
131
+ { tag: 2, name: "subjectUniqueID", schema: schema.implicitBitString(2) },
127
132
  { tag: 3, name: "extensions", schema: EXTENSIONS, explicit: true, emptyCode: "x509/bad-extensions" },
128
133
  ], { minTag: 1, maxTag: 3, unexpectedCode: "x509/bad-tbs", orderCode: "x509/bad-tbs" }),
129
- ], { assert: "constructed", code: "x509/bad-tbs", what: "tbsCertificate" });
134
+ ], { assert: "sequence", code: "x509/bad-tbs", what: "tbsCertificate" });
130
135
 
131
136
  // Certificate ::= SEQUENCE { tbsCertificate, signatureAlgorithm, signatureValue
132
- // BIT STRING }. The build runs the RFC 5280 cross-field checks (§4.1.1.2 sig-alg
133
- // agreement, §4.1.2.4 non-empty issuer, §4.1.2.9 extensions-only-in-v3) after the
137
+ // BIT STRING }. The build runs the RFC 5280 cross-field checks (sec. 4.1.1.2 sig-alg
138
+ // agreement, sec. 4.1.2.4 non-empty issuer, sec. 4.1.2.9 extensions-only-in-v3) after the
134
139
  // structural walk, then assembles the public parse result. Raw-byte accessors
135
140
  // (serialNumberHex, tbsBytes, sig-alg agreement) read off the match-tree nodes.
136
141
  // Certificate ::= SEQUENCE { tbsCertificate, signatureAlgorithm, signatureValue }
137
- // the shared SIGNED envelope. The certificate-specific invariants (outer==inner
142
+ // -- the shared SIGNED envelope. The certificate-specific invariants (outer==inner
138
143
  // signatureAlgorithm agreement, non-empty issuer, v3-only extensions) live in the
139
144
  // build; the envelope owns the SEQUENCE-of-3 shape and signature extraction.
140
145
  var CERTIFICATE = pkix.signedEnvelope(NS, CERTIFICATE_TBS, {
@@ -142,25 +147,31 @@ var CERTIFICATE = pkix.signedEnvelope(NS, CERTIFICATE_TBS, {
142
147
  build: function (e, ctx) {
143
148
  var tbs = e.tbsMatch; // CERTIFICATE_TBS seq-match (no build)
144
149
 
145
- // RFC 5280 §4.1.1.2 outer signatureAlgorithm MUST equal tbsCertificate.signature.
150
+ // RFC 5280 sec. 4.1.1.2 -- outer signatureAlgorithm MUST equal tbsCertificate.signature.
146
151
  if (!e.outerSignatureAlgorithmBytes.equals(tbs.fields.signature.node.bytes)) {
147
- throw ctx.E("x509/bad-signature-algorithm", "signatureAlgorithm must match tbsCertificate.signature (RFC 5280 §4.1.1.2)");
152
+ throw ctx.E("x509/bad-signature-algorithm", "signatureAlgorithm must match tbsCertificate.signature (RFC 5280 sec. 4.1.1.2)");
148
153
  }
149
154
 
150
155
  var version = tbs.fields.version.value; // 1 (default) | 2 | 3
151
156
  var issuer = tbs.fields.issuer.value.result;
152
- // RFC 5280 §4.1.2.4 issuer MUST be non-empty (an empty subject is permitted, §4.1.2.6).
157
+ // RFC 5280 sec. 4.1.2.4 -- issuer MUST be non-empty (an empty subject is permitted, sec. 4.1.2.6).
153
158
  if (!issuer.rdns.length) {
154
159
  throw ctx.E("x509/bad-issuer", "issuer must be a non-empty distinguished name");
155
160
  }
156
161
 
157
162
  var extField = tbs.fields.extensions;
158
163
  var hasExtensions = !!(extField && extField.present);
159
- // RFC 5280 §4.1.2.9 extensions appear only in a v3 certificate.
164
+ // RFC 5280 sec. 4.1.2.9 -- extensions appear only in a v3 certificate.
160
165
  if (hasExtensions && version !== 3) {
161
166
  throw ctx.E("x509/bad-version", "extensions are only permitted in a v3 certificate");
162
167
  }
163
168
 
169
+ // RFC 5280 sec. 4.1.2.8 -- issuerUniqueID / subjectUniqueID MUST NOT
170
+ // appear when the version is 1; they require a v2 or v3 certificate.
171
+ if ((tbs.fields.issuerUniqueID.present || tbs.fields.subjectUniqueID.present) && version < 2) {
172
+ throw ctx.E("x509/bad-version", "issuerUniqueID/subjectUniqueID require a v2 or v3 certificate (RFC 5280 sec. 4.1.2.8)");
173
+ }
174
+
164
175
  var serialNode = tbs.fields.serialNumber.node;
165
176
  return {
166
177
  version: version,
@@ -211,7 +222,7 @@ var parse = pkix.makeParser({ pemLabel: "CERTIFICATE", PemError: PemError, Error
211
222
 
212
223
  // matches(root): does the decoded DER look like a Certificate? A CSR and a CRL
213
224
  // share the outer SEQUENCE-of-3 envelope, so the discriminator is inside the
214
- // tbs: a certificate has a Validity a SEQUENCE of exactly two Time values at
225
+ // tbs: a certificate has a Validity -- a SEQUENCE of exactly two Time values -- at
215
226
  // position [serial, signature, issuer, VALIDITY] after the optional [0] version.
216
227
  // A CSR's certificationRequestInfo has subjectPublicKeyInfo there (no Validity),
217
228
  // and a CRL leads with a bare Time; neither matches. Used by the orchestrator.
package/lib/webcrypto.js CHANGED
@@ -13,13 +13,13 @@
13
13
  * A zero-dependency W3C Web Cryptography API (`Crypto` / `SubtleCrypto`
14
14
  * / `CryptoKey`) built directly on Node's native `node:crypto`. It is
15
15
  * the toolkit's injectable crypto engine, presented in the standard
16
- * WebCrypto shape so operators and every higher structure (X.509,
17
- * CMS, OCSP) reach for one familiar surface.
16
+ * WebCrypto shape so operators -- and every higher structure (X.509,
17
+ * CMS, OCSP) -- reach for one familiar surface.
18
18
  *
19
19
  * Unlike the browser's built-in `crypto.subtle`, this engine is
20
20
  * **PQC-first without being PQC-only**: the FIPS 204 ML-DSA and FIPS
21
21
  * 205 SLH-DSA signature suites sit alongside the full classical set PKI
22
- * still runs on RSASSA-PKCS1-v1_5, RSA-PSS, RSA-OAEP, ECDSA, ECDH,
22
+ * still runs on -- RSASSA-PKCS1-v1_5, RSA-PSS, RSA-OAEP, ECDSA, ECDH,
23
23
  * Ed25519 / Ed448, AES-GCM / CBC / KW, HMAC, HKDF, PBKDF2, and the SHA
24
24
  * family (including legacy SHA-1 for old certificates and signatures).
25
25
  * FIPS 203 ML-KEM key generation and encoding are available; KEM
@@ -29,16 +29,18 @@
29
29
  *
30
30
  * @card
31
31
  * A zero-dep, PQC-first W3C WebCrypto (`SubtleCrypto`) engine over
32
- * `node:crypto` ML-DSA + SLH-DSA signatures alongside the full
32
+ * `node:crypto` -- ML-DSA + SLH-DSA signatures alongside the full
33
33
  * classical algorithm set.
34
34
  */
35
35
 
36
36
  var nodeCrypto = require("node:crypto");
37
37
  var frameworkError = require("./framework-error");
38
38
 
39
- // Single-owner error class co-located with its module (framework-error
40
- // stays the cross-module home; this is webcrypto-private).
41
- var WebCryptoError = frameworkError.defineClass("WebCryptoError");
39
+ // Single-owner error class -- co-located with its module (framework-error
40
+ // stays the cross-module home; this is webcrypto-private). withCause: a
41
+ // failure discovered while processing decrypted/untrusted bytes threads the
42
+ // underlying fault instead of discarding it.
43
+ var WebCryptoError = frameworkError.defineClass("WebCryptoError", { withCause: true });
42
44
 
43
45
  var MAX_RANDOM_BYTES = 65536;
44
46
 
@@ -88,6 +90,29 @@ function _hashNode(h, who) {
88
90
  return node;
89
91
  }
90
92
 
93
+ // W3C HMAC get-key-length: an explicit `length` is used as given (validated
94
+ // to a positive multiple of 8, so the byte-level key material is exact and
95
+ // never a raw RangeError out of randomBytes); an OMITTED length defaults to
96
+ // the BLOCK size of the hash -- the HMAC key-pad width -- NOT the digest
97
+ // size. A digest-size default would mint different key material than every
98
+ // conforming WebCrypto for identical inputs, so MACs keyed through this
99
+ // engine would fail to verify elsewhere.
100
+ var HMAC_BLOCK_BITS = {
101
+ "SHA-1": 512, "SHA-256": 512, "SHA-384": 1024, "SHA-512": 1024,
102
+ "SHA3-256": 1088, "SHA3-384": 832, "SHA3-512": 576,
103
+ };
104
+
105
+ function _hmacLengthBits(alg, who) {
106
+ var name = (typeof alg.hash === "string") ? alg.hash : (alg.hash && alg.hash.name);
107
+ var blockBits = HMAC_BLOCK_BITS[String(name).toUpperCase()];
108
+ if (!blockBits) throw new WebCryptoError("webcrypto/not-supported", who + ": unsupported hash " + JSON.stringify(name));
109
+ if (alg.length == null) return blockBits;
110
+ if (typeof alg.length !== "number" || !isFinite(alg.length) || alg.length <= 0 || alg.length % 8 !== 0) {
111
+ throw new WebCryptoError("webcrypto/syntax", who + ": HMAC length must be a positive multiple of 8 bits");
112
+ }
113
+ return alg.length;
114
+ }
115
+
91
116
  // WebCrypto namedCurve -> node namedCurve.
92
117
  var CURVE_NODE = { "P-256": "prime256v1", "P-384": "secp384r1", "P-521": "secp521r1" };
93
118
  var CURVE_FIELD_BYTES = { "P-256": 32, "P-384": 48, "P-521": 66 };
@@ -95,7 +120,7 @@ var CURVE_FIELD_BYTES = { "P-256": 32, "P-384": 48, "P-521": 66 };
95
120
  var ML_DSA_NODE = { "ML-DSA-44": "ml-dsa-44", "ML-DSA-65": "ml-dsa-65", "ML-DSA-87": "ml-dsa-87" };
96
121
  var ML_KEM_NODE = { "ML-KEM-512": "ml-kem-512", "ML-KEM-768": "ml-kem-768", "ML-KEM-1024": "ml-kem-1024" };
97
122
 
98
- // FIPS 205 SLH-DSA stateless hash-based signatures. All twelve
123
+ // FIPS 205 SLH-DSA -- stateless hash-based signatures. All twelve
99
124
  // parameter sets Node exposes; signing is one-shot (null algorithm), the
100
125
  // same shape as ML-DSA / EdDSA. KEM encapsulation for ML-KEM is not yet
101
126
  // in Node's API, so ML-KEM here is key-generation / encoding only.
@@ -104,6 +129,17 @@ var SLH_DSA_NODE = {};
104
129
  "shake-128s", "shake-128f", "shake-192s", "shake-192f", "shake-256s", "shake-256f"
105
130
  ].forEach(function (s) { SLH_DSA_NODE["SLH-DSA-" + s.toUpperCase()] = "slh-dsa-" + s; });
106
131
 
132
+ // The algorithm names each keyed operation recognizes. Membership is checked
133
+ // BEFORE the algorithm/key name binding so an unrecognized algorithm reports
134
+ // NotSupportedError while a recognized-but-wrong-for-this-key one reports
135
+ // InvalidAccessError, matching the W3C error ordering.
136
+ var SIGN_VERIFY_NAMES = {};
137
+ ["RSASSA-PKCS1-V1_5", "RSA-PSS", "ECDSA", "ED25519", "ED448", "HMAC"]
138
+ .concat(Object.keys(ML_DSA_NODE), Object.keys(SLH_DSA_NODE))
139
+ .forEach(function (n) { SIGN_VERIFY_NAMES[n] = true; });
140
+ var ENCRYPT_DECRYPT_NAMES = { "RSA-OAEP": true, "AES-GCM": true, "AES-CBC": true, "AES-CTR": true };
141
+ var DERIVE_NAMES = { "ECDH": true, "X25519": true, "X448": true, "HKDF": true, "PBKDF2": true };
142
+
107
143
  // ---- CryptoKey -------------------------------------------------------
108
144
 
109
145
  /**
@@ -111,11 +147,11 @@ var SLH_DSA_NODE = {};
111
147
  * @signature new pki.webcrypto.CryptoKey(type, extractable, algorithm, usages, handle)
112
148
  * @since 0.1.0
113
149
  * @status stable
114
- * @spec W3C WebCrypto §cryptokey
150
+ * @spec W3C WebCrypto sec. cryptokey
115
151
  *
116
152
  * Opaque handle to key material, matching the W3C `CryptoKey` shape:
117
153
  * `{ type, extractable, algorithm, usages }`. The underlying
118
- * `node:crypto` KeyObject is non-enumerable and never serialized
154
+ * `node:crypto` KeyObject is non-enumerable and never serialized --
119
155
  * extract material only through `subtle.exportKey`, and only when the key
120
156
  * was created `extractable`. Instances are produced by
121
157
  * `subtle.generateKey` / `subtle.importKey`; the constructor is rarely
@@ -140,6 +176,20 @@ function _requireUsage(key, usage) {
140
176
  }
141
177
  }
142
178
 
179
+ // W3C WebCrypto sign/verify/encrypt/decrypt/deriveBits/deriveKey/wrapKey/
180
+ // unwrapKey MUST throw an InvalidAccessError when the normalized algorithm's
181
+ // name differs from the key's own algorithm name. The binding is load-bearing
182
+ // for the one-shot signature families (EdDSA / ML-DSA / SLH-DSA): node derives
183
+ // the algorithm from the KEY handle, so without this check the requested name
184
+ // would be silently ignored and an operation requested under algorithm X could
185
+ // be satisfied by a key of algorithm Y (algorithm confusion).
186
+ function _requireAlgMatch(alg, key, who) {
187
+ var keyName = key && key.algorithm && key.algorithm.name;
188
+ if (String(keyName).toUpperCase() !== alg.name) {
189
+ throw new WebCryptoError("webcrypto/invalid-access", who + ": algorithm " + JSON.stringify(alg.name) + " does not match the key's algorithm " + JSON.stringify(keyName));
190
+ }
191
+ }
192
+
143
193
  // ---- SubtleCrypto ----------------------------------------------------
144
194
 
145
195
  function SubtleCrypto() {}
@@ -164,8 +214,7 @@ SubtleCrypto.prototype.generateKey = async function generateKey(algorithm, extra
164
214
  return new CryptoKey("secret", extractable, { name: name, length: bits }, usages, secret);
165
215
  }
166
216
  if (name === "HMAC") {
167
- var hnode = _hashNode(alg.hash, "generateKey HMAC");
168
- var lenBits = alg.length || (nodeCrypto.createHash(hnode).digest().length * 8);
217
+ var lenBits = _hmacLengthBits(alg, "generateKey HMAC");
169
218
  var hkey = nodeCrypto.createSecretKey(nodeCrypto.randomBytes(lenBits / 8));
170
219
  return new CryptoKey("secret", extractable, { name: name, hash: { name: (typeof alg.hash === "string" ? alg.hash : alg.hash.name) }, length: lenBits }, usages, hkey);
171
220
  }
@@ -213,13 +262,32 @@ function _generateKeyPair(alg) {
213
262
  }
214
263
 
215
264
  function _hashObj(h) { if (!h) return undefined; return { name: (typeof h === "string" ? h : h.name) }; }
216
- function _bufToBigIntNum(exp) { var b = _toBuf(exp, "publicExponent"); return Number(BigInt("0x" + b.toString("hex"))); }
265
+ // publicExponent arrives as a W3C BigInteger octet string but node:crypto
266
+ // takes a JS number, so the value is bounds-checked BEFORE the Number()
267
+ // narrowing: an empty buffer has no integer value (BigInt("0x") is a raw
268
+ // SyntaxError), and a value above 2^32-1 is outside the interoperable
269
+ // WebCrypto exponent range and heads toward Number's exact-integer limit,
270
+ // where the narrowing would silently hand node a different exponent than
271
+ // the caller requested.
272
+ function _bufToBigIntNum(exp) {
273
+ var b = _toBuf(exp, "publicExponent");
274
+ if (b.length === 0) {
275
+ throw new WebCryptoError("webcrypto/syntax", "publicExponent must be a non-empty BigInteger octet string");
276
+ }
277
+ var v = BigInt("0x" + b.toString("hex"));
278
+ if (v > 0xffffffffn) {
279
+ throw new WebCryptoError("webcrypto/syntax", "publicExponent " + v.toString() + " exceeds the 2^32-1 bound");
280
+ }
281
+ return Number(v);
282
+ }
217
283
 
218
284
  SubtleCrypto.prototype.sign = async function sign(algorithm, key, data) {
219
285
  var alg = _normalizeAlg(algorithm, "sign");
220
286
  _requireUsage(key, "sign");
221
287
  var buf = _toBuf(data, "sign");
222
288
  var name = alg.name;
289
+ if (!SIGN_VERIFY_NAMES[name]) throw new WebCryptoError("webcrypto/not-supported", "sign: unsupported algorithm " + JSON.stringify(name));
290
+ _requireAlgMatch(alg, key, "sign");
223
291
  if (name === "RSASSA-PKCS1-V1_5") {
224
292
  return _toArrayBuffer(nodeCrypto.sign(_hashNode(key.algorithm.hash, "sign"), buf, key._handle));
225
293
  }
@@ -249,6 +317,8 @@ SubtleCrypto.prototype.verify = async function verify(algorithm, key, signature,
249
317
  var sig = _toBuf(signature, "verify");
250
318
  var buf = _toBuf(data, "verify");
251
319
  var name = alg.name;
320
+ if (!SIGN_VERIFY_NAMES[name]) throw new WebCryptoError("webcrypto/not-supported", "verify: unsupported algorithm " + JSON.stringify(name));
321
+ _requireAlgMatch(alg, key, "verify");
252
322
  if (name === "RSASSA-PKCS1-V1_5") {
253
323
  return nodeCrypto.verify(_hashNode(key.algorithm.hash, "verify"), buf, key._handle, sig);
254
324
  }
@@ -269,10 +339,11 @@ SubtleCrypto.prototype.verify = async function verify(algorithm, key, signature,
269
339
  hm.update(buf);
270
340
  // Verify must RESOLVE false for any invalid signature, including one of
271
341
  // the wrong length. timingSafeEqual throws RangeError on a length
272
- // mismatch, so gate on length first; the length is already public, so
273
- // this leaks nothing a constant-time compare would protect.
274
- var mac = hm.digest();
275
- return mac.length === sig.length && nodeCrypto.timingSafeEqual(mac, sig);
342
+ // mismatch, so gate on the (public) length first, then compare the bytes
343
+ // in constant time. The length check leaks nothing a constant-time compare
344
+ // would protect; the secret-dependent byte comparison is timingSafeEqual.
345
+ var digest = hm.digest();
346
+ return digest.length === sig.length && nodeCrypto.timingSafeEqual(digest, sig);
276
347
  }
277
348
  throw new WebCryptoError("webcrypto/not-supported", "verify: unsupported algorithm " + JSON.stringify(name));
278
349
  };
@@ -282,6 +353,8 @@ SubtleCrypto.prototype.encrypt = async function encrypt(algorithm, key, data) {
282
353
  _requireUsage(key, "encrypt");
283
354
  var buf = _toBuf(data, "encrypt");
284
355
  var name = alg.name;
356
+ if (!ENCRYPT_DECRYPT_NAMES[name]) throw new WebCryptoError("webcrypto/not-supported", "encrypt: unsupported algorithm " + JSON.stringify(name));
357
+ _requireAlgMatch(alg, key, "encrypt");
285
358
  if (name === "RSA-OAEP") {
286
359
  return _toArrayBuffer(nodeCrypto.publicEncrypt({
287
360
  key: key._handle, padding: nodeCrypto.constants.RSA_PKCS1_OAEP_PADDING,
@@ -313,6 +386,8 @@ SubtleCrypto.prototype.decrypt = async function decrypt(algorithm, key, data) {
313
386
  _requireUsage(key, "decrypt");
314
387
  var buf = _toBuf(data, "decrypt");
315
388
  var name = alg.name;
389
+ if (!ENCRYPT_DECRYPT_NAMES[name]) throw new WebCryptoError("webcrypto/not-supported", "decrypt: unsupported algorithm " + JSON.stringify(name));
390
+ _requireAlgMatch(alg, key, "decrypt");
316
391
  if (name === "RSA-OAEP") {
317
392
  return _toArrayBuffer(nodeCrypto.privateDecrypt({
318
393
  key: key._handle, padding: nodeCrypto.constants.RSA_PKCS1_OAEP_PADDING,
@@ -346,7 +421,7 @@ function _secretBytes(key) { return key._handle.export(); }
346
421
 
347
422
  // AES-CTR: node always treats the full 128-bit block as the counter and
348
423
  // never reads the spec's `length` (counter-width) parameter. A length < 128
349
- // would silently diverge from the W3C definition, so fail closed accept
424
+ // would silently diverge from the W3C definition, so fail closed -- accept
350
425
  // only the one value node can honor.
351
426
  function _requireCtrLength128(alg) {
352
427
  if (alg.length !== 128) {
@@ -354,21 +429,41 @@ function _requireCtrLength128(alg) {
354
429
  }
355
430
  }
356
431
 
357
- // Raw key-agreement / KDF derivation with NO usage check the usage a
432
+ // W3C deriveBits: the requested length must be a positive multiple of 8 --
433
+ // the `length / 8` narrowing in the per-algorithm branches below would
434
+ // otherwise silently truncate a fractional byte count.
435
+ function _requireDeriveLength(length, who) {
436
+ if (typeof length !== "number" || !isFinite(length) || length <= 0 || length % 8 !== 0) {
437
+ throw new WebCryptoError("webcrypto/operation", who + ": length must be a positive multiple of 8 bits");
438
+ }
439
+ }
440
+
441
+ // Raw key-agreement / KDF derivation with NO usage check -- the usage a
358
442
  // caller must hold differs by entry point (deriveBits requires "deriveBits",
359
443
  // deriveKey requires "deriveKey"), so each public method checks its own
360
444
  // usage and then routes the actual derivation through here.
361
445
  function _deriveBitsRaw(alg, key, length) {
362
446
  var name = alg.name;
363
447
  if (name === "ECDH" || name === "X25519" || name === "X448") {
448
+ _requireAlgMatch(alg, alg.public, name + " public key");
364
449
  var secret = nodeCrypto.diffieHellman({ privateKey: key._handle, publicKey: alg.public._handle });
365
- return _toArrayBuffer(length == null ? secret : secret.subarray(0, length / 8));
450
+ if (length == null) return _toArrayBuffer(secret);
451
+ _requireDeriveLength(length, name);
452
+ // subarray clamps at the end of the secret, so an unchecked over-request
453
+ // would silently return fewer bytes than asked. W3C deriveBits: throw an
454
+ // OperationError when the requested length cannot be satisfied.
455
+ if (length / 8 > secret.length) {
456
+ throw new WebCryptoError("webcrypto/operation", name + ": requested " + length + " bits but the shared secret has " + (secret.length * 8));
457
+ }
458
+ return _toArrayBuffer(secret.subarray(0, length / 8));
366
459
  }
367
460
  if (name === "HKDF") {
461
+ _requireDeriveLength(length, "HKDF");
368
462
  var derived = nodeCrypto.hkdfSync(_hashNode(alg.hash, "HKDF"), _secretBytes(key), _toBuf(alg.salt, "HKDF salt"), _toBuf(alg.info || Buffer.alloc(0), "HKDF info"), length / 8);
369
463
  return derived instanceof ArrayBuffer ? derived : _toArrayBuffer(Buffer.from(derived));
370
464
  }
371
465
  if (name === "PBKDF2") {
466
+ _requireDeriveLength(length, "PBKDF2");
372
467
  var out = nodeCrypto.pbkdf2Sync(_secretBytes(key), _toBuf(alg.salt, "PBKDF2 salt"), alg.iterations, length / 8, _hashNode(alg.hash, "PBKDF2"));
373
468
  return _toArrayBuffer(out);
374
469
  }
@@ -378,18 +473,42 @@ function _deriveBitsRaw(alg, key, length) {
378
473
  SubtleCrypto.prototype.deriveBits = async function deriveBits(algorithm, key, length) {
379
474
  var alg = _normalizeAlg(algorithm, "deriveBits");
380
475
  _requireUsage(key, "deriveBits");
476
+ if (!DERIVE_NAMES[alg.name]) throw new WebCryptoError("webcrypto/not-supported", "deriveBits: unsupported algorithm " + JSON.stringify(alg.name));
477
+ _requireAlgMatch(alg, key, "deriveBits");
381
478
  return _deriveBitsRaw(alg, key, length);
382
479
  };
383
480
 
384
481
  SubtleCrypto.prototype.deriveKey = async function deriveKey(algorithm, baseKey, derivedKeyType, extractable, keyUsages) {
385
- // deriveKey requires the "deriveKey" usage NOT "deriveBits". Delegating
482
+ // deriveKey requires the "deriveKey" usage -- NOT "deriveBits". Delegating
386
483
  // to this.deriveBits would false-reject a key created with ["deriveKey"]
387
484
  // and fail-open on a ["deriveBits"]-only key, so check the correct usage
388
485
  // here and route the raw derivation past deriveBits' own usage gate.
389
486
  _requireUsage(baseKey, "deriveKey");
390
487
  var alg = _normalizeAlg(algorithm, "deriveKey");
488
+ if (!DERIVE_NAMES[alg.name]) throw new WebCryptoError("webcrypto/not-supported", "deriveKey: unsupported algorithm " + JSON.stringify(alg.name));
489
+ _requireAlgMatch(alg, baseKey, "deriveKey");
391
490
  var dk = _normalizeAlg(derivedKeyType, "deriveKey");
392
- var bits = dk.length || (dk.name.indexOf("AES") === 0 ? dk.length : 256);
491
+ var bits;
492
+ if (dk.name.indexOf("AES") === 0) {
493
+ // W3C get-key-length for AES: the derivedKeyType MUST carry a length of
494
+ // 128/192/256. Without this check the derived key would silently take
495
+ // the size of the raw agreement/KDF output (e.g. a 384-bit "AES" key
496
+ // from a P-384 secret) instead of a usable AES size.
497
+ if (dk.length !== 128 && dk.length !== 192 && dk.length !== 256) {
498
+ throw new WebCryptoError("webcrypto/syntax", "deriveKey: " + dk.name + " length must be 128/192/256");
499
+ }
500
+ bits = dk.length;
501
+ } else if (dk.name === "HMAC") {
502
+ // The same W3C HMAC get-key-length rule as generateKey: an omitted
503
+ // length is the hash's BLOCK size, never a fixed 256.
504
+ bits = _hmacLengthBits(dk, "deriveKey");
505
+ } else {
506
+ // HKDF / PBKDF2 derived-key types carry no intrinsic size (W3C
507
+ // get-key-length is null): the base derivation decides -- a key
508
+ // agreement yields its full shared secret as the input keying
509
+ // material; a KDF base has no implicit output size and fails closed.
510
+ bits = dk.length != null ? dk.length : null;
511
+ }
393
512
  var raw = _deriveBitsRaw(alg, baseKey, bits);
394
513
  return this.importKey("raw", raw, dk, extractable, keyUsages);
395
514
  };
@@ -399,6 +518,8 @@ SubtleCrypto.prototype.wrapKey = async function wrapKey(format, key, wrappingKey
399
518
  var bytes = (format === "jwk") ? Buffer.from(JSON.stringify(exported)) : Buffer.from(exported);
400
519
  var alg = _normalizeAlg(wrapAlgorithm, "wrapKey");
401
520
  _requireUsage(wrappingKey, "wrapKey");
521
+ if (alg.name !== "AES-KW" && !ENCRYPT_DECRYPT_NAMES[alg.name]) throw new WebCryptoError("webcrypto/not-supported", "wrapKey: unsupported algorithm " + JSON.stringify(alg.name));
522
+ _requireAlgMatch(alg, wrappingKey, "wrapKey");
402
523
  if (alg.name === "AES-KW") {
403
524
  var c = nodeCrypto.createCipheriv("aes" + wrappingKey.algorithm.length + "-wrap", _secretBytes(wrappingKey), Buffer.from("A6A6A6A6A6A6A6A6", "hex"));
404
525
  return _toArrayBuffer(Buffer.concat([c.update(bytes), c.final()]));
@@ -416,6 +537,8 @@ SubtleCrypto.prototype.unwrapKey = async function unwrapKey(format, wrappedKey,
416
537
  // never verify the caller was actually permitted to unwrap (mirrors
417
538
  // wrapKey, which checks before both of its paths).
418
539
  _requireUsage(unwrappingKey, "unwrapKey");
540
+ if (alg.name !== "AES-KW" && !ENCRYPT_DECRYPT_NAMES[alg.name]) throw new WebCryptoError("webcrypto/not-supported", "unwrapKey: unsupported algorithm " + JSON.stringify(alg.name));
541
+ _requireAlgMatch(alg, unwrappingKey, "unwrapKey");
419
542
  var bytes;
420
543
  if (alg.name === "AES-KW") {
421
544
  var d = nodeCrypto.createDecipheriv("aes" + unwrappingKey.algorithm.length + "-wrap", _secretBytes(unwrappingKey), Buffer.from("A6A6A6A6A6A6A6A6", "hex"));
@@ -424,7 +547,17 @@ SubtleCrypto.prototype.unwrapKey = async function unwrapKey(format, wrappedKey,
424
547
  var unwrapKeyClone = _cloneWithUsage(unwrappingKey, "decrypt");
425
548
  bytes = Buffer.from(await this.decrypt(unwrapAlgorithm, unwrapKeyClone, wrappedKey));
426
549
  }
427
- var keyData = (format === "jwk") ? JSON.parse(bytes.toString()) : bytes;
550
+ var keyData;
551
+ if (format === "jwk") {
552
+ // A NON-authenticating unwrap algorithm (AES-CBC / AES-CTR) decrypts
553
+ // tampered bytes "successfully", so this JSON.parse is the first point
554
+ // that can notice -- its failure must surface as the module's typed
555
+ // error (W3C: DataError), never a raw SyntaxError from the public API.
556
+ try { keyData = JSON.parse(bytes.toString()); }
557
+ catch (e) { throw new WebCryptoError("webcrypto/data", "unwrapKey: unwrapped bytes are not a valid JWK", e); }
558
+ } else {
559
+ keyData = bytes;
560
+ }
428
561
  return this.importKey(format, keyData, unwrappedKeyAlgorithm, extractable, keyUsages);
429
562
  };
430
563
 
@@ -477,7 +610,9 @@ function _importRawPublic(name, alg, raw, extractable, usages) {
477
610
  if (name === "ED25519" || name === "ED448" || name === "X25519" || name === "X448") {
478
611
  var jwk = { kty: "OKP", crv: (name === "ED25519" ? "Ed25519" : name === "ED448" ? "Ed448" : name === "X25519" ? "X25519" : "X448"), x: _bufToB64url(raw) };
479
612
  var ko = nodeCrypto.createPublicKey({ key: jwk, format: "jwk" });
480
- return new CryptoKey("public", true, { name: alg.name === "ED25519" ? "Ed25519" : alg.name }, usages, ko);
613
+ // The canonical mixed-case EdDSA labels, matching _algFromImport -- a raw
614
+ // import must not label the same key differently than an spki/jwk one.
615
+ return new CryptoKey("public", true, { name: name === "ED25519" ? "Ed25519" : name === "ED448" ? "Ed448" : alg.name }, usages, ko);
481
616
  }
482
617
  if (name === "ECDSA" || name === "ECDH") {
483
618
  var fb = CURVE_FIELD_BYTES[alg.namedCurve];
@@ -491,7 +626,7 @@ function _importRawPublic(name, alg, raw, extractable, usages) {
491
626
 
492
627
  function _algFromImport(name, alg, keyObject) {
493
628
  if (name === "ECDSA" || name === "ECDH") {
494
- // W3C WebCrypto EC import the curve is a property of the KEY, not the
629
+ // W3C WebCrypto EC import -- the curve is a property of the KEY, not the
495
630
  // caller's claim. Derive it from the imported key material; reject an
496
631
  // unsupported curve (NotSupportedError, matching generateKey) and reject a
497
632
  // requested namedCurve that disagrees with the key (DataError). Trusting
@@ -523,7 +658,7 @@ function _curveFromKey(ko) {
523
658
  * @signature await pki.webcrypto.subtle.exportKey(format, key)
524
659
  * @since 0.1.0
525
660
  * @status stable
526
- * @spec W3C WebCrypto §subtlecrypto, FIPS 186-5, FIPS 203, FIPS 204, FIPS 205, RFC 8017
661
+ * @spec W3C WebCrypto sec. subtlecrypto, FIPS 186-5, FIPS 203, FIPS 204, FIPS 205, RFC 8017
527
662
  * @related pki.webcrypto.CryptoKey
528
663
  *
529
664
  * Export a `CryptoKey` to `spki` (public), `pkcs8` (private), `jwk`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.1.22",
3
+ "version": "0.1.24",
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",
@@ -69,6 +69,7 @@
69
69
  "check:vendor-currency": "node scripts/check-vendor-currency.js"
70
70
  },
71
71
  "devDependencies": {
72
- "esbuild": "0.28.1"
72
+ "esbuild": "0.28.1",
73
+ "eslint": "10.3.0"
73
74
  }
74
75
  }
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:38c53855-3eaa-4d1a-8c48-b03540881d8a",
5
+ "serialNumber": "urn:uuid:dfb00f98-2ca7-4fd1-b0e7-90fccf92604a",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-09T18:46:47.249Z",
8
+ "timestamp": "2026-07-10T12:57:51.582Z",
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.22",
22
+ "bom-ref": "@blamejs/pki@0.1.24",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.1.22",
25
+ "version": "0.1.24",
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.22",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.1.24",
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.22",
57
+ "ref": "@blamejs/pki@0.1.24",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]