@blamejs/pki 0.1.22 → 0.1.23

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
  }
@@ -282,6 +352,8 @@ SubtleCrypto.prototype.encrypt = async function encrypt(algorithm, key, data) {
282
352
  _requireUsage(key, "encrypt");
283
353
  var buf = _toBuf(data, "encrypt");
284
354
  var name = alg.name;
355
+ if (!ENCRYPT_DECRYPT_NAMES[name]) throw new WebCryptoError("webcrypto/not-supported", "encrypt: unsupported algorithm " + JSON.stringify(name));
356
+ _requireAlgMatch(alg, key, "encrypt");
285
357
  if (name === "RSA-OAEP") {
286
358
  return _toArrayBuffer(nodeCrypto.publicEncrypt({
287
359
  key: key._handle, padding: nodeCrypto.constants.RSA_PKCS1_OAEP_PADDING,
@@ -313,6 +385,8 @@ SubtleCrypto.prototype.decrypt = async function decrypt(algorithm, key, data) {
313
385
  _requireUsage(key, "decrypt");
314
386
  var buf = _toBuf(data, "decrypt");
315
387
  var name = alg.name;
388
+ if (!ENCRYPT_DECRYPT_NAMES[name]) throw new WebCryptoError("webcrypto/not-supported", "decrypt: unsupported algorithm " + JSON.stringify(name));
389
+ _requireAlgMatch(alg, key, "decrypt");
316
390
  if (name === "RSA-OAEP") {
317
391
  return _toArrayBuffer(nodeCrypto.privateDecrypt({
318
392
  key: key._handle, padding: nodeCrypto.constants.RSA_PKCS1_OAEP_PADDING,
@@ -346,7 +420,7 @@ function _secretBytes(key) { return key._handle.export(); }
346
420
 
347
421
  // AES-CTR: node always treats the full 128-bit block as the counter and
348
422
  // never reads the spec's `length` (counter-width) parameter. A length < 128
349
- // would silently diverge from the W3C definition, so fail closed accept
423
+ // would silently diverge from the W3C definition, so fail closed -- accept
350
424
  // only the one value node can honor.
351
425
  function _requireCtrLength128(alg) {
352
426
  if (alg.length !== 128) {
@@ -354,21 +428,41 @@ function _requireCtrLength128(alg) {
354
428
  }
355
429
  }
356
430
 
357
- // Raw key-agreement / KDF derivation with NO usage check the usage a
431
+ // W3C deriveBits: the requested length must be a positive multiple of 8 --
432
+ // the `length / 8` narrowing in the per-algorithm branches below would
433
+ // otherwise silently truncate a fractional byte count.
434
+ function _requireDeriveLength(length, who) {
435
+ if (typeof length !== "number" || !isFinite(length) || length <= 0 || length % 8 !== 0) {
436
+ throw new WebCryptoError("webcrypto/operation", who + ": length must be a positive multiple of 8 bits");
437
+ }
438
+ }
439
+
440
+ // Raw key-agreement / KDF derivation with NO usage check -- the usage a
358
441
  // caller must hold differs by entry point (deriveBits requires "deriveBits",
359
442
  // deriveKey requires "deriveKey"), so each public method checks its own
360
443
  // usage and then routes the actual derivation through here.
361
444
  function _deriveBitsRaw(alg, key, length) {
362
445
  var name = alg.name;
363
446
  if (name === "ECDH" || name === "X25519" || name === "X448") {
447
+ _requireAlgMatch(alg, alg.public, name + " public key");
364
448
  var secret = nodeCrypto.diffieHellman({ privateKey: key._handle, publicKey: alg.public._handle });
365
- return _toArrayBuffer(length == null ? secret : secret.subarray(0, length / 8));
449
+ if (length == null) return _toArrayBuffer(secret);
450
+ _requireDeriveLength(length, name);
451
+ // subarray clamps at the end of the secret, so an unchecked over-request
452
+ // would silently return fewer bytes than asked. W3C deriveBits: throw an
453
+ // OperationError when the requested length cannot be satisfied.
454
+ if (length / 8 > secret.length) {
455
+ throw new WebCryptoError("webcrypto/operation", name + ": requested " + length + " bits but the shared secret has " + (secret.length * 8));
456
+ }
457
+ return _toArrayBuffer(secret.subarray(0, length / 8));
366
458
  }
367
459
  if (name === "HKDF") {
460
+ _requireDeriveLength(length, "HKDF");
368
461
  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
462
  return derived instanceof ArrayBuffer ? derived : _toArrayBuffer(Buffer.from(derived));
370
463
  }
371
464
  if (name === "PBKDF2") {
465
+ _requireDeriveLength(length, "PBKDF2");
372
466
  var out = nodeCrypto.pbkdf2Sync(_secretBytes(key), _toBuf(alg.salt, "PBKDF2 salt"), alg.iterations, length / 8, _hashNode(alg.hash, "PBKDF2"));
373
467
  return _toArrayBuffer(out);
374
468
  }
@@ -378,18 +472,42 @@ function _deriveBitsRaw(alg, key, length) {
378
472
  SubtleCrypto.prototype.deriveBits = async function deriveBits(algorithm, key, length) {
379
473
  var alg = _normalizeAlg(algorithm, "deriveBits");
380
474
  _requireUsage(key, "deriveBits");
475
+ if (!DERIVE_NAMES[alg.name]) throw new WebCryptoError("webcrypto/not-supported", "deriveBits: unsupported algorithm " + JSON.stringify(alg.name));
476
+ _requireAlgMatch(alg, key, "deriveBits");
381
477
  return _deriveBitsRaw(alg, key, length);
382
478
  };
383
479
 
384
480
  SubtleCrypto.prototype.deriveKey = async function deriveKey(algorithm, baseKey, derivedKeyType, extractable, keyUsages) {
385
- // deriveKey requires the "deriveKey" usage NOT "deriveBits". Delegating
481
+ // deriveKey requires the "deriveKey" usage -- NOT "deriveBits". Delegating
386
482
  // to this.deriveBits would false-reject a key created with ["deriveKey"]
387
483
  // and fail-open on a ["deriveBits"]-only key, so check the correct usage
388
484
  // here and route the raw derivation past deriveBits' own usage gate.
389
485
  _requireUsage(baseKey, "deriveKey");
390
486
  var alg = _normalizeAlg(algorithm, "deriveKey");
487
+ if (!DERIVE_NAMES[alg.name]) throw new WebCryptoError("webcrypto/not-supported", "deriveKey: unsupported algorithm " + JSON.stringify(alg.name));
488
+ _requireAlgMatch(alg, baseKey, "deriveKey");
391
489
  var dk = _normalizeAlg(derivedKeyType, "deriveKey");
392
- var bits = dk.length || (dk.name.indexOf("AES") === 0 ? dk.length : 256);
490
+ var bits;
491
+ if (dk.name.indexOf("AES") === 0) {
492
+ // W3C get-key-length for AES: the derivedKeyType MUST carry a length of
493
+ // 128/192/256. Without this check the derived key would silently take
494
+ // the size of the raw agreement/KDF output (e.g. a 384-bit "AES" key
495
+ // from a P-384 secret) instead of a usable AES size.
496
+ if (dk.length !== 128 && dk.length !== 192 && dk.length !== 256) {
497
+ throw new WebCryptoError("webcrypto/syntax", "deriveKey: " + dk.name + " length must be 128/192/256");
498
+ }
499
+ bits = dk.length;
500
+ } else if (dk.name === "HMAC") {
501
+ // The same W3C HMAC get-key-length rule as generateKey: an omitted
502
+ // length is the hash's BLOCK size, never a fixed 256.
503
+ bits = _hmacLengthBits(dk, "deriveKey");
504
+ } else {
505
+ // HKDF / PBKDF2 derived-key types carry no intrinsic size (W3C
506
+ // get-key-length is null): the base derivation decides -- a key
507
+ // agreement yields its full shared secret as the input keying
508
+ // material; a KDF base has no implicit output size and fails closed.
509
+ bits = dk.length != null ? dk.length : null;
510
+ }
393
511
  var raw = _deriveBitsRaw(alg, baseKey, bits);
394
512
  return this.importKey("raw", raw, dk, extractable, keyUsages);
395
513
  };
@@ -399,6 +517,8 @@ SubtleCrypto.prototype.wrapKey = async function wrapKey(format, key, wrappingKey
399
517
  var bytes = (format === "jwk") ? Buffer.from(JSON.stringify(exported)) : Buffer.from(exported);
400
518
  var alg = _normalizeAlg(wrapAlgorithm, "wrapKey");
401
519
  _requireUsage(wrappingKey, "wrapKey");
520
+ if (alg.name !== "AES-KW" && !ENCRYPT_DECRYPT_NAMES[alg.name]) throw new WebCryptoError("webcrypto/not-supported", "wrapKey: unsupported algorithm " + JSON.stringify(alg.name));
521
+ _requireAlgMatch(alg, wrappingKey, "wrapKey");
402
522
  if (alg.name === "AES-KW") {
403
523
  var c = nodeCrypto.createCipheriv("aes" + wrappingKey.algorithm.length + "-wrap", _secretBytes(wrappingKey), Buffer.from("A6A6A6A6A6A6A6A6", "hex"));
404
524
  return _toArrayBuffer(Buffer.concat([c.update(bytes), c.final()]));
@@ -416,6 +536,8 @@ SubtleCrypto.prototype.unwrapKey = async function unwrapKey(format, wrappedKey,
416
536
  // never verify the caller was actually permitted to unwrap (mirrors
417
537
  // wrapKey, which checks before both of its paths).
418
538
  _requireUsage(unwrappingKey, "unwrapKey");
539
+ if (alg.name !== "AES-KW" && !ENCRYPT_DECRYPT_NAMES[alg.name]) throw new WebCryptoError("webcrypto/not-supported", "unwrapKey: unsupported algorithm " + JSON.stringify(alg.name));
540
+ _requireAlgMatch(alg, unwrappingKey, "unwrapKey");
419
541
  var bytes;
420
542
  if (alg.name === "AES-KW") {
421
543
  var d = nodeCrypto.createDecipheriv("aes" + unwrappingKey.algorithm.length + "-wrap", _secretBytes(unwrappingKey), Buffer.from("A6A6A6A6A6A6A6A6", "hex"));
@@ -424,7 +546,17 @@ SubtleCrypto.prototype.unwrapKey = async function unwrapKey(format, wrappedKey,
424
546
  var unwrapKeyClone = _cloneWithUsage(unwrappingKey, "decrypt");
425
547
  bytes = Buffer.from(await this.decrypt(unwrapAlgorithm, unwrapKeyClone, wrappedKey));
426
548
  }
427
- var keyData = (format === "jwk") ? JSON.parse(bytes.toString()) : bytes;
549
+ var keyData;
550
+ if (format === "jwk") {
551
+ // A NON-authenticating unwrap algorithm (AES-CBC / AES-CTR) decrypts
552
+ // tampered bytes "successfully", so this JSON.parse is the first point
553
+ // that can notice -- its failure must surface as the module's typed
554
+ // error (W3C: DataError), never a raw SyntaxError from the public API.
555
+ try { keyData = JSON.parse(bytes.toString()); }
556
+ catch (e) { throw new WebCryptoError("webcrypto/data", "unwrapKey: unwrapped bytes are not a valid JWK", e); }
557
+ } else {
558
+ keyData = bytes;
559
+ }
428
560
  return this.importKey(format, keyData, unwrappedKeyAlgorithm, extractable, keyUsages);
429
561
  };
430
562
 
@@ -477,7 +609,9 @@ function _importRawPublic(name, alg, raw, extractable, usages) {
477
609
  if (name === "ED25519" || name === "ED448" || name === "X25519" || name === "X448") {
478
610
  var jwk = { kty: "OKP", crv: (name === "ED25519" ? "Ed25519" : name === "ED448" ? "Ed448" : name === "X25519" ? "X25519" : "X448"), x: _bufToB64url(raw) };
479
611
  var ko = nodeCrypto.createPublicKey({ key: jwk, format: "jwk" });
480
- return new CryptoKey("public", true, { name: alg.name === "ED25519" ? "Ed25519" : alg.name }, usages, ko);
612
+ // The canonical mixed-case EdDSA labels, matching _algFromImport -- a raw
613
+ // import must not label the same key differently than an spki/jwk one.
614
+ return new CryptoKey("public", true, { name: name === "ED25519" ? "Ed25519" : name === "ED448" ? "Ed448" : alg.name }, usages, ko);
481
615
  }
482
616
  if (name === "ECDSA" || name === "ECDH") {
483
617
  var fb = CURVE_FIELD_BYTES[alg.namedCurve];
@@ -491,7 +625,7 @@ function _importRawPublic(name, alg, raw, extractable, usages) {
491
625
 
492
626
  function _algFromImport(name, alg, keyObject) {
493
627
  if (name === "ECDSA" || name === "ECDH") {
494
- // W3C WebCrypto EC import the curve is a property of the KEY, not the
628
+ // W3C WebCrypto EC import -- the curve is a property of the KEY, not the
495
629
  // caller's claim. Derive it from the imported key material; reject an
496
630
  // unsupported curve (NotSupportedError, matching generateKey) and reject a
497
631
  // requested namedCurve that disagrees with the key (DataError). Trusting
@@ -523,7 +657,7 @@ function _curveFromKey(ko) {
523
657
  * @signature await pki.webcrypto.subtle.exportKey(format, key)
524
658
  * @since 0.1.0
525
659
  * @status stable
526
- * @spec W3C WebCrypto §subtlecrypto, FIPS 186-5, FIPS 203, FIPS 204, FIPS 205, RFC 8017
660
+ * @spec W3C WebCrypto sec. subtlecrypto, FIPS 186-5, FIPS 203, FIPS 204, FIPS 205, RFC 8017
527
661
  * @related pki.webcrypto.CryptoKey
528
662
  *
529
663
  * 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.23",
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:a62885e0-295d-4f1c-aa93-536269dabd5b",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-09T18:46:47.249Z",
8
+ "timestamp": "2026-07-10T06:16:22.938Z",
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.23",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.1.22",
25
+ "version": "0.1.23",
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.23",
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.23",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]