@blamejs/pki 0.3.0 → 0.3.2
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 +21 -1
- package/README.md +2 -0
- package/index.js +11 -0
- package/lib/attrcert-sign.js +504 -0
- package/lib/cms-sign.js +2 -9
- package/lib/csr-sign.js +209 -0
- package/lib/pki-build.js +310 -0
- package/lib/schema-attrcert.js +17 -0
- package/lib/x509-sign.js +14 -284
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/x509-sign.js
CHANGED
|
@@ -21,16 +21,15 @@
|
|
|
21
21
|
// cms/tsp/ocsp producing pattern); the strict schema-x509 decoder round-trips it, and that round-trip
|
|
22
22
|
// -- plus OpenSSL interop -- is the divergence guard.
|
|
23
23
|
|
|
24
|
-
var nodeCrypto = require("crypto");
|
|
25
24
|
var asn1 = require("./asn1-der");
|
|
26
25
|
var oid = require("./oid");
|
|
27
26
|
var x509 = require("./schema-x509");
|
|
28
27
|
var signScheme = require("./sign-scheme");
|
|
29
|
-
var compositeSig = require("./composite-sig");
|
|
30
28
|
var guard = require("./guard-all");
|
|
31
29
|
var frameworkError = require("./framework-error");
|
|
32
30
|
var schema = require("./schema-engine");
|
|
33
31
|
var pkix = require("./schema-pkix");
|
|
32
|
+
var pkiBuild = require("./pki-build");
|
|
34
33
|
|
|
35
34
|
var CertificateError = frameworkError.CertificateError;
|
|
36
35
|
// The x509 schema namespace + Name parser -- the SAME RDNSequence parser pki.schema.x509.parse uses, so
|
|
@@ -59,189 +58,25 @@ var KNOWN_EXT_KEYS = {
|
|
|
59
58
|
certificatePolicies: 1, certificatePoliciesCritical: 1,
|
|
60
59
|
};
|
|
61
60
|
|
|
62
|
-
//
|
|
63
|
-
// the
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
61
|
+
// The shared PKIX producing primitives (lib/pki-build.js), bound to the x509 namespace so they keep the
|
|
62
|
+
// frozen x509/* codes. Thin local aliases let the x509 call sites read unchanged. csr-sign binds the same
|
|
63
|
+
// builder to its own namespace (Hard rule #5 -- one encode definition, no per-format hand-roll).
|
|
64
|
+
var _b = pkiBuild.makeBuilder({ ErrorClass: CertificateError, prefix: "x509", O: O, NS: NS, NAME_SCHEMA: NAME_SCHEMA, SPKI_SCHEMA: SPKI_SCHEMA, EXT_DECODERS: EXT_DECODERS });
|
|
65
|
+
var _encodeName = _b.encodeName, _isEmptyName = _b.isEmptyName, _reqDer = _b.reqDer,
|
|
66
|
+
_assertValidSpki = _b.assertValidSpki, _assertValidExtension = _b.assertValidExtension,
|
|
67
|
+
_certLikeFromSpki = _b.certLikeFromSpki, _assertCertVerifies = _b.assertSignatureVerifies,
|
|
68
|
+
_ext = _b.ext, _extBasicConstraints = _b.extBasicConstraints, _validateBcSpec = _b.validateBcSpec,
|
|
69
|
+
_extKeyUsage = _b.extKeyUsage, _extExtKeyUsage = _b.extExtKeyUsage, _extSki = _b.extSki,
|
|
70
|
+
_extAki = _b.extAki, _extSan = _b.extSan, _extCertPolicies = _b.extCertPolicies,
|
|
71
|
+
_skiKeyId = _b.skiKeyId, _spkiKeyId = _b.spkiKeyId, _serialInteger = _b.serialInteger;
|
|
68
72
|
|
|
69
|
-
// ----
|
|
73
|
+
// ---- issuer-side extension helpers (subjectKeyIdentifier lookup + authorityKeyIdentifier) ----
|
|
70
74
|
|
|
71
|
-
// countryName is a PrintableString, emailAddress an IA5String; every other new-certificate attribute
|
|
72
|
-
// is a UTF8String (Teletex/BMP/Universal are backward-compat only and never emitted).
|
|
73
|
-
function _atvString(attrName, value) {
|
|
74
|
-
if (attrName === "countryName") {
|
|
75
|
-
// countryName is a PrintableString SIZE(2) -- the two-letter ISO 3166 alpha-2 code (RFC 5280 /
|
|
76
|
-
// X.520). Reject any other length at config-time.
|
|
77
|
-
if (String(value).length !== 2) throw _err("x509/bad-name", "countryName must be a two-letter ISO 3166 code (PrintableString SIZE(2))");
|
|
78
|
-
return b.printable(value);
|
|
79
|
-
}
|
|
80
|
-
if (attrName === "emailAddress") return b.ia5(value);
|
|
81
|
-
return b.utf8(value);
|
|
82
|
-
}
|
|
83
|
-
function _encodeAtv(attrName, value) {
|
|
84
|
-
if (value == null || value === "") throw _err("x509/bad-name", "the " + attrName + " attribute value must be a non-empty string");
|
|
85
|
-
var typeOid;
|
|
86
|
-
try { typeOid = O(attrName); }
|
|
87
|
-
catch (e) { throw _err("x509/bad-name", "unknown distinguished-name attribute " + JSON.stringify(attrName), e); }
|
|
88
|
-
var valueTlv;
|
|
89
|
-
try { valueTlv = _atvString(attrName, value); }
|
|
90
|
-
catch (e) { if (e instanceof CertificateError) throw e; throw _err("x509/bad-name", "the " + attrName + " value has characters invalid for its string type", e); }
|
|
91
|
-
return b.sequence([b.oid(typeOid), valueTlv]);
|
|
92
|
-
}
|
|
93
|
-
function _encodeRdn(rdnSpec) {
|
|
94
|
-
if (!rdnSpec || typeof rdnSpec !== "object" || Buffer.isBuffer(rdnSpec)) throw _err("x509/bad-name", "each RDN must be an object of { attributeName: value }");
|
|
95
|
-
var keys = Object.keys(rdnSpec);
|
|
96
|
-
if (!keys.length) throw _err("x509/bad-name", "an RDN must carry at least one attribute");
|
|
97
|
-
// build.set DER-sorts the AttributeTypeAndValue members (X.690 SET-OF ordering) for a multi-valued RDN.
|
|
98
|
-
return b.set(keys.map(function (k) { return _encodeAtv(k, rdnSpec[k]); }));
|
|
99
|
-
}
|
|
100
|
-
// A DN spec -> RDNSequence DER. A string is shorthand for a single commonName RDN; a Buffer is raw
|
|
101
|
-
// pre-encoded Name DER (the escape hatch). An empty array yields an empty RDNSequence (a subject MAY
|
|
102
|
-
// be empty with a critical SAN; the issuer non-empty rule is enforced by the caller).
|
|
103
|
-
function _encodeName(spec) {
|
|
104
|
-
if (Buffer.isBuffer(spec)) { _assertValidNameDer(spec); return spec; }
|
|
105
|
-
if (typeof spec === "string") spec = [{ commonName: spec }];
|
|
106
|
-
if (!Array.isArray(spec)) throw _err("x509/bad-name", "a name must be a string, an array of RDNs, or raw Name DER");
|
|
107
|
-
return b.sequence(spec.map(_encodeRdn));
|
|
108
|
-
}
|
|
109
|
-
// A raw Name DER is embedded verbatim, so validate it is a well-formed RDNSequence -- a SEQUENCE OF
|
|
110
|
-
// RelativeDistinguishedName, each a non-empty SET OF AttributeTypeAndValue{ type OID, value } -- before
|
|
111
|
-
// it is embedded. An empty RDNSequence is permitted (an empty subject; the issuer non-empty rule is
|
|
112
|
-
// enforced separately). asn1.decode already rejects trailing bytes.
|
|
113
|
-
function _assertValidNameDer(der) {
|
|
114
|
-
var node;
|
|
115
|
-
try { node = asn1.decode(der); }
|
|
116
|
-
catch (e) { throw _err("x509/bad-name", "the raw Name DER is not valid DER", e); }
|
|
117
|
-
// Full validation: walk the raw Name through the exact RDNSequence parser the certificate parser
|
|
118
|
-
// uses (the SET-OF structure, the AttributeTypeAndValue arity, the DirectoryString value types),
|
|
119
|
-
// not a partial hand-rolled shape check -- so an embedded raw Name is exactly what parses back.
|
|
120
|
-
try { schema.walk(NAME_SCHEMA, node, NS); }
|
|
121
|
-
catch (e) {
|
|
122
|
-
if (e instanceof CertificateError || (e && e.name === "Asn1Error")) throw e;
|
|
123
|
-
throw _err("x509/bad-name", "the raw Name DER is not a well-formed distinguished name", e);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
function _isEmptyName(nameDer) { return asn1.decode(nameDer).children.length === 0; }
|
|
127
|
-
|
|
128
|
-
// ---- GeneralName encoding (RFC 5280 sec. 4.2.1.6) --------------------------
|
|
129
|
-
|
|
130
|
-
function _ia5Content(s) {
|
|
131
|
-
s = String(s);
|
|
132
|
-
for (var i = 0; i < s.length; i++) {
|
|
133
|
-
if (s.charCodeAt(i) > 0x7F) throw _err("x509/bad-input", "value requires 7-bit ASCII (IA5String): " + JSON.stringify(s));
|
|
134
|
-
}
|
|
135
|
-
return Buffer.from(s, "latin1");
|
|
136
|
-
}
|
|
137
|
-
function _encodeGeneralName(entry) {
|
|
138
|
-
if (!entry || typeof entry !== "object" || Buffer.isBuffer(entry)) throw _err("x509/bad-input", "a GeneralName must be an object with exactly one name form");
|
|
139
|
-
var keys = Object.keys(entry);
|
|
140
|
-
if (keys.length !== 1) throw _err("x509/bad-input", "a GeneralName entry must have exactly one form, got " + keys.length);
|
|
141
|
-
var k = keys[0], v = entry[k];
|
|
142
|
-
if (v == null || v === "") throw _err("x509/bad-input", "an empty GeneralName value is not permitted (RFC 5280 sec. 4.2.1.6)");
|
|
143
|
-
switch (k) {
|
|
144
|
-
// IMPLICIT [1]/[2]/[6] IA5String -- context-primitive over the raw ASCII content.
|
|
145
|
-
case "rfc822Name": return b.contextPrimitive(1, _ia5Content(v));
|
|
146
|
-
case "dNSName": return b.contextPrimitive(2, _ia5Content(v));
|
|
147
|
-
case "uniformResourceIdentifier": case "uri": return b.contextPrimitive(6, _ia5Content(v));
|
|
148
|
-
// IMPLICIT [7] OCTET STRING -- 4 octets (IPv4) or 16 (IPv6).
|
|
149
|
-
case "iPAddress":
|
|
150
|
-
if (!Buffer.isBuffer(v) || (v.length !== 4 && v.length !== 16)) throw _err("x509/bad-input", "iPAddress must be a 4- or 16-octet Buffer");
|
|
151
|
-
return b.contextPrimitive(7, v);
|
|
152
|
-
// [4] Name -- Name is a CHOICE, so the context tag is necessarily EXPLICIT.
|
|
153
|
-
case "directoryName": return b.explicit(4, _encodeName(v));
|
|
154
|
-
default: throw _err("x509/bad-input", "unsupported GeneralName form " + JSON.stringify(k) + " (supported: rfc822Name, dNSName, uniformResourceIdentifier, iPAddress, directoryName)");
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// ---- extension-value encoders (the inverse of certExtensionDecoders) -------
|
|
159
|
-
|
|
160
|
-
function _extKeyUsage(names) {
|
|
161
|
-
if (!Array.isArray(names) || !names.length) throw _err("x509/bad-input", "keyUsage must assert at least one bit (RFC 5280 sec. 4.2.1.3)");
|
|
162
|
-
var positions = names.map(function (n) {
|
|
163
|
-
var pos = KU_BIT[n];
|
|
164
|
-
if (pos == null) throw _err("x509/bad-input", "unknown keyUsage bit " + JSON.stringify(n));
|
|
165
|
-
return pos;
|
|
166
|
-
});
|
|
167
|
-
return b.namedBitString(positions); // minimal NamedBitList DER (X.690 sec. 11.2.2)
|
|
168
|
-
}
|
|
169
|
-
function _extExtKeyUsage(names) {
|
|
170
|
-
if (!Array.isArray(names) || !names.length) throw _err("x509/bad-input", "extendedKeyUsage must list at least one KeyPurposeId");
|
|
171
|
-
return b.sequence(names.map(function (n) {
|
|
172
|
-
var purposeOid;
|
|
173
|
-
try { purposeOid = O(n); }
|
|
174
|
-
catch (e) { throw _err("x509/bad-input", "unknown extendedKeyUsage purpose " + JSON.stringify(n), e); }
|
|
175
|
-
return b.oid(purposeOid);
|
|
176
|
-
}));
|
|
177
|
-
}
|
|
178
|
-
// Validate a basicConstraints spec object before it drives the coherence checks + encoding: cA must be
|
|
179
|
-
// an explicit boolean (a truthy non-true value like 1 or "true" would silently encode as a non-CA),
|
|
180
|
-
// pathLen a non-negative integer, and only cA/pathLen/critical are recognized.
|
|
181
|
-
function _validateBcSpec(bc) {
|
|
182
|
-
if (bc.cA != null && typeof bc.cA !== "boolean") throw _err("x509/bad-input", "basicConstraints cA must be a boolean");
|
|
183
|
-
if (bc.critical != null && typeof bc.critical !== "boolean") throw _err("x509/bad-input", "basicConstraints critical must be a boolean");
|
|
184
|
-
if (bc.pathLen != null) _pathLen(bc.pathLen);
|
|
185
|
-
Object.keys(bc).forEach(function (k) {
|
|
186
|
-
if (k !== "cA" && k !== "pathLen" && k !== "critical") throw _err("x509/bad-input", "unknown basicConstraints field " + JSON.stringify(k));
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
function _extBasicConstraints(spec) {
|
|
190
|
-
var children = [];
|
|
191
|
-
if (spec.cA === true) children.push(b.boolean(true)); // cA=FALSE omitted (DER DEFAULT)
|
|
192
|
-
if (spec.pathLen != null) children.push(b.integer(_pathLen(spec.pathLen)));
|
|
193
|
-
return b.sequence(children);
|
|
194
|
-
}
|
|
195
|
-
function _pathLen(v) {
|
|
196
|
-
if (typeof v !== "number" || !isFinite(v) || v < 0 || (v | 0) !== v) throw _err("x509/bad-input", "basicConstraints pathLenConstraint must be a non-negative integer");
|
|
197
|
-
return BigInt(v);
|
|
198
|
-
}
|
|
199
|
-
function _extSki(keyid) { return b.octetString(keyid); }
|
|
200
|
-
function _extAki(keyid) { return b.sequence([b.contextPrimitive(0, keyid)]); } // keyIdentifier [0] IMPLICIT OCTET STRING
|
|
201
|
-
function _extSan(entries) {
|
|
202
|
-
if (!Array.isArray(entries) || !entries.length) throw _err("x509/bad-input", "subjectAltName must carry at least one GeneralName");
|
|
203
|
-
return b.sequence(entries.map(_encodeGeneralName));
|
|
204
|
-
}
|
|
205
|
-
function _extCertPolicies(names) {
|
|
206
|
-
if (!Array.isArray(names) || !names.length) throw _err("x509/bad-input", "certificatePolicies must list at least one policy OID");
|
|
207
|
-
var seen = {};
|
|
208
|
-
return b.sequence(names.map(function (n) {
|
|
209
|
-
var pOid;
|
|
210
|
-
try { pOid = O(n); }
|
|
211
|
-
catch (e) { throw _err("x509/bad-input", "unknown certificate policy " + JSON.stringify(n), e); }
|
|
212
|
-
if (seen[pOid]) throw _err("x509/bad-input", "duplicate certificate policy " + JSON.stringify(n) + " (RFC 5280 sec. 4.2.1.4)");
|
|
213
|
-
seen[pOid] = true;
|
|
214
|
-
return b.sequence([b.oid(pOid)]); // PolicyInformation ::= SEQUENCE { policyIdentifier OID }
|
|
215
|
-
}));
|
|
216
|
-
}
|
|
217
|
-
// Wrap an encoded extension value in Extension ::= SEQUENCE { extnID, critical?, extnValue }. A
|
|
218
|
-
// FALSE critical bit is omitted (DER DEFAULT), so the boolean appears only when the extension is critical.
|
|
219
|
-
function _ext(oidStr, critical, valueDer) {
|
|
220
|
-
var children = [b.oid(oidStr)];
|
|
221
|
-
if (critical) children.push(b.boolean(true));
|
|
222
|
-
children.push(b.octetString(valueDer));
|
|
223
|
-
return b.sequence(children);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// The SHA-1 subjectKeyIdentifier (RFC 5280 sec. 4.2.1.2 method 1): the hash of the subjectPublicKey
|
|
227
|
-
// BIT STRING CONTENT (past the unused-bits octet), NOT the whole SPKI or the whole BIT STRING TLV.
|
|
228
|
-
function _spkiKeyId(spkiDer) {
|
|
229
|
-
var keyBytes = asn1.read.bitString(asn1.decode(spkiDer).children[1]).bytes;
|
|
230
|
-
// nosemgrep: pki-weak-hash-md5-sha1 -- RFC 5280 sec. 4.2.1.2 method 1 DEFINES the subjectKeyIdentifier
|
|
231
|
-
// as the SHA-1 of the subjectPublicKey; this is a key identifier, not a signature or a
|
|
232
|
-
// collision-resistance use, and the algorithm is fixed by the standard.
|
|
233
|
-
return nodeCrypto.createHash("sha1").update(keyBytes).digest();
|
|
234
|
-
}
|
|
235
75
|
function _skiValueOf(caCert) {
|
|
236
76
|
var ext = (caCert.extensions || []).filter(function (e) { return e.oid === OID_SKI; })[0];
|
|
237
77
|
if (ext) { try { return asn1.read.octetString(asn1.decode(ext.value)); } catch (_e) { /* fall through to re-derive from the issuer SPKI */ } }
|
|
238
78
|
return null;
|
|
239
79
|
}
|
|
240
|
-
function _skiKeyId(val, spkiDer) {
|
|
241
|
-
if (Buffer.isBuffer(val)) return val;
|
|
242
|
-
if (val === true) return _spkiKeyId(spkiDer);
|
|
243
|
-
throw _err("x509/bad-input", "subjectKeyIdentifier must be true (auto-derive) or a Buffer key id");
|
|
244
|
-
}
|
|
245
80
|
function _akiKeyId(val, ctx) {
|
|
246
81
|
if (Buffer.isBuffer(val)) return val;
|
|
247
82
|
if (val === true) {
|
|
@@ -330,25 +165,6 @@ function _buildExtensions(extSpec, ctx) {
|
|
|
330
165
|
|
|
331
166
|
// ---- serial + validity + key plumbing --------------------------------------
|
|
332
167
|
|
|
333
|
-
// RFC 5280 sec. 4.1.2.2 -- a positive, non-zero INTEGER of at most 20 content octets. A random
|
|
334
|
-
// 20-octet positive serial is generated when none is supplied.
|
|
335
|
-
function _serialInteger(serial) {
|
|
336
|
-
var v;
|
|
337
|
-
if (serial == null) {
|
|
338
|
-
var rnd = nodeCrypto.randomBytes(20);
|
|
339
|
-
rnd[0] &= 0x7f; // keep the top bit clear so the magnitude stays <= 20 octets and positive
|
|
340
|
-
if (rnd[0] === 0) rnd[0] = 0x01; // never all-zero leading -> non-zero and no redundant sign octet
|
|
341
|
-
v = BigInt("0x" + rnd.toString("hex"));
|
|
342
|
-
} else if (typeof serial === "bigint") { v = serial; }
|
|
343
|
-
else if (typeof serial === "number") { if (!Number.isInteger(serial)) throw _err("x509/bad-serial", "serialNumber must be an integer"); v = BigInt(serial); }
|
|
344
|
-
else if (typeof serial === "string") { try { v = BigInt(serial); } catch (e) { throw _err("x509/bad-serial", "serialNumber string must be a decimal or 0x-hex integer", e); } }
|
|
345
|
-
else if (Buffer.isBuffer(serial)) { v = serial.length ? BigInt("0x" + serial.toString("hex")) : 0n; }
|
|
346
|
-
else { throw _err("x509/bad-serial", "serialNumber must be a BigInt, integer, hex string, or Buffer"); }
|
|
347
|
-
if (v <= 0n) throw _err("x509/bad-serial", "serialNumber must be a positive integer (RFC 5280 sec. 4.1.2.2)");
|
|
348
|
-
var tlv = b.integer(v);
|
|
349
|
-
if (asn1.decode(tlv).content.length > 20) throw _err("x509/bad-serial", "serialNumber must not exceed 20 octets (RFC 5280 sec. 4.1.2.2)");
|
|
350
|
-
return tlv;
|
|
351
|
-
}
|
|
352
168
|
// RFC 5280 sec. 4.1.2.5 -- UTCTime for 1950..2049, GeneralizedTime otherwise. UTCTime's two-digit year
|
|
353
169
|
// only represents 1950..2049 (sec. 4.1.2.5.1), so a pre-1950 date MUST use GeneralizedTime (the same
|
|
354
170
|
// arm as a date from 2050 on); using UTCTime for a pre-1950 year would be unrepresentable.
|
|
@@ -357,65 +173,6 @@ function _timeDer(date, which) {
|
|
|
357
173
|
var y = date.getUTCFullYear();
|
|
358
174
|
return (y >= 1950 && y <= 2049) ? b.utcTime(date) : b.generalizedTime(date);
|
|
359
175
|
}
|
|
360
|
-
function _reqDer(v, what) {
|
|
361
|
-
if (Buffer.isBuffer(v)) return v;
|
|
362
|
-
if (v instanceof Uint8Array) return Buffer.from(v);
|
|
363
|
-
throw _err("x509/bad-input", what + " must be a DER Buffer");
|
|
364
|
-
}
|
|
365
|
-
// A well-formed SubjectPublicKeyInfo is a SEQUENCE { algorithm AlgorithmIdentifier, subjectPublicKey
|
|
366
|
-
// BIT STRING }. The subject key is embedded verbatim (b.raw), so reject a malformed one AT ISSUANCE
|
|
367
|
-
// rather than emitting a certificate that fails to parse. (asn1.decode already rejects trailing bytes.)
|
|
368
|
-
function _assertValidSpki(spkiDer, what) {
|
|
369
|
-
var node;
|
|
370
|
-
try { node = asn1.decode(spkiDer); }
|
|
371
|
-
catch (e) { throw _err("x509/bad-input", what + " is not valid DER", e); }
|
|
372
|
-
// Full validation: run it through the SAME SubjectPublicKeyInfo parser pki.schema.x509.parse uses
|
|
373
|
-
// (the AlgorithmIdentifier + the subjectPublicKey BIT STRING), not a partial hand-rolled shape check.
|
|
374
|
-
try { schema.walk(SPKI_SCHEMA, node, NS); }
|
|
375
|
-
catch (e) {
|
|
376
|
-
if (e instanceof CertificateError || (e && e.name === "Asn1Error")) throw e;
|
|
377
|
-
throw _err("x509/bad-input", what + " is not a well-formed SubjectPublicKeyInfo", e);
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
// A well-formed Extension is a SEQUENCE { extnID OID, critical BOOLEAN OPTIONAL, extnValue OCTET
|
|
381
|
-
// STRING }. A pre-encoded (array-form) extension is embedded verbatim, so validate its shape here.
|
|
382
|
-
function _assertValidExtension(der, idx) {
|
|
383
|
-
var n;
|
|
384
|
-
try { n = asn1.decode(der); }
|
|
385
|
-
catch (e) { throw _err("x509/bad-input", "pre-encoded extension [" + idx + "] is not valid DER", e); }
|
|
386
|
-
if (n.tagNumber !== asn1.TAGS.SEQUENCE || n.tagClass !== "universal" || !n.children || n.children.length < 2 || n.children.length > 3) throw _err("x509/bad-input", "pre-encoded extension [" + idx + "] must be an Extension SEQUENCE { extnID, critical?, extnValue }");
|
|
387
|
-
try { asn1.read.oid(n.children[0]); }
|
|
388
|
-
catch (e) { throw _err("x509/bad-input", "pre-encoded extension [" + idx + "] extnID is not an OBJECT IDENTIFIER", e); }
|
|
389
|
-
if (n.children.length === 3) {
|
|
390
|
-
var crit;
|
|
391
|
-
try { crit = asn1.read.boolean(n.children[1]); }
|
|
392
|
-
catch (e) { throw _err("x509/bad-input", "pre-encoded extension [" + idx + "] critical must be a BOOLEAN", e); }
|
|
393
|
-
// DER DEFAULT: a FALSE criticality MUST be omitted, so an explicitly-encoded critical=FALSE is
|
|
394
|
-
// non-canonical (RFC 5280 sec. 4.2 / X.690 sec. 11.5).
|
|
395
|
-
if (crit !== true) throw _err("x509/bad-input", "pre-encoded extension [" + idx + "] critical=FALSE must be omitted (DER DEFAULT)");
|
|
396
|
-
}
|
|
397
|
-
var last = n.children[n.children.length - 1];
|
|
398
|
-
if (last.tagNumber !== asn1.TAGS.OCTET_STRING || last.tagClass !== "universal") throw _err("x509/bad-input", "pre-encoded extension [" + idx + "] extnValue must be an OCTET STRING");
|
|
399
|
-
}
|
|
400
|
-
// Synthesize the parsed-cert shape resolveSignScheme reads (it only needs subjectPublicKeyInfo.algorithm)
|
|
401
|
-
// from a raw SPKI DER: { algorithm: { oid, parameters } }. `parameters` is the raw params TLV (or undefined).
|
|
402
|
-
function _certLikeFromSpki(spkiDer) {
|
|
403
|
-
var spki = asn1.decode(spkiDer);
|
|
404
|
-
if (!spki.children || !spki.children.length) throw _err("x509/bad-input", "the signing key SPKI is not a SubjectPublicKeyInfo");
|
|
405
|
-
var alg = spki.children[0];
|
|
406
|
-
var keyOid;
|
|
407
|
-
try { keyOid = asn1.read.oid(alg.children[0]); }
|
|
408
|
-
catch (e) { throw _err("x509/bad-input", "the signing key SPKI algorithm is not an OID", e); }
|
|
409
|
-
return { subjectPublicKeyInfo: { algorithm: { oid: keyOid, parameters: alg.children.length > 1 ? alg.children[1].bytes : undefined } } };
|
|
410
|
-
}
|
|
411
|
-
// The raw subject Name TLV of a parsed CA certificate (byte-identical, so the child issuer chains to
|
|
412
|
-
// the CA subject exactly). The subject follows the optional version [0], serial, signature, issuer,
|
|
413
|
-
// and validity in the tbsCertificate.
|
|
414
|
-
function _caSubjectBytes(caCert) {
|
|
415
|
-
var tbs = asn1.decode(caCert.tbsBytes);
|
|
416
|
-
var hasVersion = tbs.children[0].tagClass === "context" && tbs.children[0].tagNumber === 0;
|
|
417
|
-
return tbs.children[(hasVersion ? 1 : 0) + 4].bytes; // [version?] serial(0) sig(1) issuer(2) validity(3) SUBJECT(4)
|
|
418
|
-
}
|
|
419
176
|
// A supplied issuer certificate MUST be a CA that may sign certificates: basicConstraints present AND
|
|
420
177
|
// critical AND cA=TRUE (RFC 5280 sec. 4.2.1.9), and -- when a keyUsage extension is present -- the
|
|
421
178
|
// keyCertSign bit (sec. 4.2.1.3). Refuse a non-CA issuer rather than mint a certificate that will not
|
|
@@ -455,33 +212,6 @@ function _issuedCaInfo(extSpec) {
|
|
|
455
212
|
}
|
|
456
213
|
return { cA: false, pathLen: null };
|
|
457
214
|
}
|
|
458
|
-
// Confirm the produced signature verifies under the ISSUER public key, so a signing key that does not
|
|
459
|
-
// correspond to it cannot silently yield a certificate that fails to chain. Verifying the actual
|
|
460
|
-
// signature is key-type-agnostic -- it holds for a PKCS#8 key, a WebCrypto CryptoKey, or any signer --
|
|
461
|
-
// where deriving-and-comparing the public key cannot (a non-extractable CryptoKey has no exportable
|
|
462
|
-
// public half). A composite {mldsa, trad} arm is the caller's to pair and is skipped here.
|
|
463
|
-
function _assertCertVerifies(tbsDer, sig, issuerSpki, scheme) {
|
|
464
|
-
if (scheme.composite) {
|
|
465
|
-
// Verify BOTH composite component signatures against the composite public key.
|
|
466
|
-
return compositeSig.compositeVerify(issuerSpki, sig, tbsDer, scheme.composite, CertificateError, "x509/unsupported-algorithm", "x509/bad-input").then(function (r) {
|
|
467
|
-
if (!r.ok) throw _err("x509/bad-input", "the composite signing key does not correspond to the issuer public key -- the certificate would not chain");
|
|
468
|
-
});
|
|
469
|
-
}
|
|
470
|
-
var pub;
|
|
471
|
-
try { pub = nodeCrypto.createPublicKey({ key: issuerSpki, format: "der", type: "spki" }); }
|
|
472
|
-
catch (e) { throw _err("x509/bad-input", "the issuer public key could not be imported for the chain self-check", e); }
|
|
473
|
-
var s = scheme.sign, ok;
|
|
474
|
-
try {
|
|
475
|
-
if (s.name === "ECDSA") ok = nodeCrypto.verify(scheme.digest, tbsDer, { key: pub, dsaEncoding: "der" }, sig);
|
|
476
|
-
else if (s.name === "RSA-PSS") ok = nodeCrypto.verify(scheme.digest, tbsDer, { key: pub, padding: nodeCrypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: s.saltLength }, sig);
|
|
477
|
-
else if (s.name === "RSASSA-PKCS1-v1_5") ok = nodeCrypto.verify(scheme.digest, tbsDer, pub, sig);
|
|
478
|
-
// allow:eddsa-verify-without-loworder-gate -- this is a self-check that OUR just-produced signature
|
|
479
|
-
// verifies under the issuer key the caller controls, not a security verify of an untrusted EdDSA
|
|
480
|
-
// signature, so the low-order-point gate (a forged-signature defense) does not apply.
|
|
481
|
-
else ok = nodeCrypto.verify(null, tbsDer, pub, sig); // Ed25519 / Ed448 / ML-DSA / SLH-DSA
|
|
482
|
-
} catch (e) { throw _err("x509/bad-input", "the chain self-check could not run against the issuer public key", e); }
|
|
483
|
-
if (!ok) throw _err("x509/bad-input", "the signing key does not correspond to the issuer public key -- the certificate would not chain");
|
|
484
|
-
}
|
|
485
215
|
// Does the extensions spec carry a subjectAltName that will be emitted critical? The object form forces
|
|
486
216
|
// SAN critical when the subject is empty (so any subjectAltName qualifies); the pre-encoded array form
|
|
487
217
|
// is decoded to look for an Extension whose extnID is subjectAltName with a TRUE critical flag.
|
|
@@ -558,7 +288,7 @@ function _sign(spec, issuer, opts) {
|
|
|
558
288
|
issuerCert = (Buffer.isBuffer(issuer.cert) || typeof issuer.cert === "string") ? x509.parse(issuer.cert) : issuer.cert;
|
|
559
289
|
if (!issuerCert || !issuerCert.tbsBytes) throw _err("x509/bad-input", "issuer.cert must be a certificate DER/PEM or a parsed certificate");
|
|
560
290
|
issuerPathLen = _assertIssuerIsCa(issuerCert);
|
|
561
|
-
issuerDer =
|
|
291
|
+
issuerDer = pkiBuild.tbsNameField(issuerCert, "subject");
|
|
562
292
|
issuerSpki = issuerCert.subjectPublicKeyInfo.bytes;
|
|
563
293
|
} else {
|
|
564
294
|
issuerDer = _encodeName(issuer.name == null ? [] : issuer.name);
|
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:3787cd37-e9d0-465d-bed3-0f6822edb52c",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-17T08:27:56.310Z",
|
|
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.3.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.3.2",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.3.
|
|
25
|
+
"version": "0.3.2",
|
|
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.3.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.3.2",
|
|
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.3.
|
|
57
|
+
"ref": "@blamejs/pki@0.3.2",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|