@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.
- package/CHANGELOG.md +35 -0
- package/README.md +16 -6
- package/index.js +16 -12
- package/lib/asn1-der.js +126 -68
- package/lib/constants.js +46 -21
- package/lib/ct.js +54 -39
- package/lib/framework-error.js +57 -34
- package/lib/oid.js +110 -59
- package/lib/path-validate.js +302 -135
- package/lib/schema-all.js +49 -41
- package/lib/schema-attrcert.js +101 -64
- package/lib/schema-cmp.js +152 -131
- package/lib/schema-cms.js +621 -163
- package/lib/schema-crl.js +43 -21
- package/lib/schema-crmf.js +136 -79
- package/lib/schema-csr.js +63 -14
- package/lib/schema-engine.js +92 -43
- package/lib/schema-ocsp.js +101 -55
- package/lib/schema-pkcs12.js +80 -64
- package/lib/schema-pkcs8.js +12 -12
- package/lib/schema-pkix.js +283 -100
- package/lib/schema-smime.js +39 -39
- package/lib/schema-tsp.js +108 -59
- package/lib/schema-x509.js +36 -25
- package/lib/webcrypto.js +156 -22
- package/package.json +3 -2
- package/sbom.cdx.json +6 -6
package/lib/schema-x509.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* @intro
|
|
13
13
|
* X.509 certificate handling per RFC 5280. The seed surface is
|
|
14
|
-
* `parse`
|
|
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
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
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
|
|
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 }
|
|
89
|
-
//
|
|
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",
|
|
92
|
-
schema.field("notAfter",
|
|
94
|
+
schema.field("notBefore", pkix.time(NS)),
|
|
95
|
+
schema.field("notAfter", pkix.time(NS)),
|
|
93
96
|
], {
|
|
94
|
-
assert: "
|
|
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
|
|
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
|
-
|
|
126
|
-
|
|
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: "
|
|
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 (
|
|
133
|
-
// agreement,
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
17
|
-
* CMS, OCSP)
|
|
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
|
|
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`
|
|
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
|
|
40
|
-
// stays the cross-module home; this is webcrypto-private).
|
|
41
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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:
|
|
5
|
+
"serialNumber": "urn:uuid:a62885e0-295d-4f1c-aa93-536269dabd5b",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
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
|
+
"bom-ref": "@blamejs/pki@0.1.23",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.1.
|
|
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.
|
|
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.
|
|
57
|
+
"ref": "@blamejs/pki@0.1.23",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|