@blamejs/pki 0.1.22 → 0.1.24
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 +47 -0
- package/README.md +18 -6
- package/index.js +22 -12
- package/lib/asn1-der.js +126 -68
- package/lib/constants.js +46 -21
- package/lib/ct.js +54 -39
- package/lib/est.js +717 -0
- package/lib/framework-error.js +74 -34
- package/lib/oid.js +116 -60
- package/lib/path-validate.js +302 -135
- package/lib/schema-all.js +65 -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 +78 -15
- package/lib/schema-csrattrs.js +371 -0
- 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 +295 -105
- 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 +161 -26
- 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
|
}
|
|
@@ -269,10 +339,11 @@ SubtleCrypto.prototype.verify = async function verify(algorithm, key, signature,
|
|
|
269
339
|
hm.update(buf);
|
|
270
340
|
// Verify must RESOLVE false for any invalid signature, including one of
|
|
271
341
|
// the wrong length. timingSafeEqual throws RangeError on a length
|
|
272
|
-
// mismatch, so gate on length first
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
|
|
342
|
+
// mismatch, so gate on the (public) length first, then compare the bytes
|
|
343
|
+
// in constant time. The length check leaks nothing a constant-time compare
|
|
344
|
+
// would protect; the secret-dependent byte comparison is timingSafeEqual.
|
|
345
|
+
var digest = hm.digest();
|
|
346
|
+
return digest.length === sig.length && nodeCrypto.timingSafeEqual(digest, sig);
|
|
276
347
|
}
|
|
277
348
|
throw new WebCryptoError("webcrypto/not-supported", "verify: unsupported algorithm " + JSON.stringify(name));
|
|
278
349
|
};
|
|
@@ -282,6 +353,8 @@ SubtleCrypto.prototype.encrypt = async function encrypt(algorithm, key, data) {
|
|
|
282
353
|
_requireUsage(key, "encrypt");
|
|
283
354
|
var buf = _toBuf(data, "encrypt");
|
|
284
355
|
var name = alg.name;
|
|
356
|
+
if (!ENCRYPT_DECRYPT_NAMES[name]) throw new WebCryptoError("webcrypto/not-supported", "encrypt: unsupported algorithm " + JSON.stringify(name));
|
|
357
|
+
_requireAlgMatch(alg, key, "encrypt");
|
|
285
358
|
if (name === "RSA-OAEP") {
|
|
286
359
|
return _toArrayBuffer(nodeCrypto.publicEncrypt({
|
|
287
360
|
key: key._handle, padding: nodeCrypto.constants.RSA_PKCS1_OAEP_PADDING,
|
|
@@ -313,6 +386,8 @@ SubtleCrypto.prototype.decrypt = async function decrypt(algorithm, key, data) {
|
|
|
313
386
|
_requireUsage(key, "decrypt");
|
|
314
387
|
var buf = _toBuf(data, "decrypt");
|
|
315
388
|
var name = alg.name;
|
|
389
|
+
if (!ENCRYPT_DECRYPT_NAMES[name]) throw new WebCryptoError("webcrypto/not-supported", "decrypt: unsupported algorithm " + JSON.stringify(name));
|
|
390
|
+
_requireAlgMatch(alg, key, "decrypt");
|
|
316
391
|
if (name === "RSA-OAEP") {
|
|
317
392
|
return _toArrayBuffer(nodeCrypto.privateDecrypt({
|
|
318
393
|
key: key._handle, padding: nodeCrypto.constants.RSA_PKCS1_OAEP_PADDING,
|
|
@@ -346,7 +421,7 @@ function _secretBytes(key) { return key._handle.export(); }
|
|
|
346
421
|
|
|
347
422
|
// AES-CTR: node always treats the full 128-bit block as the counter and
|
|
348
423
|
// never reads the spec's `length` (counter-width) parameter. A length < 128
|
|
349
|
-
// would silently diverge from the W3C definition, so fail closed
|
|
424
|
+
// would silently diverge from the W3C definition, so fail closed -- accept
|
|
350
425
|
// only the one value node can honor.
|
|
351
426
|
function _requireCtrLength128(alg) {
|
|
352
427
|
if (alg.length !== 128) {
|
|
@@ -354,21 +429,41 @@ function _requireCtrLength128(alg) {
|
|
|
354
429
|
}
|
|
355
430
|
}
|
|
356
431
|
|
|
357
|
-
//
|
|
432
|
+
// W3C deriveBits: the requested length must be a positive multiple of 8 --
|
|
433
|
+
// the `length / 8` narrowing in the per-algorithm branches below would
|
|
434
|
+
// otherwise silently truncate a fractional byte count.
|
|
435
|
+
function _requireDeriveLength(length, who) {
|
|
436
|
+
if (typeof length !== "number" || !isFinite(length) || length <= 0 || length % 8 !== 0) {
|
|
437
|
+
throw new WebCryptoError("webcrypto/operation", who + ": length must be a positive multiple of 8 bits");
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Raw key-agreement / KDF derivation with NO usage check -- the usage a
|
|
358
442
|
// caller must hold differs by entry point (deriveBits requires "deriveBits",
|
|
359
443
|
// deriveKey requires "deriveKey"), so each public method checks its own
|
|
360
444
|
// usage and then routes the actual derivation through here.
|
|
361
445
|
function _deriveBitsRaw(alg, key, length) {
|
|
362
446
|
var name = alg.name;
|
|
363
447
|
if (name === "ECDH" || name === "X25519" || name === "X448") {
|
|
448
|
+
_requireAlgMatch(alg, alg.public, name + " public key");
|
|
364
449
|
var secret = nodeCrypto.diffieHellman({ privateKey: key._handle, publicKey: alg.public._handle });
|
|
365
|
-
|
|
450
|
+
if (length == null) return _toArrayBuffer(secret);
|
|
451
|
+
_requireDeriveLength(length, name);
|
|
452
|
+
// subarray clamps at the end of the secret, so an unchecked over-request
|
|
453
|
+
// would silently return fewer bytes than asked. W3C deriveBits: throw an
|
|
454
|
+
// OperationError when the requested length cannot be satisfied.
|
|
455
|
+
if (length / 8 > secret.length) {
|
|
456
|
+
throw new WebCryptoError("webcrypto/operation", name + ": requested " + length + " bits but the shared secret has " + (secret.length * 8));
|
|
457
|
+
}
|
|
458
|
+
return _toArrayBuffer(secret.subarray(0, length / 8));
|
|
366
459
|
}
|
|
367
460
|
if (name === "HKDF") {
|
|
461
|
+
_requireDeriveLength(length, "HKDF");
|
|
368
462
|
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
463
|
return derived instanceof ArrayBuffer ? derived : _toArrayBuffer(Buffer.from(derived));
|
|
370
464
|
}
|
|
371
465
|
if (name === "PBKDF2") {
|
|
466
|
+
_requireDeriveLength(length, "PBKDF2");
|
|
372
467
|
var out = nodeCrypto.pbkdf2Sync(_secretBytes(key), _toBuf(alg.salt, "PBKDF2 salt"), alg.iterations, length / 8, _hashNode(alg.hash, "PBKDF2"));
|
|
373
468
|
return _toArrayBuffer(out);
|
|
374
469
|
}
|
|
@@ -378,18 +473,42 @@ function _deriveBitsRaw(alg, key, length) {
|
|
|
378
473
|
SubtleCrypto.prototype.deriveBits = async function deriveBits(algorithm, key, length) {
|
|
379
474
|
var alg = _normalizeAlg(algorithm, "deriveBits");
|
|
380
475
|
_requireUsage(key, "deriveBits");
|
|
476
|
+
if (!DERIVE_NAMES[alg.name]) throw new WebCryptoError("webcrypto/not-supported", "deriveBits: unsupported algorithm " + JSON.stringify(alg.name));
|
|
477
|
+
_requireAlgMatch(alg, key, "deriveBits");
|
|
381
478
|
return _deriveBitsRaw(alg, key, length);
|
|
382
479
|
};
|
|
383
480
|
|
|
384
481
|
SubtleCrypto.prototype.deriveKey = async function deriveKey(algorithm, baseKey, derivedKeyType, extractable, keyUsages) {
|
|
385
|
-
// deriveKey requires the "deriveKey" usage
|
|
482
|
+
// deriveKey requires the "deriveKey" usage -- NOT "deriveBits". Delegating
|
|
386
483
|
// to this.deriveBits would false-reject a key created with ["deriveKey"]
|
|
387
484
|
// and fail-open on a ["deriveBits"]-only key, so check the correct usage
|
|
388
485
|
// here and route the raw derivation past deriveBits' own usage gate.
|
|
389
486
|
_requireUsage(baseKey, "deriveKey");
|
|
390
487
|
var alg = _normalizeAlg(algorithm, "deriveKey");
|
|
488
|
+
if (!DERIVE_NAMES[alg.name]) throw new WebCryptoError("webcrypto/not-supported", "deriveKey: unsupported algorithm " + JSON.stringify(alg.name));
|
|
489
|
+
_requireAlgMatch(alg, baseKey, "deriveKey");
|
|
391
490
|
var dk = _normalizeAlg(derivedKeyType, "deriveKey");
|
|
392
|
-
var bits
|
|
491
|
+
var bits;
|
|
492
|
+
if (dk.name.indexOf("AES") === 0) {
|
|
493
|
+
// W3C get-key-length for AES: the derivedKeyType MUST carry a length of
|
|
494
|
+
// 128/192/256. Without this check the derived key would silently take
|
|
495
|
+
// the size of the raw agreement/KDF output (e.g. a 384-bit "AES" key
|
|
496
|
+
// from a P-384 secret) instead of a usable AES size.
|
|
497
|
+
if (dk.length !== 128 && dk.length !== 192 && dk.length !== 256) {
|
|
498
|
+
throw new WebCryptoError("webcrypto/syntax", "deriveKey: " + dk.name + " length must be 128/192/256");
|
|
499
|
+
}
|
|
500
|
+
bits = dk.length;
|
|
501
|
+
} else if (dk.name === "HMAC") {
|
|
502
|
+
// The same W3C HMAC get-key-length rule as generateKey: an omitted
|
|
503
|
+
// length is the hash's BLOCK size, never a fixed 256.
|
|
504
|
+
bits = _hmacLengthBits(dk, "deriveKey");
|
|
505
|
+
} else {
|
|
506
|
+
// HKDF / PBKDF2 derived-key types carry no intrinsic size (W3C
|
|
507
|
+
// get-key-length is null): the base derivation decides -- a key
|
|
508
|
+
// agreement yields its full shared secret as the input keying
|
|
509
|
+
// material; a KDF base has no implicit output size and fails closed.
|
|
510
|
+
bits = dk.length != null ? dk.length : null;
|
|
511
|
+
}
|
|
393
512
|
var raw = _deriveBitsRaw(alg, baseKey, bits);
|
|
394
513
|
return this.importKey("raw", raw, dk, extractable, keyUsages);
|
|
395
514
|
};
|
|
@@ -399,6 +518,8 @@ SubtleCrypto.prototype.wrapKey = async function wrapKey(format, key, wrappingKey
|
|
|
399
518
|
var bytes = (format === "jwk") ? Buffer.from(JSON.stringify(exported)) : Buffer.from(exported);
|
|
400
519
|
var alg = _normalizeAlg(wrapAlgorithm, "wrapKey");
|
|
401
520
|
_requireUsage(wrappingKey, "wrapKey");
|
|
521
|
+
if (alg.name !== "AES-KW" && !ENCRYPT_DECRYPT_NAMES[alg.name]) throw new WebCryptoError("webcrypto/not-supported", "wrapKey: unsupported algorithm " + JSON.stringify(alg.name));
|
|
522
|
+
_requireAlgMatch(alg, wrappingKey, "wrapKey");
|
|
402
523
|
if (alg.name === "AES-KW") {
|
|
403
524
|
var c = nodeCrypto.createCipheriv("aes" + wrappingKey.algorithm.length + "-wrap", _secretBytes(wrappingKey), Buffer.from("A6A6A6A6A6A6A6A6", "hex"));
|
|
404
525
|
return _toArrayBuffer(Buffer.concat([c.update(bytes), c.final()]));
|
|
@@ -416,6 +537,8 @@ SubtleCrypto.prototype.unwrapKey = async function unwrapKey(format, wrappedKey,
|
|
|
416
537
|
// never verify the caller was actually permitted to unwrap (mirrors
|
|
417
538
|
// wrapKey, which checks before both of its paths).
|
|
418
539
|
_requireUsage(unwrappingKey, "unwrapKey");
|
|
540
|
+
if (alg.name !== "AES-KW" && !ENCRYPT_DECRYPT_NAMES[alg.name]) throw new WebCryptoError("webcrypto/not-supported", "unwrapKey: unsupported algorithm " + JSON.stringify(alg.name));
|
|
541
|
+
_requireAlgMatch(alg, unwrappingKey, "unwrapKey");
|
|
419
542
|
var bytes;
|
|
420
543
|
if (alg.name === "AES-KW") {
|
|
421
544
|
var d = nodeCrypto.createDecipheriv("aes" + unwrappingKey.algorithm.length + "-wrap", _secretBytes(unwrappingKey), Buffer.from("A6A6A6A6A6A6A6A6", "hex"));
|
|
@@ -424,7 +547,17 @@ SubtleCrypto.prototype.unwrapKey = async function unwrapKey(format, wrappedKey,
|
|
|
424
547
|
var unwrapKeyClone = _cloneWithUsage(unwrappingKey, "decrypt");
|
|
425
548
|
bytes = Buffer.from(await this.decrypt(unwrapAlgorithm, unwrapKeyClone, wrappedKey));
|
|
426
549
|
}
|
|
427
|
-
var keyData
|
|
550
|
+
var keyData;
|
|
551
|
+
if (format === "jwk") {
|
|
552
|
+
// A NON-authenticating unwrap algorithm (AES-CBC / AES-CTR) decrypts
|
|
553
|
+
// tampered bytes "successfully", so this JSON.parse is the first point
|
|
554
|
+
// that can notice -- its failure must surface as the module's typed
|
|
555
|
+
// error (W3C: DataError), never a raw SyntaxError from the public API.
|
|
556
|
+
try { keyData = JSON.parse(bytes.toString()); }
|
|
557
|
+
catch (e) { throw new WebCryptoError("webcrypto/data", "unwrapKey: unwrapped bytes are not a valid JWK", e); }
|
|
558
|
+
} else {
|
|
559
|
+
keyData = bytes;
|
|
560
|
+
}
|
|
428
561
|
return this.importKey(format, keyData, unwrappedKeyAlgorithm, extractable, keyUsages);
|
|
429
562
|
};
|
|
430
563
|
|
|
@@ -477,7 +610,9 @@ function _importRawPublic(name, alg, raw, extractable, usages) {
|
|
|
477
610
|
if (name === "ED25519" || name === "ED448" || name === "X25519" || name === "X448") {
|
|
478
611
|
var jwk = { kty: "OKP", crv: (name === "ED25519" ? "Ed25519" : name === "ED448" ? "Ed448" : name === "X25519" ? "X25519" : "X448"), x: _bufToB64url(raw) };
|
|
479
612
|
var ko = nodeCrypto.createPublicKey({ key: jwk, format: "jwk" });
|
|
480
|
-
|
|
613
|
+
// The canonical mixed-case EdDSA labels, matching _algFromImport -- a raw
|
|
614
|
+
// import must not label the same key differently than an spki/jwk one.
|
|
615
|
+
return new CryptoKey("public", true, { name: name === "ED25519" ? "Ed25519" : name === "ED448" ? "Ed448" : alg.name }, usages, ko);
|
|
481
616
|
}
|
|
482
617
|
if (name === "ECDSA" || name === "ECDH") {
|
|
483
618
|
var fb = CURVE_FIELD_BYTES[alg.namedCurve];
|
|
@@ -491,7 +626,7 @@ function _importRawPublic(name, alg, raw, extractable, usages) {
|
|
|
491
626
|
|
|
492
627
|
function _algFromImport(name, alg, keyObject) {
|
|
493
628
|
if (name === "ECDSA" || name === "ECDH") {
|
|
494
|
-
// W3C WebCrypto EC import
|
|
629
|
+
// W3C WebCrypto EC import -- the curve is a property of the KEY, not the
|
|
495
630
|
// caller's claim. Derive it from the imported key material; reject an
|
|
496
631
|
// unsupported curve (NotSupportedError, matching generateKey) and reject a
|
|
497
632
|
// requested namedCurve that disagrees with the key (DataError). Trusting
|
|
@@ -523,7 +658,7 @@ function _curveFromKey(ko) {
|
|
|
523
658
|
* @signature await pki.webcrypto.subtle.exportKey(format, key)
|
|
524
659
|
* @since 0.1.0
|
|
525
660
|
* @status stable
|
|
526
|
-
* @spec W3C WebCrypto
|
|
661
|
+
* @spec W3C WebCrypto sec. subtlecrypto, FIPS 186-5, FIPS 203, FIPS 204, FIPS 205, RFC 8017
|
|
527
662
|
* @related pki.webcrypto.CryptoKey
|
|
528
663
|
*
|
|
529
664
|
* 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.24",
|
|
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:dfb00f98-2ca7-4fd1-b0e7-90fccf92604a",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-10T12:57:51.582Z",
|
|
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.24",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.1.
|
|
25
|
+
"version": "0.1.24",
|
|
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.24",
|
|
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.24",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|