@blamejs/pki 0.2.5 → 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 +32 -0
- package/README.md +1 -1
- package/lib/acme.js +2 -1
- package/lib/edwards-point.js +91 -0
- package/lib/inspect.js +13 -3
- package/lib/path-validate.js +2 -0
- package/lib/validator-all.js +47 -0
- package/lib/validator-attcert.js +110 -0
- package/lib/validator-cose.js +176 -0
- package/lib/validator-keydesc.js +89 -0
- package/lib/validator-sig.js +57 -0
- package/lib/validator-tpm.js +135 -0
- package/lib/webauthn.js +77 -352
- package/lib/webcrypto.js +92 -21
- package/package.json +3 -1
- package/sbom.cdx.json +6 -6
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
|
|
@@ -658,7 +725,11 @@ function _algFromImport(name, alg, keyObject) {
|
|
|
658
725
|
}
|
|
659
726
|
|
|
660
727
|
function _curveFromKey(ko) {
|
|
661
|
-
try {
|
|
728
|
+
try {
|
|
729
|
+
var jwk = ko.export({ format: "jwk" });
|
|
730
|
+
for (var k in CURVE_NODE) { if (jwk.crv === k) return k; }
|
|
731
|
+
}
|
|
732
|
+
catch (_e) { /* best-effort */ }
|
|
662
733
|
return undefined;
|
|
663
734
|
}
|
|
664
735
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blamejs/pki",
|
|
3
|
-
"version": "0.2.
|
|
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",
|
|
@@ -66,6 +66,8 @@
|
|
|
66
66
|
"fuzz": "npm ci --prefix fuzz && npx --prefix fuzz jazzer fuzz/asn1-der.fuzz.js -- -max_total_time=60",
|
|
67
67
|
"gates": "node test/layer-0-primitives/codebase-patterns.test.js && node scripts/validate-source-comment-blocks.js && node scripts/check-api-snapshot.js",
|
|
68
68
|
"coverage": "c8 --include=lib/** --include=index.js --reporter=text-summary --reporter=lcov node test/smoke.js",
|
|
69
|
+
"check:swallows": "node scripts/check-swallow-coverage.js",
|
|
70
|
+
"coverage:gated": "npm run coverage && npm run check:swallows",
|
|
69
71
|
"prepack": "node scripts/check-pack-against-gitignore.js",
|
|
70
72
|
"check:vendor-currency": "node scripts/check-vendor-currency.js"
|
|
71
73
|
},
|
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
|
]
|