@blamejs/pki 0.2.6 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,7 +4,29 @@ 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.8 — 2026-07-13
8
+
9
+ pki.webcrypto rejects an AES key of invalid length at import instead of deferring the failure to first use.
10
+
11
+ ### Fixed
12
+
13
+ - pki.webcrypto.subtle.importKey now rejects a raw or JWK AES key whose length is not 128, 192, or 256 bits as a webcrypto/data DataError at import, rather than returning a CryptoKey that only fails at first use. Covers AES-GCM, AES-CBC, AES-CTR, and AES-KW; HMAC, HKDF, and PBKDF2 keys are unaffected.
14
+ - The README capability table and the documentation site render the certificate-inspection entry correctly: an inline code span containing pipe characters no longer breaks the surrounding table into misaligned columns.
15
+
16
+ ## v0.2.7 — 2026-07-13
17
+
18
+ pki.webcrypto rejects an imported key whose type disagrees with its algorithm, and reports every cipher fault as a typed error.
19
+
20
+ ### Changed
21
+
22
+ - 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.
23
+
24
+ ### Fixed
25
+
26
+ - 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.
27
+ - 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.
28
+
29
+ ## v0.2.6 — 2026-07-12
8
30
 
9
31
  WebAuthn attestation verification covers Ed448 and the RFC 9864 fully-specified COSE algorithms, and hardens credential-key conformance.
10
32
 
package/README.md CHANGED
@@ -227,7 +227,7 @@ is callable today; nothing below is a stub.
227
227
  | `pki.shbs` | Stateful hash-based signature **verification** — HSS/LMS (RFC 8554), carried in X.509 by RFC 9802 and CMS by RFC 9708, profiled by NIST SP 800-208 (CNSA 2.0 firmware signing). `verify` checks an HSS signature (every level must pass) and `verifyLms` a single-tree LMS, over the raw public-key / signature blobs the parsers already surface. Pure public-input SHA-256 / SHAKE256 hashing, a data-driven typecode registry, bounds-before-slice reads; a malformed blob throws a typed `ShbsError`, a well-formed-but-wrong signature returns `false`. **Verify only by design** — stateful signing needs atomic one-time-key state that belongs in an HSM — `verify`, `verifyLms` |
228
228
  | `pki.hpke` | Hybrid Public Key Encryption (RFC 9180) — the encrypt-to-a-public-key primitive behind TLS ECH, MLS, and OHTTP. `setupS`/`setupR` establish a sender/recipient context (KEM encapsulation + HKDF key schedule); the context's `seal`/`open` AEAD-encrypt with a sequence-counter nonce and `export` derives further secrets; `seal`/`open` are single-shot wrappers. DHKEM (P-256, P-521, X25519, X448) × HKDF-SHA256/SHA512 × AES-GCM/ChaCha20Poly1305/export-only × all four modes, proven against the RFC 9180 Appendix A vectors. DHKEM(P-384) and HKDF-SHA384 are RFC-registered but Appendix A ships no vector for them, so they fail closed until an authoritative KAT exists. Pure composition over `node:crypto`; ML-KEM / X-Wing are a registry data-row extension pending stable drafts — `suites`, `setupS`, `setupR`, `seal`, `open` |
229
229
  | `pki.sigstore` | Offline verifier for a Sigstore bundle — the exact artifact `npm publish --provenance` produces and the registry serves. `verifyBundle` composes five fail-closed legs against caller-pinned trust (Fulcio CA roots + Rekor log keys, never trusted from the bundle): the DSSE signature over its PAE preimage under the Fulcio leaf key; the ephemeral Fulcio certificate chain, validated as of the Rekor log time; the RFC 9162 inclusion proof folded to a Rekor-signed tree root; the log entry bound to this exact signature; and the in-toto SLSA subject digest the caller confirms against the published artifact. Zero runtime deps — reuses the X.509 parser, RFC 5280 path validator, and Merkle verifier; the net-new codecs are the DSSE PAE byte-builder and a fail-closed JSON reader. `pae`, `parseBundle`, `verifyBundle` |
230
- | `pki.inspect` | Human-readable certificate inspection — the pure-JS equivalent of `openssl x509 -text`. `certificate(pem | der | parsed)` renders a familiar OpenSSL-style report: version, serial, signature algorithm, issuer/subject distinguished names, validity, public-key details (curve or modulus size + the raw point/modulus), every decoded extension with its critical flag, and the signature. Built over the strict parser and the two-way OID registry, so it names extension and algorithm OIDs an OpenSSL build shows only as raw bytes. No OpenSSL dependency; the format is stable and OpenSSL-familiar rather than pinned to one OpenSSL version; a malformed extension falls back to a hex dump rather than throwing — `certificate` |
230
+ | `pki.inspect` | Human-readable certificate inspection — the pure-JS equivalent of `openssl x509 -text`. `certificate(pem \| der \| parsed)` renders a familiar OpenSSL-style report: version, serial, signature algorithm, issuer/subject distinguished names, validity, public-key details (curve or modulus size + the raw point/modulus), every decoded extension with its critical flag, and the signature. Built over the strict parser and the two-way OID registry, so it names extension and algorithm OIDs an OpenSSL build shows only as raw bytes. No OpenSSL dependency; the format is stable and OpenSSL-familiar rather than pinned to one OpenSSL version; a malformed extension falls back to a hex dump rather than throwing — `certificate` |
231
231
  | `pki.webauthn` | WebAuthn / passkey attestation verification — offline trust evaluation of a W3C WebAuthn (Level 3) attestation. `parseAttestationObject(bytes)` decodes the CBOR attestation object + authenticatorData + COSE credential key over the strict `pki.cbor` codec; `verify(attestationObject, clientDataHash, opts)` checks the attestation-statement signature and each format's structural bindings for **packed / tpm / android-key / apple / fido-u2f / none** — the x5c leaf key, the apple nonce, the tpm `certInfo` Name/`extraData` over the `pubArea`, the android `KeyDescription`, the fido-u2f `verificationData` — binding the credential public key to each attestation (via the signed authenticatorData for packed/fido-u2f, or a cert/`pubArea`-key equality check for android-key/apple/tpm) and enforcing each leaf's certificate requirements. The credential-key check covers the full WebAuthn COSE algorithm set — ES256/384/512, RS256/384/512, PS256, EdDSA (Ed25519), and the RFC 9864 fully-specified identifiers **ESP256/384/512, Ed25519, and Ed448** — validating the public-key point on its curve, rejecting the compressed EC point form, and enforcing a minimally-encoded DER ECDSA signature. A verifier, not a ceremony client; fail-closed with typed `webauthn/*` errors. Chaining the returned trust path to a pinned root (and aaguid→root via FIDO MDS) is the caller's step through `pki.path.validate` — `parseAttestationObject`, `verify` |
232
232
  | `pki.C` / `pki.constants` | Version-stable constants — functional scale helpers (`C.TIME.*`, `C.BYTES.*`), codec `LIMITS`, `version` |
233
233
  | `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError` / `Asn1Error` / `OidError` / `PemError` / `CertificateError` / `CrlError` / `CsrError` / `Pkcs8Error` / `CmsError` / `OcspError` / `TspError` / `AttrCertError` / `CrmfError` / `Pkcs12Error` / `CmpError` / `PathError` / `CtError` / `JoseError` / `AcmeError` / `WebauthnError`, each carrying a stable `code` in `domain/reason` form |
@@ -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));
@@ -575,6 +621,17 @@ function _cloneWithUsage(key, usage) {
575
621
  return k;
576
622
  }
577
623
 
624
+ // A raw or JWK-oct AES key MUST be 128/192/256 bits: W3C WebCrypto importKey rejects a
625
+ // non-conforming length as a DataError AT IMPORT, never a CryptoKey that only faults at
626
+ // first use (where node would build an "aes-160-gcm" cipher that fails only then). HMAC /
627
+ // HKDF / PBKDF2 keys carry no length restriction, so the check is scoped to the AES names.
628
+ function _assertAesImportLen(name, byteLen) {
629
+ if ((name === "AES-GCM" || name === "AES-CBC" || name === "AES-CTR" || name === "AES-KW") &&
630
+ byteLen !== 16 && byteLen !== 24 && byteLen !== 32) {
631
+ throw new WebCryptoError("webcrypto/data", name + ": an imported AES key must be 128, 192, or 256 bits");
632
+ }
633
+ }
634
+
578
635
  SubtleCrypto.prototype.importKey = async function importKey(format, keyData, algorithm, extractable, keyUsages) {
579
636
  var alg = _normalizeAlg(algorithm, "importKey");
580
637
  var usages = keyUsages || [];
@@ -584,6 +641,7 @@ SubtleCrypto.prototype.importKey = async function importKey(format, keyData, alg
584
641
  // Symmetric raw material, or a raw public key for EC/OKP.
585
642
  if (name === "AES-GCM" || name === "AES-CBC" || name === "AES-CTR" || name === "AES-KW" || name === "HMAC" || name === "HKDF" || name === "PBKDF2") {
586
643
  var raw = _toBuf(keyData, "importKey raw");
644
+ _assertAesImportLen(name, raw.length);
587
645
  var secret = nodeCrypto.createSecretKey(raw);
588
646
  var symAlg = (name === "HMAC") ? { name: name, hash: _hashObj(alg.hash), length: raw.length * 8 } : { name: name, length: raw.length * 8 };
589
647
  return new CryptoKey("secret", extractable, symAlg, usages, secret);
@@ -604,6 +662,7 @@ SubtleCrypto.prototype.importKey = async function importKey(format, keyData, alg
604
662
  var jwk = keyData;
605
663
  if (jwk.kty === "oct") {
606
664
  var kbuf = _b64urlToBuf(jwk.k, "JWK oct key material");
665
+ _assertAesImportLen(name, kbuf.length);
607
666
  var s2 = nodeCrypto.createSecretKey(kbuf);
608
667
  var a2 = (name === "HMAC") ? { name: name, hash: _hashObj(alg.hash), length: kbuf.length * 8 } : { name: name, length: kbuf.length * 8 };
609
668
  return new CryptoKey("secret", extractable, a2, usages, s2);
@@ -633,7 +692,28 @@ function _importRawPublic(name, alg, raw, extractable, usages) {
633
692
  throw new WebCryptoError("webcrypto/not-supported", "importKey raw: unsupported public-key algorithm " + JSON.stringify(name));
634
693
  }
635
694
 
695
+ // The node asymmetricKeyType(s) each imported asymmetric algorithm requires. The key TYPE
696
+ // is a property of the key material, not the caller's claim: an RSA key imported under
697
+ // {name:"Ed25519"} must be a DataError, not a CryptoKey mislabeled Ed25519 that then signs
698
+ // under the wrong scheme (algorithm confusion). EC is additionally curve-validated below.
699
+ var IMPORT_KEY_TYPE = {
700
+ "RSASSA-PKCS1-V1_5": { rsa: 1 }, "RSA-PSS": { rsa: 1, "rsa-pss": 1 }, "RSA-OAEP": { rsa: 1 },
701
+ "ECDSA": { ec: 1 }, "ECDH": { ec: 1 },
702
+ "ED25519": { ed25519: 1 }, "ED448": { ed448: 1 },
703
+ "X25519": { x25519: 1 }, "X448": { x448: 1 },
704
+ };
705
+ // The FIPS 203/204/205 PQC families import the same key-is-authority rule: each WebCrypto
706
+ // algorithm name maps to exactly its node asymmetricKeyType, so an RSA (or any) key imported
707
+ // under an ML-DSA / ML-KEM / SLH-DSA name is a DataError, not a mislabeled PQC CryptoKey.
708
+ [ML_DSA_NODE, ML_KEM_NODE, SLH_DSA_NODE].forEach(function (m) {
709
+ Object.keys(m).forEach(function (webName) { var t = {}; t[m[webName]] = 1; IMPORT_KEY_TYPE[webName] = t; });
710
+ });
636
711
  function _algFromImport(name, alg, keyObject) {
712
+ var wantType = IMPORT_KEY_TYPE[name];
713
+ if (wantType && keyObject && keyObject.asymmetricKeyType && !wantType[keyObject.asymmetricKeyType]) {
714
+ throw new WebCryptoError("webcrypto/data", name + ": the imported key is a " + keyObject.asymmetricKeyType +
715
+ " key, which is not compatible with algorithm " + name);
716
+ }
637
717
  if (name === "ECDSA" || name === "ECDH") {
638
718
  // W3C WebCrypto EC import -- the curve is a property of the KEY, not the
639
719
  // 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.8",
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:fb3ea0b4-6f7b-459d-b24d-19dcdee7f0a8",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-13T05:42:29.445Z",
8
+ "timestamp": "2026-07-13T08:58:47.895Z",
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.8",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.2.6",
25
+ "version": "0.2.8",
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.8",
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.8",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]