@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 +14 -1
- package/lib/path-validate.js +1 -1
- package/lib/webcrypto.js +87 -20
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
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.
|
|
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
|
|
package/lib/path-validate.js
CHANGED
|
@@ -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; }
|
|
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
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
|
379
|
-
return _toArrayBuffer(
|
|
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
|
|
384
|
-
return _toArrayBuffer(
|
|
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
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
|
415
|
-
return _toArrayBuffer(
|
|
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
|
|
420
|
-
return _toArrayBuffer(
|
|
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
|
-
|
|
530
|
-
|
|
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
|
|
550
|
-
|
|
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
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:
|
|
5
|
+
"serialNumber": "urn:uuid:ea60e88f-9354-49c4-aa35-ad6712102de1",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
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.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.2.7",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.2.
|
|
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.
|
|
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.
|
|
57
|
+
"ref": "@blamejs/pki@0.2.7",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|