@blamejs/pki 0.2.6 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,7 +4,20 @@ 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.2.6 — 2026-07-13
7
+ ## v0.2.7 — 2026-07-13
8
+
9
+ pki.webcrypto rejects an imported key whose type disagrees with its algorithm, and reports every cipher fault as a typed error.
10
+
11
+ ### Changed
12
+
13
+ - The pkijs.com documentation site is regenerated with content-hashed CSS/JS under a strict Content-Security-Policy, an in-memory search endpoint, a browsable reference of every error class and code, symbol autocomplete, and concept guides. Documentation only -- the published package is unchanged.
14
+
15
+ ### Fixed
16
+
17
+ - pki.webcrypto.subtle.importKey now validates that an imported asymmetric key's actual type matches the requested algorithm (an RSA key imported under an Ed25519, ECDSA, or RSA-PSS name is rejected as webcrypto/data), closing an algorithm-confusion path where a mislabeled CryptoKey could later be used under the wrong signature scheme. The EC import path already derived and checked the curve; this extends the same key-is-authority rule to RSA and the Edwards/Montgomery curves.
18
+ - pki.webcrypto AES cipher faults now fail closed with a typed webcrypto/operation error instead of a raw Node exception: a decrypt of a tampered AES-GCM ciphertext (failed authentication tag), bad AES-CBC padding, a non-8-byte-multiple AES-KW wrap/unwrap length, and a malformed cipher parameter all surface as a WebCryptoError, so a caller catching pki.errors.PkiError sees a typed verdict rather than a bare Node error crossing the API boundary.
19
+
20
+ ## v0.2.6 — 2026-07-12
8
21
 
9
22
  WebAuthn attestation verification covers Ed448 and the RFC 9864 fully-specified COSE algorithms, and hardens credential-key conformance.
10
23
 
@@ -1463,7 +1463,7 @@ function crlIssuerNamesIssuer(cRLIssuer, issuerRdns) {
1463
1463
  // dnEqual throws only on a control-byte / malformed cRLIssuer DN; returning false excludes
1464
1464
  // that DP from the correspondence (a malformed indirect-CRL issuer name never corresponds).
1465
1465
  try { if (dnEqual(n.value.rdns, issuerRdns)) return true; }
1466
- catch (_e) { return false; } // allow:swallow-unverified fail-closed predicate: error -> no match -> DP excluded (restrictive)
1466
+ catch (_e) { return false; }
1467
1467
  }
1468
1468
  return false;
1469
1469
  }
package/lib/webcrypto.js CHANGED
@@ -353,6 +353,16 @@ SubtleCrypto.prototype.verify = async function verify(algorithm, key, signature,
353
353
  throw new WebCryptoError("webcrypto/not-supported", "verify: unsupported algorithm " + JSON.stringify(name));
354
354
  };
355
355
 
356
+ // Run a node:crypto AES cipher sequence, converting any raw node fault -- a bad IV length,
357
+ // a failed GCM authentication tag, bad CBC padding -- into a typed OperationError. A
358
+ // decrypt of tampered ciphertext (or a malformed cipher parameter) is a typed webcrypto
359
+ // verdict, never a raw Error crossing the public API (input prep that already throws a
360
+ // typed error runs OUTSIDE this, so a typed fault is never double-wrapped).
361
+ function _runCipher(fn, who) {
362
+ try { return fn(); }
363
+ catch (e) { throw new WebCryptoError("webcrypto/operation", who + ": AES cipher operation failed (bad key / iv / tag / padding)", e); }
364
+ }
365
+
356
366
  SubtleCrypto.prototype.encrypt = async function encrypt(algorithm, key, data) {
357
367
  var alg = _normalizeAlg(algorithm, "encrypt");
358
368
  _requireUsage(key, "encrypt");
@@ -369,19 +379,28 @@ SubtleCrypto.prototype.encrypt = async function encrypt(algorithm, key, data) {
369
379
  }
370
380
  if (name === "AES-GCM") {
371
381
  var iv = _toBuf(alg.iv, "AES-GCM iv");
372
- var cipher = nodeCrypto.createCipheriv("aes-" + key.algorithm.length + "-gcm", _secretBytes(key), iv, { authTagLength: (alg.tagLength || 128) / 8 });
373
- if (alg.additionalData) cipher.setAAD(_toBuf(alg.additionalData, "AES-GCM aad"));
374
- var ct = Buffer.concat([cipher.update(buf), cipher.final()]);
375
- return _toArrayBuffer(Buffer.concat([ct, cipher.getAuthTag()]));
382
+ var aad = alg.additionalData ? _toBuf(alg.additionalData, "AES-GCM aad") : null;
383
+ return _toArrayBuffer(_runCipher(function () {
384
+ var cipher = nodeCrypto.createCipheriv("aes-" + key.algorithm.length + "-gcm", _secretBytes(key), iv, { authTagLength: (alg.tagLength || 128) / 8 });
385
+ if (aad) cipher.setAAD(aad);
386
+ var ct = Buffer.concat([cipher.update(buf), cipher.final()]);
387
+ return Buffer.concat([ct, cipher.getAuthTag()]);
388
+ }, "encrypt"));
376
389
  }
377
390
  if (name === "AES-CBC") {
378
- var c2 = nodeCrypto.createCipheriv("aes-" + key.algorithm.length + "-cbc", _secretBytes(key), _toBuf(alg.iv, "AES-CBC iv"));
379
- return _toArrayBuffer(Buffer.concat([c2.update(buf), c2.final()]));
391
+ var cbcIv = _toBuf(alg.iv, "AES-CBC iv");
392
+ return _toArrayBuffer(_runCipher(function () {
393
+ var c2 = nodeCrypto.createCipheriv("aes-" + key.algorithm.length + "-cbc", _secretBytes(key), cbcIv);
394
+ return Buffer.concat([c2.update(buf), c2.final()]);
395
+ }, "encrypt"));
380
396
  }
381
397
  if (name === "AES-CTR") {
382
398
  _requireCtrLength128(alg);
383
- var c3 = nodeCrypto.createCipheriv("aes-" + key.algorithm.length + "-ctr", _secretBytes(key), _toBuf(alg.counter, "AES-CTR counter"));
384
- return _toArrayBuffer(Buffer.concat([c3.update(buf), c3.final()]));
399
+ var ctrCounter = _toBuf(alg.counter, "AES-CTR counter");
400
+ return _toArrayBuffer(_runCipher(function () {
401
+ var c3 = nodeCrypto.createCipheriv("aes-" + key.algorithm.length + "-ctr", _secretBytes(key), ctrCounter);
402
+ return Buffer.concat([c3.update(buf), c3.final()]);
403
+ }, "encrypt"));
385
404
  }
386
405
  throw new WebCryptoError("webcrypto/not-supported", "encrypt: unsupported algorithm " + JSON.stringify(name));
387
406
  };
@@ -403,21 +422,31 @@ SubtleCrypto.prototype.decrypt = async function decrypt(algorithm, key, data) {
403
422
  if (name === "AES-GCM") {
404
423
  var tagLen = (alg.tagLength || 128) / 8;
405
424
  var iv = _toBuf(alg.iv, "AES-GCM iv");
425
+ if (buf.length < tagLen) throw new WebCryptoError("webcrypto/operation", "decrypt: AES-GCM ciphertext is shorter than its authentication tag");
406
426
  var ct = buf.subarray(0, buf.length - tagLen);
407
427
  var tag = buf.subarray(buf.length - tagLen);
408
- var d = nodeCrypto.createDecipheriv("aes-" + key.algorithm.length + "-gcm", _secretBytes(key), iv, { authTagLength: tagLen });
409
- if (alg.additionalData) d.setAAD(_toBuf(alg.additionalData, "AES-GCM aad"));
410
- d.setAuthTag(tag);
411
- return _toArrayBuffer(Buffer.concat([d.update(ct), d.final()]));
428
+ var gcmAad = alg.additionalData ? _toBuf(alg.additionalData, "AES-GCM aad") : null;
429
+ return _toArrayBuffer(_runCipher(function () {
430
+ var d = nodeCrypto.createDecipheriv("aes-" + key.algorithm.length + "-gcm", _secretBytes(key), iv, { authTagLength: tagLen });
431
+ if (gcmAad) d.setAAD(gcmAad);
432
+ d.setAuthTag(tag);
433
+ return Buffer.concat([d.update(ct), d.final()]);
434
+ }, "decrypt"));
412
435
  }
413
436
  if (name === "AES-CBC") {
414
- var d2 = nodeCrypto.createDecipheriv("aes-" + key.algorithm.length + "-cbc", _secretBytes(key), _toBuf(alg.iv, "AES-CBC iv"));
415
- return _toArrayBuffer(Buffer.concat([d2.update(buf), d2.final()]));
437
+ var cbcIv2 = _toBuf(alg.iv, "AES-CBC iv");
438
+ return _toArrayBuffer(_runCipher(function () {
439
+ var d2 = nodeCrypto.createDecipheriv("aes-" + key.algorithm.length + "-cbc", _secretBytes(key), cbcIv2);
440
+ return Buffer.concat([d2.update(buf), d2.final()]);
441
+ }, "decrypt"));
416
442
  }
417
443
  if (name === "AES-CTR") {
418
444
  _requireCtrLength128(alg);
419
- var d3 = nodeCrypto.createDecipheriv("aes-" + key.algorithm.length + "-ctr", _secretBytes(key), _toBuf(alg.counter, "AES-CTR counter"));
420
- return _toArrayBuffer(Buffer.concat([d3.update(buf), d3.final()]));
445
+ var ctrCounter2 = _toBuf(alg.counter, "AES-CTR counter");
446
+ return _toArrayBuffer(_runCipher(function () {
447
+ var d3 = nodeCrypto.createDecipheriv("aes-" + key.algorithm.length + "-ctr", _secretBytes(key), ctrCounter2);
448
+ return Buffer.concat([d3.update(buf), d3.final()]);
449
+ }, "decrypt"));
421
450
  }
422
451
  throw new WebCryptoError("webcrypto/not-supported", "decrypt: unsupported algorithm " + JSON.stringify(name));
423
452
  };
@@ -526,8 +555,16 @@ SubtleCrypto.prototype.wrapKey = async function wrapKey(format, key, wrappingKey
526
555
  if (alg.name !== "AES-KW" && !ENCRYPT_DECRYPT_NAMES[alg.name]) throw new WebCryptoError("webcrypto/not-supported", "wrapKey: unsupported algorithm " + JSON.stringify(alg.name));
527
556
  _requireAlgMatch(alg, wrappingKey, "wrapKey");
528
557
  if (alg.name === "AES-KW") {
529
- var c = nodeCrypto.createCipheriv("aes" + wrappingKey.algorithm.length + "-wrap", _secretBytes(wrappingKey), Buffer.from("A6A6A6A6A6A6A6A6", "hex"));
530
- return _toArrayBuffer(Buffer.concat([c.update(bytes), c.final()]));
558
+ // AES-KW wraps 64-bit blocks (RFC 3394): the serialized key MUST be a multiple of 8
559
+ // bytes and at least 16. A "jwk" serialization (arbitrary-length JSON) or any other
560
+ // non-conforming length is an OperationError -- a typed verdict, never a raw node fault.
561
+ if (bytes.length < 16 || bytes.length % 8 !== 0) {
562
+ throw new WebCryptoError("webcrypto/operation", "wrapKey: AES-KW requires the serialized key be a multiple of 8 bytes (>= 16); got " + bytes.length + " -- format " + JSON.stringify(format) + " is not AES-KW-wrappable");
563
+ }
564
+ try {
565
+ var c = nodeCrypto.createCipheriv("aes" + wrappingKey.algorithm.length + "-wrap", _secretBytes(wrappingKey), Buffer.from("A6A6A6A6A6A6A6A6", "hex"));
566
+ return _toArrayBuffer(Buffer.concat([c.update(bytes), c.final()]));
567
+ } catch (e) { throw new WebCryptoError("webcrypto/operation", "wrapKey: AES-KW key wrap failed", e); }
531
568
  }
532
569
  // Delegate to a content-encryption algorithm (RSA-OAEP / AES-GCM).
533
570
  var wrapKeyClone = _cloneWithUsage(wrappingKey, "encrypt");
@@ -546,8 +583,17 @@ SubtleCrypto.prototype.unwrapKey = async function unwrapKey(format, wrappedKey,
546
583
  _requireAlgMatch(alg, unwrappingKey, "unwrapKey");
547
584
  var bytes;
548
585
  if (alg.name === "AES-KW") {
549
- var d = nodeCrypto.createDecipheriv("aes" + unwrappingKey.algorithm.length + "-wrap", _secretBytes(unwrappingKey), Buffer.from("A6A6A6A6A6A6A6A6", "hex"));
550
- bytes = Buffer.concat([d.update(_toBuf(wrappedKey, "unwrapKey")), d.final()]);
586
+ var wrapped = _toBuf(wrappedKey, "unwrapKey");
587
+ // The wrapped input is the plaintext plus one 64-bit integrity block: a multiple of 8,
588
+ // at least 24. A wrong length, or a failed integrity check inside final(), is an
589
+ // OperationError -- a typed verdict, never a raw node cipher fault.
590
+ if (wrapped.length < 24 || wrapped.length % 8 !== 0) {
591
+ throw new WebCryptoError("webcrypto/operation", "unwrapKey: AES-KW wrapped key must be a multiple of 8 bytes (>= 24); got " + wrapped.length);
592
+ }
593
+ try {
594
+ var d = nodeCrypto.createDecipheriv("aes" + unwrappingKey.algorithm.length + "-wrap", _secretBytes(unwrappingKey), Buffer.from("A6A6A6A6A6A6A6A6", "hex"));
595
+ bytes = Buffer.concat([d.update(wrapped), d.final()]);
596
+ } catch (e) { throw new WebCryptoError("webcrypto/operation", "unwrapKey: AES-KW key unwrap failed (integrity or length)", e); }
551
597
  } else {
552
598
  var unwrapKeyClone = _cloneWithUsage(unwrappingKey, "decrypt");
553
599
  bytes = Buffer.from(await this.decrypt(unwrapAlgorithm, unwrapKeyClone, wrappedKey));
@@ -633,7 +679,28 @@ function _importRawPublic(name, alg, raw, extractable, usages) {
633
679
  throw new WebCryptoError("webcrypto/not-supported", "importKey raw: unsupported public-key algorithm " + JSON.stringify(name));
634
680
  }
635
681
 
682
+ // The node asymmetricKeyType(s) each imported asymmetric algorithm requires. The key TYPE
683
+ // is a property of the key material, not the caller's claim: an RSA key imported under
684
+ // {name:"Ed25519"} must be a DataError, not a CryptoKey mislabeled Ed25519 that then signs
685
+ // under the wrong scheme (algorithm confusion). EC is additionally curve-validated below.
686
+ var IMPORT_KEY_TYPE = {
687
+ "RSASSA-PKCS1-V1_5": { rsa: 1 }, "RSA-PSS": { rsa: 1, "rsa-pss": 1 }, "RSA-OAEP": { rsa: 1 },
688
+ "ECDSA": { ec: 1 }, "ECDH": { ec: 1 },
689
+ "ED25519": { ed25519: 1 }, "ED448": { ed448: 1 },
690
+ "X25519": { x25519: 1 }, "X448": { x448: 1 },
691
+ };
692
+ // The FIPS 203/204/205 PQC families import the same key-is-authority rule: each WebCrypto
693
+ // algorithm name maps to exactly its node asymmetricKeyType, so an RSA (or any) key imported
694
+ // under an ML-DSA / ML-KEM / SLH-DSA name is a DataError, not a mislabeled PQC CryptoKey.
695
+ [ML_DSA_NODE, ML_KEM_NODE, SLH_DSA_NODE].forEach(function (m) {
696
+ Object.keys(m).forEach(function (webName) { var t = {}; t[m[webName]] = 1; IMPORT_KEY_TYPE[webName] = t; });
697
+ });
636
698
  function _algFromImport(name, alg, keyObject) {
699
+ var wantType = IMPORT_KEY_TYPE[name];
700
+ if (wantType && keyObject && keyObject.asymmetricKeyType && !wantType[keyObject.asymmetricKeyType]) {
701
+ throw new WebCryptoError("webcrypto/data", name + ": the imported key is a " + keyObject.asymmetricKeyType +
702
+ " key, which is not compatible with algorithm " + name);
703
+ }
637
704
  if (name === "ECDSA" || name === "ECDH") {
638
705
  // W3C WebCrypto EC import -- the curve is a property of the KEY, not the
639
706
  // caller's claim. Derive it from the imported key material; reject an
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:a602dc26-1f23-43cd-ad30-ef4f457ccc22",
5
+ "serialNumber": "urn:uuid:ea60e88f-9354-49c4-aa35-ad6712102de1",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-13T05:42:29.445Z",
8
+ "timestamp": "2026-07-13T07:29:25.087Z",
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.2.6",
22
+ "bom-ref": "@blamejs/pki@0.2.7",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.2.6",
25
+ "version": "0.2.7",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
29
- "purl": "pkg:npm/%40blamejs/pki@0.2.6",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.2.7",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/pki@0.2.6",
57
+ "ref": "@blamejs/pki@0.2.7",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]