@blamejs/pki 0.2.32 → 0.3.0
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 -0
- package/README.md +2 -1
- package/index.js +6 -0
- package/lib/asn1-der.js +18 -0
- package/lib/schema-attrcert.js +251 -2
- package/lib/schema-pkix.js +19 -13
- package/lib/sigstore.js +8 -1
- package/lib/tsp-sign.js +1 -9
- package/lib/x509-sign.js +637 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,27 @@ All notable changes to `@blamejs/pki` are documented here. The format
|
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
|
|
5
5
|
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## v0.3.0 — 2026-07-17
|
|
8
|
+
|
|
9
|
+
pki.x509.sign issues X.509 certificates -- self-signed or CA-signed, over every signature algorithm the toolkit supports, from RSA and ECDSA through EdDSA, ML-DSA, and SLH-DSA.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- pki.x509.sign(spec, issuer, opts) builds and signs an X.509 certificate -- self-signed (a signing key alone) or CA-signed (an explicit issuer name and public key, or an issuing certificate) -- and returns DER, or a PEM CERTIFICATE string with opts.pem. The signing algorithm is resolved from the key: RSA PKCS#1 v1.5, RSASSA-PSS (opts.pss), ECDSA P-256/P-384/P-521, Ed25519, Ed448, ML-DSA-44/65/87, the twelve SLH-DSA parameter sets, and the composite arms. The version, serial-number bounds, UTCTime/GeneralizedTime cutover, DER default omissions, and the CA cross-field rules are enforced; malformed input throws a typed CertificateError. Certificate parsing remains pki.schema.x509.parse.
|
|
14
|
+
- pki.asn1.build.namedBitString(positions) encodes a minimal DER NamedBitList (X.690 section 11.2.2) -- the asserted bit positions with every trailing zero bit removed -- the single canonical home the keyUsage and PKIFailureInfo encoders now compose.
|
|
15
|
+
|
|
16
|
+
### Security
|
|
17
|
+
|
|
18
|
+
- The Sigstore bundle verifier now routes an Ed25519 or Ed448 key through the shared full-order, on-curve Edwards-point gate at the raw signature-verification sink, not only at key parsing -- a low-order or off-curve key that would verify a forged EdDSA signature is refused wherever a verify path handles one. This completes the defense across every EdDSA verification sink in the toolkit.
|
|
19
|
+
|
|
20
|
+
## v0.2.33 — 2026-07-16
|
|
21
|
+
|
|
22
|
+
Attribute certificates now decode their RFC 5755 attribute values and attribute-certificate extensions, not just the certificate structure.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- pki.schema.attrcert.parse decodes the RFC 5755 section 4.4 attribute values -- role (RoleSyntax), clearance (Clearance, including the classification bit list and security categories), authenticationInfo and accessIdentity (SvceAuthInfo), and group and chargingIdentity (IetfAttrSyntax) -- and the section 4.3 attribute-certificate extensions -- auditIdentity, targetInformation and proxyInfo (Targets, with targetCert reusing the IssuerSerial decoder), noRevAvail, and aaControls. Each is surfaced additively (a decoded field alongside the raw value) through the parse consumer path; an unrecognized attribute type or extension id is preserved opaque, and a malformed recognized value fails closed with a typed error. Every GeneralName inside these structures is validated through the shared name decoder.
|
|
27
|
+
|
|
7
28
|
## v0.2.32 — 2026-07-16
|
|
8
29
|
|
|
9
30
|
X.509 certificates now decode the Microsoft Active Directory Certificate Services enrollment extensions, and pki.lint's critical-extension check is aligned with certification-path validation.
|
package/README.md
CHANGED
|
@@ -211,7 +211,7 @@ is callable today; nothing below is a stub.
|
|
|
211
211
|
| `pki.schema.cms` | Parse DER / PEM CMS per RFC 5652 / 5083 / 9629 — SignedData (§5, signer infos + raw signed-attribute bytes for external verification), EnvelopedData (§6, all five RecipientInfo kinds incl. RFC 5753 key-agreement and RFC 9629 KEM recipients with ML-KEM validation), EncryptedData (§8), AuthenticatedData (§9, MAC surface + raw `authAttrsBytes`), and AuthEnvelopedData (RFC 5083, with RFC 5084 AES-GCM/CCM parameter validation); §11 attribute placement enforced, countersignatures validated recursively, certificates / CRLs validated against the closed CHOICE sets and kept raw, every result tagged `contentTypeName`, fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
|
212
212
|
| `pki.schema.ocsp` | Parse DER / PEM OCSP requests and responses per RFC 6960 — per-certificate status (good / revoked / unknown), responder identity, raw tbs bytes for external verification, certificates kept raw, fail-closed; non-basic response types recognized (not decoded) — `parseRequest`, `parseResponse`, `pemDecode`, `pemEncode` |
|
|
213
213
|
| `pki.schema.tsp` | Parse DER / PEM RFC 3161 timestamp requests, responses, and tokens — the TimeStampReq a client sends (imprint, requested policy, nonce, certReq), the TSTInfo payload (imprint, genTime with sub-second precision, serial, nonce, accuracy), the status-to-token coupling, and the token wrapper composed over CMS with the single-signer rule, fail-closed — `parse`, `parseRequest`, `parseResponse`, `parseTstInfo`, `parseToken`, `pemDecode`, `pemEncode` |
|
|
214
|
-
| `pki.schema.attrcert` | Parse DER / PEM RFC 5755 attribute certificates — the holder and issuer identities (validated GeneralNames), the validity window (real `Date`s), the privilege attributes (role / group /
|
|
214
|
+
| `pki.schema.attrcert` | Parse DER / PEM RFC 5755 attribute certificates — the holder and issuer identities (validated GeneralNames), the validity window (real `Date`s), the privilege attributes and extensions **decoded to structured values** (role, clearance, service/access identity, group, charging identity; audit identity, target/proxy information, no-rev-avail, AA controls — unknown types preserved opaque), with the raw signed region for a verifier; the obsolete v1 form is recognized and deferred, fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
|
215
215
|
| `pki.schema.crmf` | Parse DER / PEM RFC 4211 certificate request messages (CertReqMessages — the CMP / EST enrollment body) — the requested-certificate template (subject, public key, validity, extensions), proof-of-possession, and registration controls, with the raw `CertRequest` region a POP verifier hashes; names dual-accepted (IMPLICIT and EXPLICIT), fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
|
216
216
|
| `pki.schema.pkcs12` | Parse DER / BER / PEM RFC 7292 PKCS#12 (PFX) stores — key bags via the PKCS#8 parser, shrouded keys (algorithm surfaced, ciphertext opaque), cert / CRL / secret bags raw and byte-exact, encrypted and enveloped safes structurally via CMS, `friendlyName` / `localKeyId` decoded, and the exact MAC byte range (`macedBytes`) plus RFC 9579 PBMAC1 recognition for external verification; BER accepted exactly where §4.1 requires it, fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
|
217
217
|
| `pki.schema.cmp` | Parse DER / PEM RFC 9810 Certificate Management Protocol messages (PKIMessage) — the header (version, sender / recipient incl. the anonymous NULL-DN, nonces, transaction id, general info), the 27-arm body (certificate requests via the CRMF parser, an encrypted certificate's EnvelopedData via CMS, response / revocation / confirmation / error / support / polling arms structural, the rest raw), and the exact `headerBytes` / `bodyBytes` slices an external verifier reconstructs the protected part from; the CMP-before-OCSP dispatch order is enforced, fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
|
@@ -222,6 +222,7 @@ is callable today; nothing below is a stub.
|
|
|
222
222
|
| `pki.schema.smime` | Decode S/MIME ESS signed-attribute values (RFC 5035 / RFC 8551) — `parseSigningCertificate` / `parseSigningCertificateV2` bind a signature to its signing certificate (cert hash, hash algorithm, issuer `GeneralNames` + serial), `parseSmimeCapabilities` decodes the ordered capability list, and `decodeAttribute` OID-dispatches a CMS attribute (enforcing the single-value rule, recognize-and-defer for unknown types). A companion decoder for CMS signed attributes, not an auto-routed format, fail-closed — `parseSigningCertificate`, `parseSigningCertificateV2`, `parseSmimeCapabilities`, `decodeAttribute` |
|
|
223
223
|
| `pki.schema.engine` | The declarative ASN.1 structure-schema engine every format parser composes — `walk` / `encode` / `embeddedDer` plus the schema combinators |
|
|
224
224
|
| `pki.path` | RFC 5280 §6 certification-path validation — `validate` runs the §6.1 state machine (signature chaining across RSA, ECDSA, EdDSA, ML-DSA, SLH-DSA and hybrid composite ML-DSA signatures — a composite is accepted only when **both** its post-quantum and traditional components verify; validity windows, name chaining, basic constraints and path length, key usage, name constraints, the certificate-policy tree) over an ordered path and a trust anchor, returning a structured verdict with per-check reason codes, and enforces a `pki.trust` anchor's per-purpose distrust-after dates and delegator purposes via `checkPurpose`; `crlChecker` supplies CRL-based revocation — including partitioned/sharded CRLs, whose §6.3.3 Distribution Point ↔ IDP correspondence lets a corresponding full-reason shard establish non-revocation — and `ocspChecker` supplies OCSP-based revocation (RFC 6960 — CertID binding, responder authorization, signature, currency) over the same pluggable hook. Pure and re-entrant, fail-closed — `validate`, `crlChecker`, `ocspChecker` |
|
|
225
|
+
| `pki.x509` | X.509 certificate issuance (RFC 5280 §4) — `sign(spec, issuer, opts)` builds and signs a certificate: a `spec` of subject (a common-name string, an array of RDNs, or raw Name DER), the public key being certified, the validity window, an optional serial, and an optional `extensions` object; an `issuer` that is a key alone (self-signed — issuer equals subject, signed with that key) or a name + public key + key, or an issuing certificate + key (CA-signed). The signature algorithm is resolved from the signing key through the shared registry, so RSA (PKCS#1 v1.5 / PSS via `opts.pss`), ECDSA P-256/384/521, Ed25519, Ed448, ML-DSA-44/65/87, the twelve SLH-DSA sets, and the composite arms all issue without a per-algorithm branch. It encodes basic constraints, key usage, extended key usage, subject and authority key identifiers (the SKI auto-derived by SHA-1 of the subject key), subject alternative names, and certificate policies from the spec — any other extension supplied as pre-encoded DER — derives the version from the field set, and enforces the serial bounds, the UTCTime/GeneralizedTime cutover, the DER default omissions, and the CA cross-field rules; a violation throws a typed `CertificateError`. Returns DER, or a PEM `CERTIFICATE` with `opts.pem`; every arm is independently verified by OpenSSL. Parsing stays at `pki.schema.x509.parse` — `sign` |
|
|
225
226
|
| `pki.cms` | RFC 5652 §5 CMS SignedData signing + signature verification — `sign(content, signers, opts)` produces a SignedData (attached or detached, one or many signers, RSA / RSASSA-PSS / ECDSA / EdDSA, the post-quantum ML-DSA-44/65/87 (RFC 9882) and SLH-DSA (all twelve FIPS 205 sets, RFC 9814), and composite ML-DSA (pairing ML-DSA with a traditional RSA / ECDSA / EdDSA — accepted only when **both** components verify — draft-ietf-lamps-cms-composite-sigs)); it builds the signed attributes (content-type, message-digest, signing-time) as canonical DER, signs the exact §5.4 preimage, and emits a DER `Buffer` or PEM. `verify(input, opts)` parses a SignedData over the strict `pki.schema.cms` codec, locates each SignerInfo's signer certificate by its issuerAndSerialNumber or subjectKeyIdentifier, and checks the signature over the exact §5.4 preimage: when signed attributes are present it confirms the message-digest attribute equals the content digest and verifies over the DER re-encoding of the SignedAttributes (the on-wire `[0]` tag replaced by a universal SET OF), otherwise directly over the content. It returns a per-signer verdict with the matched signer certificate; it does not chain that certificate to a trust anchor — that is the caller's step through `pki.path.validate`. **Content encryption** (RFC 5652/5083/5084/9629): `encrypt(content, recipients, opts)` produces an EnvelopedData, AuthEnvelopedData (AES-GCM, the authenticated default), or EncryptedData — recipients auto-dispatch off the certificate key to key-transport (RSAES-OAEP; v1.5 never emitted), key-agreement (ephemeral-static ECDH over P-256/384/521 with the X9.63 KDF, and X25519/X448 with HKDF), symmetric key-wrap, password (PBKDF2 + RFC 3211 PWRI-KEK), or the post-quantum ML-KEM KEMRecipientInfo (RFC 9629/9936) — one fresh content key wrapped for every recipient. `decrypt(input, keyMaterial, opts)` recovers the content through the matching arm and returns it with an `authenticated` flag; every secret-dependent failure collapses to one uniform `cms/decrypt-failed` verdict (Bleichenbacher / EFAIL / password-oracle freedom), and PKCS#1 v1.5 is decrypt-only under the RFC 3218 implicit-rejection countermeasure. **Compression** (RFC 3274): `compress(content, opts)` / `decompress(input, opts)` produce and consume a CompressedData (ZLIB, version 0, id-alg-zlibCompress); decompress bounds the uncompressed output at 16 MiB and stops before it is materialized, so a decompression bomb fails closed as `cms/decompress-too-large` — a size transform with no integrity/confidentiality (RFC 8551 §2.4.5). Fail-closed with typed `cms/*` errors — `sign`, `verify`, `encrypt`, `decrypt`, `compress`, `decompress` |
|
|
226
227
|
| `pki.smime` | RFC 8551 S/MIME message assembly, verification, encryption, and compression over the CMS layer — `sign(content, signers, opts)` wraps a MIME entity as a signed S/MIME message in either form: `multipart/signed` (clear-signed — the content stays readable in any MUA, a detached CMS SignedData rides alongside as `application/pkcs7-signature` with a matching `micalg`) or `application/pkcs7-mime; smime-type=signed-data` (opaque — the whole entity is a base64 CMS SignedData). The signed bytes are the entity's RFC 8551 §3.1.1 canonical form (CRLF line endings); `verify(message, opts)` unwraps both forms and recomputes over the same canonicalizer, so a transport that re-wraps line endings still verifies and a tampered part fails. `encrypt(content, recipients, opts)` envelopes a MIME entity as an opaque `application/pkcs7-mime` message and `decrypt(message, keyMaterial, opts)` opens one — `smime-type=authEnveloped-data` (AES-GCM, confidentiality and integrity, the default) or `smime-type=enveloped-data` (AES-CBC, confidentiality only, so `decrypt` reports `authenticated: false`, the §3.3 no-integrity caveat); the `smime-type` is derived from the CMS body, not the header, and decryption is fail-closed and oracle-free. The crypto is entirely `pki.cms.sign` / `verify` / `encrypt` / `decrypt` — any RSA / RSASSA-PSS / ECDSA / EdDSA / ML-DSA / SLH-DSA signer and any RSA-OAEP / ECDH / X25519 / X448 / AES-KW / PBKDF2 / ML-KEM recipient carries through (algorithm-agnostic). Like `cms.verify`, `verify` returns the per-signer cryptographic verdict plus the recovered content; chaining a signer to a trust anchor is the caller's `pki.path.validate` step. `compress(content, opts)` / `decompress(message, opts)` add the opaque `application/pkcs7-mime; smime-type=compressed-data; name=smime.p7z` frame (RFC 8551 §3.6, RFC 3274) — a size transform with no integrity/confidentiality (§2.4.5), decompress bounded against a bomb; the recovered content, which may itself be signed or enveloped, is returned for the caller to re-verify. Bidirectionally interoperable with `openssl smime` / `openssl cms`. Fail-closed with typed `smime/*` errors — `sign`, `verify`, `encrypt`, `decrypt`, `compress`, `decompress` |
|
|
227
228
|
| `pki.tsp` | RFC 3161 Time-Stamp Protocol — `sign(messageImprint, tsa, opts)` produces a TimeStampToken: a CMS SignedData (over `pki.cms.sign`) whose content is a `TSTInfo` carrying the timestamped message imprint, the TSA policy, a serial number, and `genTime` (with optional accuracy / nonce / ordering), plus the RFC 3161 §2.4.2 signing-certificate attribute binding the token to the TSA certificate (SHA-2 imprints, any `pki.cms.sign` TSA key). `request` / `parseRequest` build and parse the TimeStampReq a client sends (imprint, requested policy, nonce, certReq), `response` / `parseResponse` the TimeStampResp a TSA returns — a granted status wrapping a token, or a rejection with PKIStatus and failure info, the §2.4.2 status↔token coupling enforced in both directions. `verify(token, data, opts)` verifies a token fail-closed: the CMS signature over the exact signed bytes, the message imprint recomputed from the data, the TSTInfo content type, the ESSCertID(V2) binding to the TSA certificate, the §2.3 critical timeStamping-only extendedKeyUsage, the request nonce when used, and — with a trust anchor supplied — full certification-path validation of the TSA certificate at the token's `genTime`, returning `{ valid, genTime, serialNumber, tstInfo, … }` — `sign`, `request`, `parseRequest`, `response`, `parseResponse`, `verify` |
|
package/index.js
CHANGED
|
@@ -40,6 +40,7 @@ var cms = require("./lib/cms-verify");
|
|
|
40
40
|
var smime = require("./lib/smime");
|
|
41
41
|
var tsp = require("./lib/tsp-sign");
|
|
42
42
|
var ocsp = require("./lib/ocsp");
|
|
43
|
+
var x509 = require("./lib/x509-sign");
|
|
43
44
|
var merkle = require("./lib/merkle");
|
|
44
45
|
var shbs = require("./lib/shbs");
|
|
45
46
|
var hpke = require("./lib/hpke");
|
|
@@ -81,6 +82,11 @@ module.exports = {
|
|
|
81
82
|
smime: smime,
|
|
82
83
|
tsp: tsp,
|
|
83
84
|
ocsp: ocsp,
|
|
85
|
+
// `x509` is the certificate-issuance producing side -- pki.x509.sign builds and
|
|
86
|
+
// signs an X.509 certificate (RFC 5280 sec. 4), self-signed or CA-signed, over any
|
|
87
|
+
// signature algorithm the registry resolves (RSA/ECDSA/EdDSA/ML-DSA/SLH-DSA/composite).
|
|
88
|
+
// Parsing lives at pki.schema.x509.parse.
|
|
89
|
+
x509: x509,
|
|
84
90
|
// `merkle` is the RFC 6962 / RFC 9162 Merkle-tree proof-verification core --
|
|
85
91
|
// pki.merkle.leafHash / nodeHash / emptyRootHash build the domain-separated
|
|
86
92
|
// (0x00 leaf / 0x01 node) SHA-256 tree hashes; pki.merkle.verifyInclusion and
|
package/lib/asn1-der.js
CHANGED
|
@@ -937,6 +937,24 @@ var build = {
|
|
|
937
937
|
}
|
|
938
938
|
return _universal(TAGS.BIT_STRING, false, Buffer.concat([Buffer.from([u]), body]));
|
|
939
939
|
},
|
|
940
|
+
// A minimal DER NamedBitList BIT STRING (X.690 sec. 11.2.2): the asserted `positions` (0-based bit
|
|
941
|
+
// indices, 0 = the MSB of the first octet) with every trailing zero bit removed and the unused-bit
|
|
942
|
+
// count set below the last set bit. The single home the keyUsage / PKIFailureInfo / any named-bit
|
|
943
|
+
// encoder composes, so the minimal-encoding rule lives once (the CHECK side is
|
|
944
|
+
// schema.assertMinimalNamedBits). No bits asserted -> the empty BIT STRING.
|
|
945
|
+
namedBitString: function (positions) {
|
|
946
|
+
if (!Array.isArray(positions)) throw new Asn1Error("asn1/bad-bit-string", "namedBitString requires an array of bit positions");
|
|
947
|
+
var hi = -1, i, p;
|
|
948
|
+
for (i = 0; i < positions.length; i++) {
|
|
949
|
+
p = positions[i];
|
|
950
|
+
if (typeof p !== "number" || !isFinite(p) || p < 0 || (p | 0) !== p) throw new Asn1Error("asn1/bad-bit-string", "a named-bit position must be a non-negative integer");
|
|
951
|
+
if (p > hi) hi = p;
|
|
952
|
+
}
|
|
953
|
+
if (hi < 0) return build.bitString(Buffer.alloc(0), 0);
|
|
954
|
+
var buf = Buffer.alloc((hi >> 3) + 1);
|
|
955
|
+
for (i = 0; i < positions.length; i++) { p = positions[i]; buf[p >> 3] |= 0x80 >> (p & 7); }
|
|
956
|
+
return build.bitString(buf, 7 - (hi & 7));
|
|
957
|
+
},
|
|
940
958
|
utf8: function (s) { return _universal(TAGS.UTF8_STRING, false, Buffer.from(String(s), "utf8")); },
|
|
941
959
|
ia5: function (s) {
|
|
942
960
|
// Validate the INPUT code points, not the latin1 output bytes: latin1
|
package/lib/schema-attrcert.js
CHANGED
|
@@ -281,7 +281,24 @@ var ATTRIBUTE_CERTIFICATE = pkix.signedEnvelope(NS, ACINFO, {
|
|
|
281
281
|
// kept stable for consumers keying off `form`.
|
|
282
282
|
var issuer = { form: "v2Form", v2Form: acinfo.fields.issuer.value.result, v1Form: null };
|
|
283
283
|
|
|
284
|
+
// RFC 5755 sec. 4.4 -- decode each attribute's value(s) additively: a recognized attribute type
|
|
285
|
+
// gains `decoded` (parallel to `values`); an unrecognized type falls back to { opaque, bytes } so
|
|
286
|
+
// the parse never fails on an unknown attribute, and a recognized-but-malformed value fails closed.
|
|
287
|
+
var attributes = acinfo.fields.attributes.value.result;
|
|
288
|
+
attributes.forEach(function (a) {
|
|
289
|
+
var dec = _ATTR_VALUE_DECODERS[a.type];
|
|
290
|
+
a.decoded = a.values.map(function (v) { return dec ? dec(v) : { opaque: true, bytes: v }; });
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// RFC 5755 sec. 4.3 -- decode each AC extension additively (extensions[i].decoded), same opaque
|
|
294
|
+
// fallback + fail-closed-on-malformed contract as the attributes above.
|
|
284
295
|
var extField = acinfo.fields.extensions;
|
|
296
|
+
var extensions = (extField && extField.present) ? extField.value.result : [];
|
|
297
|
+
extensions.forEach(function (x) {
|
|
298
|
+
var dec = _AC_EXT_DECODERS[x.oid];
|
|
299
|
+
x.decoded = dec ? dec(x.value) : { opaque: true, bytes: x.value };
|
|
300
|
+
});
|
|
301
|
+
|
|
285
302
|
return {
|
|
286
303
|
version: acinfo.fields.version.value, // 2
|
|
287
304
|
holder: acinfo.fields.holder.value.result,
|
|
@@ -291,9 +308,9 @@ var ATTRIBUTE_CERTIFICATE = pkix.signedEnvelope(NS, ACINFO, {
|
|
|
291
308
|
serialNumber: acinfo.fields.serialNumber.value,
|
|
292
309
|
serialNumberHex: sc.toString("hex"),
|
|
293
310
|
validity: acinfo.fields.attrCertValidityPeriod.value.result,
|
|
294
|
-
attributes:
|
|
311
|
+
attributes: attributes,
|
|
295
312
|
issuerUniqueID: acinfo.fields.issuerUniqueID.present ? acinfo.fields.issuerUniqueID.value : null,
|
|
296
|
-
extensions:
|
|
313
|
+
extensions: extensions,
|
|
297
314
|
tbsBytes: e.tbsBytes,
|
|
298
315
|
signatureValue: e.signatureValue,
|
|
299
316
|
};
|
|
@@ -417,6 +434,238 @@ function matchesV1(root) {
|
|
|
417
434
|
return schema.isUniversal(k[1], TAGS.SEQUENCE);
|
|
418
435
|
}
|
|
419
436
|
|
|
437
|
+
// ---- RFC 5755 sec. 4.4 attribute-value decoders ----------------------
|
|
438
|
+
// A per-attribute-type decoder table (mirroring pkix.certExtensionDecoders): each takes the raw
|
|
439
|
+
// AttributeValue TLV Buffer and returns a structured value, throwing a typed attrcert/bad-* error on
|
|
440
|
+
// any non-conformant shape. Composes pkix's shared imperative helpers + generalName/generalNames.
|
|
441
|
+
// A single GeneralName decodes as the walk OBJECT itself (gn.tagNumber / gn.value); generalNames
|
|
442
|
+
// (plural) returns { names:[...] } on .result.
|
|
443
|
+
var _AV = pkix._decodeHelpers(NS);
|
|
444
|
+
var _decodeTop = _AV.decodeTop, _seqChildren = _AV.seqChildren, _readInt = _AV.readInt, _Oav = _AV.O;
|
|
445
|
+
var _GN = pkix.generalName(NS, { decodeValue: true, code: "attrcert/bad-general-name" });
|
|
446
|
+
var _GNS_0 = pkix.generalNames(NS, { implicitTag: 0, decodeValue: true, code: "attrcert/bad-general-names" });
|
|
447
|
+
function _isCtx(node, n) { return node && node.tagClass === "context" && node.tagNumber === n; }
|
|
448
|
+
function _isU(node, tag) { return node && node.tagClass === "universal" && node.tagNumber === tag; }
|
|
449
|
+
function _oneGN(node, code) {
|
|
450
|
+
if (!_isCtx(node, 0) && !_isCtx(node, 1) && !_isCtx(node, 2) && !_isCtx(node, 3) &&
|
|
451
|
+
!_isCtx(node, 4) && !_isCtx(node, 5) && !_isCtx(node, 6) && !_isCtx(node, 7) && !_isCtx(node, 8)) {
|
|
452
|
+
throw NS.E(code, "a GeneralName must be a context-tagged [0]..[8] CHOICE alternative");
|
|
453
|
+
}
|
|
454
|
+
var gn = schema.walk(_GN, node, NS);
|
|
455
|
+
return { tagNumber: gn.tagNumber, value: gn.value };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// SvceAuthInfo ::= SEQUENCE { service GeneralName, ident GeneralName, authInfo OCTET STRING OPTIONAL }
|
|
459
|
+
// -- authenticationInfo (sec. 4.4.1) allows authInfo; accessIdentity (sec. 4.4.2) MUST omit it.
|
|
460
|
+
function _svceAuthInfo(authInfoAllowed, code) {
|
|
461
|
+
return function (buf) {
|
|
462
|
+
var kids = _seqChildren(buf, code, "SvceAuthInfo");
|
|
463
|
+
if (kids.length < 2 || kids.length > 3) throw NS.E(code, "SvceAuthInfo must be a SEQUENCE { service, ident, authInfo? }");
|
|
464
|
+
var service = _oneGN(kids[0], code);
|
|
465
|
+
var ident = _oneGN(kids[1], code);
|
|
466
|
+
var authInfo = null;
|
|
467
|
+
if (kids.length === 3) {
|
|
468
|
+
if (!authInfoAllowed) throw NS.E(code, "this attribute must not carry authInfo (RFC 5755 sec. 4.4.2)");
|
|
469
|
+
if (!_isU(kids[2], TAGS.OCTET_STRING)) throw NS.E(code, "SvceAuthInfo authInfo must be an OCTET STRING");
|
|
470
|
+
authInfo = Buffer.concat([asn1.read.octetString(kids[2])]);
|
|
471
|
+
}
|
|
472
|
+
return { service: service, ident: ident, authInfo: authInfo };
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// IetfAttrSyntax ::= SEQUENCE { policyAuthority [0] IMPLICIT GeneralNames OPTIONAL,
|
|
477
|
+
// values SEQUENCE OF CHOICE { octets OCTET STRING, oid OBJECT IDENTIFIER, string UTF8String } }
|
|
478
|
+
function _ietfAttrSyntax(buf) {
|
|
479
|
+
var C = "attrcert/bad-ietf-attr";
|
|
480
|
+
var kids = _seqChildren(buf, C, "IetfAttrSyntax");
|
|
481
|
+
if (!kids.length || kids.length > 2) throw NS.E(C, "IetfAttrSyntax must be a SEQUENCE { policyAuthority [0]?, values }");
|
|
482
|
+
var policyAuthority = null, i = 0;
|
|
483
|
+
if (_isCtx(kids[0], 0)) { policyAuthority = schema.walk(_GNS_0, kids[0], NS).result; i = 1; }
|
|
484
|
+
var valuesNode = kids[i];
|
|
485
|
+
if (!valuesNode || !_isU(valuesNode, TAGS.SEQUENCE) || !valuesNode.children || !valuesNode.children.length) {
|
|
486
|
+
throw NS.E(C, "IetfAttrSyntax values must be a non-empty SEQUENCE OF value");
|
|
487
|
+
}
|
|
488
|
+
if (i + 1 !== kids.length) throw NS.E(C, "IetfAttrSyntax has unexpected trailing fields (a [0] must precede values)");
|
|
489
|
+
var kinds = {};
|
|
490
|
+
var values = valuesNode.children.map(function (v) {
|
|
491
|
+
if (_isU(v, TAGS.OCTET_STRING)) { kinds.octets = true; return { kind: "octets", value: Buffer.concat([asn1.read.octetString(v)]) }; }
|
|
492
|
+
if (_isU(v, TAGS.OBJECT_IDENTIFIER)) { kinds.oid = true; return { kind: "oid", value: asn1.read.oid(v) }; }
|
|
493
|
+
if (_isU(v, TAGS.UTF8_STRING)) { kinds.string = true; return { kind: "string", value: asn1.read.string(v) }; }
|
|
494
|
+
throw NS.E(C, "IetfAttrSyntax value must be an OCTET STRING, OBJECT IDENTIFIER, or UTF8String");
|
|
495
|
+
});
|
|
496
|
+
return { policyAuthority: policyAuthority, values: values, homogeneous: Object.keys(kinds).length === 1 };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// RoleSyntax ::= SEQUENCE { roleAuthority [0] IMPLICIT GeneralNames OPTIONAL, roleName [1] EXPLICIT GeneralName }
|
|
500
|
+
function _role(buf) {
|
|
501
|
+
var C = "attrcert/bad-role";
|
|
502
|
+
var kids = _seqChildren(buf, C, "RoleSyntax");
|
|
503
|
+
if (!kids.length || kids.length > 2) throw NS.E(C, "RoleSyntax must be a SEQUENCE { roleAuthority [0]?, roleName [1] }");
|
|
504
|
+
var roleAuthority = null, i = 0;
|
|
505
|
+
if (_isCtx(kids[0], 0)) { roleAuthority = schema.walk(_GNS_0, kids[0], NS).result; i = 1; }
|
|
506
|
+
var rn = kids[i];
|
|
507
|
+
if (!_isCtx(rn, 1) || !rn.children || rn.children.length !== 1) throw NS.E(C, "RoleSyntax roleName must be an EXPLICIT [1] wrapping exactly one GeneralName");
|
|
508
|
+
if (i + 1 !== kids.length) throw NS.E(C, "RoleSyntax has unexpected trailing fields");
|
|
509
|
+
return { roleAuthority: roleAuthority, roleName: _oneGN(rn.children[0], C) };
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Clearance ::= SEQUENCE { policyId OBJECT IDENTIFIER, classList ClassList DEFAULT {unclassified},
|
|
513
|
+
// securityCategories SET OF SecurityCategory OPTIONAL }.
|
|
514
|
+
// ClassList ::= BIT STRING { unmarked(0), unclassified(1), restricted(2), confidential(3), secret(4), topSecret(5) }.
|
|
515
|
+
var _CLASSLIST_NAMES = ["unmarked", "unclassified", "restricted", "confidential", "secret", "topSecret"];
|
|
516
|
+
function _decodeClassList(node, C) {
|
|
517
|
+
var bs;
|
|
518
|
+
try { bs = asn1.read.bitString(node); } catch (e) { throw NS.E(C, "Clearance classList must be a BIT STRING", e); }
|
|
519
|
+
schema.assertMinimalNamedBits(bs.unusedBits, bs.bytes, function (m) { throw NS.E(C, m); });
|
|
520
|
+
var names = [], flags = {}, reserved = [];
|
|
521
|
+
var total = bs.bytes.length * 8 - bs.unusedBits;
|
|
522
|
+
for (var bit = 0; bit < total; bit++) {
|
|
523
|
+
if ((bs.bytes[bit >> 3] & (0x80 >> (bit & 7))) === 0) continue;
|
|
524
|
+
if (bit < _CLASSLIST_NAMES.length) { names.push(_CLASSLIST_NAMES[bit]); flags[_CLASSLIST_NAMES[bit]] = true; }
|
|
525
|
+
else reserved.push(bit);
|
|
526
|
+
}
|
|
527
|
+
return { names: names, flags: flags, reservedBits: reserved };
|
|
528
|
+
}
|
|
529
|
+
// SecurityCategory ::= SEQUENCE { type [0] IMPLICIT OBJECT IDENTIFIER, value [1] EXPLICIT ANY }.
|
|
530
|
+
function _securityCategory(node, C) {
|
|
531
|
+
if (!_isU(node, TAGS.SEQUENCE) || !node.children || node.children.length !== 2) throw NS.E(C, "a SecurityCategory must be a SEQUENCE of a type and a value");
|
|
532
|
+
var typeNode = node.children[0], valueNode = node.children[1];
|
|
533
|
+
if (!_isCtx(typeNode, 0) || typeNode.children) throw NS.E(C, "SecurityCategory type must be a primitive [0] IMPLICIT OBJECT IDENTIFIER");
|
|
534
|
+
var type; try { type = asn1.decodeOidContent(typeNode.content); } catch (e) { throw NS.E(C, "SecurityCategory type is not a valid OBJECT IDENTIFIER", e); }
|
|
535
|
+
if (!_isCtx(valueNode, 1) || !valueNode.children || valueNode.children.length !== 1) throw NS.E(C, "SecurityCategory value must be an EXPLICIT [1] wrapping one element");
|
|
536
|
+
return { type: type, valueBytes: Buffer.concat([valueNode.children[0].bytes]) };
|
|
537
|
+
}
|
|
538
|
+
function _clearance(buf) {
|
|
539
|
+
var C = "attrcert/bad-clearance";
|
|
540
|
+
var kids = _seqChildren(buf, C, "Clearance");
|
|
541
|
+
if (!kids.length || kids.length > 3) throw NS.E(C, "Clearance must be a SEQUENCE { policyId, classList?, securityCategories? }");
|
|
542
|
+
if (!_isU(kids[0], TAGS.OBJECT_IDENTIFIER)) throw NS.E(C, "Clearance policyId must be an OBJECT IDENTIFIER");
|
|
543
|
+
var policyId; try { policyId = asn1.read.oid(kids[0]); } catch (e) { throw NS.E(C, "Clearance policyId must be an OBJECT IDENTIFIER", e); }
|
|
544
|
+
var classList = { names: ["unclassified"], flags: { unclassified: true }, reservedBits: [] }, i = 1; // DEFAULT {unclassified}
|
|
545
|
+
if (kids[i] && _isU(kids[i], TAGS.BIT_STRING)) {
|
|
546
|
+
classList = _decodeClassList(kids[i], C);
|
|
547
|
+
if (classList.names.length === 1 && classList.flags.unclassified && !classList.reservedBits.length) {
|
|
548
|
+
throw NS.E(C, "Clearance classList equal to the DEFAULT {unclassified} must be omitted (non-canonical DER)");
|
|
549
|
+
}
|
|
550
|
+
i++;
|
|
551
|
+
}
|
|
552
|
+
var securityCategories = null;
|
|
553
|
+
if (kids[i]) {
|
|
554
|
+
var setNode = kids[i];
|
|
555
|
+
if (!_isU(setNode, TAGS.SET) || !setNode.children || !setNode.children.length) throw NS.E(C, "Clearance securityCategories must be a non-empty SET OF SecurityCategory");
|
|
556
|
+
securityCategories = setNode.children.map(function (categoryNode) { return _securityCategory(categoryNode, C); });
|
|
557
|
+
i++;
|
|
558
|
+
}
|
|
559
|
+
if (i !== kids.length) throw NS.E(C, "Clearance has fields out of order or unexpected trailing fields");
|
|
560
|
+
return { policyId: policyId, classList: classList, securityCategories: securityCategories };
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
var _ATTR_VALUE_DECODERS = {};
|
|
564
|
+
_ATTR_VALUE_DECODERS[_Oav("role")] = _role;
|
|
565
|
+
_ATTR_VALUE_DECODERS[_Oav("clearance")] = _clearance;
|
|
566
|
+
// RFC 5755 sec. 4.4.6 requires decoding the legacy RFC 3281 id-at-clearance too (the X.501
|
|
567
|
+
// selected-attribute-types arc, an alias of "clearance" -- built from arcs, never a dotted literal).
|
|
568
|
+
_ATTR_VALUE_DECODERS[oid.fromArcs([2, 5, 1, 5, 55])] = _clearance;
|
|
569
|
+
_ATTR_VALUE_DECODERS[_Oav("authenticationInfo")] = _svceAuthInfo(true, "attrcert/bad-svce-auth-info");
|
|
570
|
+
_ATTR_VALUE_DECODERS[_Oav("accessIdentity")] = _svceAuthInfo(false, "attrcert/bad-access-identity");
|
|
571
|
+
_ATTR_VALUE_DECODERS[_Oav("chargingIdentity")] = _ietfAttrSyntax;
|
|
572
|
+
_ATTR_VALUE_DECODERS[_Oav("group")] = _ietfAttrSyntax;
|
|
573
|
+
|
|
574
|
+
// ---- RFC 5755 sec. 4.3 AC-extension decoders -------------------------
|
|
575
|
+
// A per-extension-OID decoder table for the AC-specific extensions (kept in the AC domain, not the
|
|
576
|
+
// shared cert-extension table, because Targets/TargetCert reuse the local IssuerSerial). Each takes
|
|
577
|
+
// the raw extnValue content Buffer and returns a structured value, fail-closed with a typed error.
|
|
578
|
+
|
|
579
|
+
// AuditIdentity ::= OCTET STRING (sec. 4.3.1) -- an opaque audit tag, unconstrained (no SIZE), so an
|
|
580
|
+
// empty OCTET STRING (04 00) is valid; only the OCTET STRING type is enforced.
|
|
581
|
+
function _acAuditIdentity(buf) {
|
|
582
|
+
var C = "attrcert/bad-audit-identity";
|
|
583
|
+
var n = _decodeTop(buf, C, "AuditIdentity");
|
|
584
|
+
if (!_isU(n, TAGS.OCTET_STRING)) throw NS.E(C, "AuditIdentity must be an OCTET STRING");
|
|
585
|
+
return Buffer.concat([asn1.read.octetString(n)]);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// NoRevAvail syntax is NULL (sec. 4.3.6: '0500'H is the DER encoding) -- the extnValue OCTET STRING
|
|
589
|
+
// contains a DER NULL. An empty or any-other-shape value is malformed and fails closed.
|
|
590
|
+
function _noRevAvail(buf) {
|
|
591
|
+
var C = "attrcert/bad-no-rev-avail";
|
|
592
|
+
var n = _decodeTop(buf, C, "NoRevAvail");
|
|
593
|
+
try { asn1.read.nullValue(n); } catch (e) { throw NS.E(C, "NoRevAvail must be a DER NULL (RFC 5755 sec. 4.3.6)", e); }
|
|
594
|
+
return { noRevAvail: true };
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Target ::= CHOICE { targetName [0] GeneralName, targetGroup [1] GeneralName, targetCert [2] TargetCert }.
|
|
598
|
+
// [0]/[1] wrap a GeneralName CHOICE (EXPLICIT); [2] is an IMPLICIT TargetCert SEQUENCE.
|
|
599
|
+
function _acTargetCert(node, C) {
|
|
600
|
+
if (!node.children || !node.children.length) throw NS.E(C, "a targetCert [2] must be a non-empty TargetCert SEQUENCE");
|
|
601
|
+
var out = { kind: "targetCert", targetCertificate: null, targetName: null, certDigestInfo: null }, i = 0;
|
|
602
|
+
out.targetCertificate = schema.walk(ISSUER_SERIAL, node.children[i++], NS).result;
|
|
603
|
+
if (node.children[i] && node.children[i].tagClass === "context") out.targetName = _oneGN(node.children[i++], C);
|
|
604
|
+
if (node.children[i] && _isU(node.children[i], TAGS.SEQUENCE)) out.certDigestInfo = schema.walk(OBJECT_DIGEST_INFO, node.children[i++], NS).result;
|
|
605
|
+
if (i !== node.children.length) throw NS.E(C, "a targetCert has unexpected trailing fields");
|
|
606
|
+
return out;
|
|
607
|
+
}
|
|
608
|
+
function _acTarget(node, C, allowTargetCert) {
|
|
609
|
+
if (_isCtx(node, 0) || _isCtx(node, 1)) {
|
|
610
|
+
if (!node.children || node.children.length !== 1) throw NS.E(C, "a Target name must be an EXPLICIT [0]/[1] wrapping one GeneralName");
|
|
611
|
+
return { kind: node.tagNumber === 0 ? "targetName" : "targetGroup", name: _oneGN(node.children[0], C) };
|
|
612
|
+
}
|
|
613
|
+
if (_isCtx(node, 2)) {
|
|
614
|
+
if (!allowTargetCert) throw NS.E(C, "the targetCert [2] CHOICE MUST NOT be used in proxying information (RFC 5755 sec. 7.4)");
|
|
615
|
+
return _acTargetCert(node, C);
|
|
616
|
+
}
|
|
617
|
+
throw NS.E(C, "a Target must be [0] targetName, [1] targetGroup, or [2] targetCert");
|
|
618
|
+
}
|
|
619
|
+
// Targets ::= SEQUENCE OF Target -- decode a Targets SEQUENCE node into an array of Target.
|
|
620
|
+
function _acDecodeTargets(node, C, allowTargetCert) {
|
|
621
|
+
if (!_isU(node, TAGS.SEQUENCE) || !node.children || !node.children.length) throw NS.E(C, "Targets must be a non-empty SEQUENCE OF Target");
|
|
622
|
+
return node.children.map(function (t) { return _acTarget(t, C, allowTargetCert); });
|
|
623
|
+
}
|
|
624
|
+
// Both targetInformation (sec. 4.3.2) and ProxyInfo (sec. 7.4) are SEQUENCE OF Targets -- an outer
|
|
625
|
+
// SEQUENCE whose every element is itself a Targets (SEQUENCE OF Target). A conforming issuer emits one
|
|
626
|
+
// Targets, but users MUST accept several. proxying additionally forbids the targetCert [2] alternative.
|
|
627
|
+
function _seqOfTargets(buf, C, allowTargetCert) {
|
|
628
|
+
var kids = _seqChildren(buf, C, "SEQUENCE OF Targets");
|
|
629
|
+
if (!kids.length) throw NS.E(C, "a SEQUENCE OF Targets must be non-empty");
|
|
630
|
+
return kids.map(function (targetsNode) { return _acDecodeTargets(targetsNode, C, allowTargetCert); });
|
|
631
|
+
}
|
|
632
|
+
function _targetInformation(buf) { return _seqOfTargets(buf, "attrcert/bad-targets", true); }
|
|
633
|
+
function _acProxying(buf) { return _seqOfTargets(buf, "attrcert/bad-proxy-info", false); }
|
|
634
|
+
|
|
635
|
+
// AAControls ::= SEQUENCE { pathLenConstraint INTEGER (0..MAX) OPTIONAL, permittedAttrs [0] AttrSpec
|
|
636
|
+
// OPTIONAL, excludedAttrs [1] AttrSpec OPTIONAL, permitUnSpecified BOOLEAN DEFAULT TRUE }.
|
|
637
|
+
// AttrSpec ::= SEQUENCE OF OBJECT IDENTIFIER (the [0]/[1] IMPLICIT tag replaces the SEQUENCE tag).
|
|
638
|
+
function _acAttrSpec(node, C) {
|
|
639
|
+
if (!node.children) throw NS.E(C, "an AttrSpec must be a SEQUENCE OF OBJECT IDENTIFIER");
|
|
640
|
+
return node.children.map(function (o) {
|
|
641
|
+
if (!_isU(o, TAGS.OBJECT_IDENTIFIER)) throw NS.E(C, "an AttrSpec element must be an OBJECT IDENTIFIER");
|
|
642
|
+
try { return asn1.read.oid(o); } catch (e) { throw NS.E(C, "an AttrSpec element must be an OBJECT IDENTIFIER", e); }
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
function _aaControls(buf) {
|
|
646
|
+
var C = "attrcert/bad-aa-controls";
|
|
647
|
+
var kids = _seqChildren(buf, C, "AAControls");
|
|
648
|
+
if (kids.length > 4) throw NS.E(C, "AAControls has too many fields");
|
|
649
|
+
var out = { pathLenConstraint: null, permittedAttrs: null, excludedAttrs: null, permitUnSpecified: true }, i = 0;
|
|
650
|
+
if (kids[i] && _isU(kids[i], TAGS.INTEGER)) { out.pathLenConstraint = guard.range.uint31(_readInt(kids[i], C, "pathLenConstraint"), NS.E, C, "AAControls pathLenConstraint"); i++; }
|
|
651
|
+
if (kids[i] && _isCtx(kids[i], 0)) { out.permittedAttrs = _acAttrSpec(kids[i], C); i++; }
|
|
652
|
+
if (kids[i] && _isCtx(kids[i], 1)) { out.excludedAttrs = _acAttrSpec(kids[i], C); i++; }
|
|
653
|
+
if (kids[i] && _isU(kids[i], TAGS.BOOLEAN)) {
|
|
654
|
+
var v = asn1.read.boolean(kids[i]);
|
|
655
|
+
if (v === true) throw NS.E(C, "AAControls permitUnSpecified TRUE equals the DEFAULT and must be omitted (non-canonical DER)");
|
|
656
|
+
out.permitUnSpecified = v; i++;
|
|
657
|
+
}
|
|
658
|
+
if (i !== kids.length) throw NS.E(C, "AAControls has fields out of order or unexpected trailing fields");
|
|
659
|
+
return out;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
var _AC_EXT_DECODERS = {};
|
|
663
|
+
_AC_EXT_DECODERS[_Oav("acAuditIdentity")] = _acAuditIdentity;
|
|
664
|
+
_AC_EXT_DECODERS[_Oav("targetInformation")] = _targetInformation;
|
|
665
|
+
_AC_EXT_DECODERS[_Oav("noRevAvail")] = _noRevAvail;
|
|
666
|
+
_AC_EXT_DECODERS[_Oav("aaControls")] = _aaControls;
|
|
667
|
+
_AC_EXT_DECODERS[_Oav("acProxying")] = _acProxying;
|
|
668
|
+
|
|
420
669
|
module.exports = {
|
|
421
670
|
parse: parse,
|
|
422
671
|
parseV1: parseV1,
|
package/lib/schema-pkix.js
CHANGED
|
@@ -598,13 +598,14 @@ function assertPolicyQualifiers(qNode, fail) {
|
|
|
598
598
|
});
|
|
599
599
|
}
|
|
600
600
|
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
601
|
+
// Shared imperative decode helpers for the certExtensionDecoders + attrValueDecoders factories. Both
|
|
602
|
+
// take the caller's ns, so the helpers close over ns.E / ns.oid; extracted once so the two factories
|
|
603
|
+
// share one copy (a per-OID value-decoder body is the identical idiom to a per-OID extension body).
|
|
604
|
+
function _decodeHelpers(ns) {
|
|
604
605
|
function decodeTop(buf, code, what) {
|
|
605
606
|
var n;
|
|
606
607
|
try { n = asn1.decode(buf); }
|
|
607
|
-
catch (e) { throw ns.E(code, "malformed " + what + "
|
|
608
|
+
catch (e) { throw ns.E(code, "malformed " + what + " value: " + ((e && e.message) || String(e)), e); }
|
|
608
609
|
return n;
|
|
609
610
|
}
|
|
610
611
|
function seqChildren(buf, code, what) {
|
|
@@ -618,6 +619,19 @@ function certExtensionDecoders(ns) {
|
|
|
618
619
|
try { return asn1.read.integer(node); }
|
|
619
620
|
catch (e) { throw ns.E(code, what + " must be an INTEGER", e); }
|
|
620
621
|
}
|
|
622
|
+
// Resolve a registered OID name -> dotted string, throwing at module load on a typo (a decoder keyed
|
|
623
|
+
// by an unresolved name would silently never match, treating a real extension/attribute as opaque).
|
|
624
|
+
function O(nm) {
|
|
625
|
+
var d = ns.oid.byName(nm);
|
|
626
|
+
if (typeof d !== "string") throw new TypeError("pki.schema.pkix: " + JSON.stringify(nm) + " is not a registered OID name");
|
|
627
|
+
return d;
|
|
628
|
+
}
|
|
629
|
+
return { decodeTop: decodeTop, seqChildren: seqChildren, readInt: readInt, O: O };
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function certExtensionDecoders(ns) {
|
|
633
|
+
var GN_SUBTREE = generalName(ns, { decodeValue: true, subtreeBase: true, code: ns.prefix + "/bad-name-constraints" });
|
|
634
|
+
var _h = _decodeHelpers(ns), decodeTop = _h.decodeTop, seqChildren = _h.seqChildren, readInt = _h.readInt, O = _h.O;
|
|
621
635
|
|
|
622
636
|
// basicConstraints ::= SEQUENCE { cA BOOLEAN DEFAULT FALSE, pathLen INTEGER (0..MAX) OPTIONAL }
|
|
623
637
|
function basicConstraints(buf) {
|
|
@@ -910,15 +924,6 @@ function certExtensionDecoders(ns) {
|
|
|
910
924
|
});
|
|
911
925
|
}
|
|
912
926
|
|
|
913
|
-
// byName returns undefined for an unregistered name; keying a decoder row
|
|
914
|
-
// under "undefined" would silently drop that extension from dispatch (the
|
|
915
|
-
// consumer would treat the extension as structurally unvalidated / absent),
|
|
916
|
-
// so a typo'd key-list entry must fail here, at module load.
|
|
917
|
-
function O(nm) {
|
|
918
|
-
var d = ns.oid.byName(nm);
|
|
919
|
-
if (typeof d !== "string") throw new TypeError("certExtensionDecoders: " + JSON.stringify(nm) + " is not a registered OID name");
|
|
920
|
-
return d;
|
|
921
|
-
}
|
|
922
927
|
// qcStatements ::= SEQUENCE OF QCStatement (RFC 3739 sec. 3.2.6). QCStatement ::= SEQUENCE {
|
|
923
928
|
// statementId OBJECT IDENTIFIER, statementInfo ANY DEFINED BY statementId OPTIONAL }. Known statementIds
|
|
924
929
|
// (RFC 3739 id-qcs + the ETSI EN 319 412-5 esi4 catalog) decode their statementInfo; an unknown statementId
|
|
@@ -1319,6 +1324,7 @@ module.exports = {
|
|
|
1319
1324
|
rawNonEmptySequence: rawNonEmptySequence,
|
|
1320
1325
|
CRL_REASON_NAMES: CRL_REASON_NAMES,
|
|
1321
1326
|
certExtensionDecoders: certExtensionDecoders,
|
|
1327
|
+
_decodeHelpers: _decodeHelpers,
|
|
1322
1328
|
attribute: attribute,
|
|
1323
1329
|
extension: extension,
|
|
1324
1330
|
extensions: extensions,
|
package/lib/sigstore.js
CHANGED
|
@@ -179,7 +179,14 @@ function _chainDers(vm) {
|
|
|
179
179
|
// Verify a DER ECDSA / Ed25519 signature over `data` under a node public key.
|
|
180
180
|
function _rawVerify(keyObj, data, derSig) {
|
|
181
181
|
var t = keyObj.asymmetricKeyType;
|
|
182
|
-
if (t === "ed25519" || t === "ed448")
|
|
182
|
+
if (t === "ed25519" || t === "ed448") {
|
|
183
|
+
// Defense in depth at the verify SINK (not only at _pubFromSpki): node imports a low-order /
|
|
184
|
+
// off-curve OKP key without complaint and such a key verifies a FORGED EdDSA signature, so re-gate
|
|
185
|
+
// the point here regardless of how keyObj was obtained -- no verify sink may hand an unvalidated
|
|
186
|
+
// Edwards key to node's verify.
|
|
187
|
+
edwardsPoint.validateSpki(keyObj.export({ type: "spki", format: "der" }), t === "ed25519" ? 6 : 7, SigstoreError, "sigstore/bad-key");
|
|
188
|
+
return nodeCrypto.verify(null, data, keyObj, derSig);
|
|
189
|
+
}
|
|
183
190
|
var crv = keyObj.asymmetricKeyDetails && keyObj.asymmetricKeyDetails.namedCurve;
|
|
184
191
|
var hash = crv === "secp384r1" ? "sha384" : (crv === "secp521r1" ? "sha512" : "sha256");
|
|
185
192
|
return nodeCrypto.verify(hash, data, { key: keyObj, dsaEncoding: "der" }, derSig);
|
package/lib/tsp-sign.js
CHANGED
|
@@ -178,15 +178,7 @@ function _failInfoBits(names) {
|
|
|
178
178
|
if (i == null) throw _err("tsp/bad-input", "unknown PKIFailureInfo name " + JSON.stringify(n) + " (RFC 3161 sec. 2.4.2)");
|
|
179
179
|
return i;
|
|
180
180
|
});
|
|
181
|
-
|
|
182
|
-
// buf is sized to the highest set bit, so its last octet always carries that bit (non-zero) --
|
|
183
|
-
// there is never a trailing zero octet to strip; only the trailing zero BITS of the last octet
|
|
184
|
-
// are removed, via the DER unusedBits count (X.690 sec. 11.2.2).
|
|
185
|
-
var buf = Buffer.alloc((Math.max.apply(null, idxs) >> 3) + 1);
|
|
186
|
-
idxs.forEach(function (i) { buf[i >> 3] |= 0x80 >> (i & 7); });
|
|
187
|
-
var unused = 0, last = buf[buf.length - 1];
|
|
188
|
-
while (unused < 7 && ((last >> unused) & 1) === 0) unused++; // unusedBits below the lowest set bit
|
|
189
|
-
return b.bitString(buf, unused);
|
|
181
|
+
return b.namedBitString(idxs); // minimal NamedBitList DER (X.690 sec. 11.2.2), empty when no bits set
|
|
190
182
|
}
|
|
191
183
|
|
|
192
184
|
// Coerce a timeStampToken (a CMS ContentInfo the token producer emits) to DER for embedding in a
|
package/lib/x509-sign.js
ADDED
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @module pki.x509
|
|
7
|
+
* @nav Signing
|
|
8
|
+
* @title Certificates
|
|
9
|
+
* @intro The X.509 certificate-issuance producing side. `pki.x509.sign` builds a `TBSCertificate`,
|
|
10
|
+
* signs it, and emits a `Certificate` (RFC 5280 sec. 4) that `pki.schema.x509.parse`,
|
|
11
|
+
* `pki.path.validate`, and OpenSSL all accept -- self-signed or CA-signed, over any signature
|
|
12
|
+
* algorithm the toolkit registry resolves: RSA (PKCS#1 v1.5 / PSS), ECDSA, EdDSA, ML-DSA, SLH-DSA,
|
|
13
|
+
* and the composite (hybrid) arms. Parsing lives at `pki.schema.x509.parse`.
|
|
14
|
+
* @spec RFC 5280
|
|
15
|
+
* @card Build and sign an X.509 certificate -- self-signed or CA-signed, over any registry algorithm.
|
|
16
|
+
*/
|
|
17
|
+
//
|
|
18
|
+
// The whole algorithm matrix comes from the shared sign-scheme resolver (the same registry
|
|
19
|
+
// pki.cms.sign / pki.tsp.sign drive), so a new algorithm is a registry row, never a branch here. The
|
|
20
|
+
// TBS + extension DER is hand-assembled through the canonical asn1.build.* layer (the shipped
|
|
21
|
+
// cms/tsp/ocsp producing pattern); the strict schema-x509 decoder round-trips it, and that round-trip
|
|
22
|
+
// -- plus OpenSSL interop -- is the divergence guard.
|
|
23
|
+
|
|
24
|
+
var nodeCrypto = require("crypto");
|
|
25
|
+
var asn1 = require("./asn1-der");
|
|
26
|
+
var oid = require("./oid");
|
|
27
|
+
var x509 = require("./schema-x509");
|
|
28
|
+
var signScheme = require("./sign-scheme");
|
|
29
|
+
var compositeSig = require("./composite-sig");
|
|
30
|
+
var guard = require("./guard-all");
|
|
31
|
+
var frameworkError = require("./framework-error");
|
|
32
|
+
var schema = require("./schema-engine");
|
|
33
|
+
var pkix = require("./schema-pkix");
|
|
34
|
+
|
|
35
|
+
var CertificateError = frameworkError.CertificateError;
|
|
36
|
+
// The x509 schema namespace + Name parser -- the SAME RDNSequence parser pki.schema.x509.parse uses, so
|
|
37
|
+
// a raw Name DER is validated fully (structure, DirectoryString types) with the frozen x509/* codes.
|
|
38
|
+
var NS = pkix.makeNS("x509", CertificateError, oid);
|
|
39
|
+
var NAME_SCHEMA = pkix.name(NS);
|
|
40
|
+
var SPKI_SCHEMA = pkix.spki(NS);
|
|
41
|
+
// The SAME RFC 5280 sec. 4.2.1 extension value decoders pki.schema.x509.parse uses, so a recognized
|
|
42
|
+
// pre-encoded (array-form) extension is fully validated with the frozen x509/* codes.
|
|
43
|
+
var EXT_DECODERS = pkix.certExtensionDecoders(NS).byOid;
|
|
44
|
+
var b = asn1.build;
|
|
45
|
+
// Two error factories (cms-sign.js pattern): `_err` takes a full x509/* code; `_signE` prepends the
|
|
46
|
+
// domain so the shared sign-scheme resolver/signer faults keep the x509/* codes. Both are FACTORIES
|
|
47
|
+
// -- guard.time.assertValid and resolveSignScheme invoke them as `E(code, msg)` with no `new`.
|
|
48
|
+
function _err(code, message, cause) { return new CertificateError(code, message, cause); }
|
|
49
|
+
function _signE(kind, message, cause) { return new CertificateError("x509/" + kind, message, cause); }
|
|
50
|
+
function O(n) { return oid.byName(n); }
|
|
51
|
+
|
|
52
|
+
var OID_SKI = O("subjectKeyIdentifier");
|
|
53
|
+
|
|
54
|
+
// The recognized keys of the `extensions` spec object; an unknown key is a typo and throws at
|
|
55
|
+
// config-time (a custom extension is passed as pre-encoded DER via the array form).
|
|
56
|
+
var KNOWN_EXT_KEYS = {
|
|
57
|
+
subjectKeyIdentifier: 1, authorityKeyIdentifier: 1, keyUsage: 1, keyUsageCritical: 1,
|
|
58
|
+
extendedKeyUsage: 1, extendedKeyUsageCritical: 1, basicConstraints: 1, subjectAltName: 1,
|
|
59
|
+
certificatePolicies: 1, certificatePoliciesCritical: 1,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// KeyUsage named-bit positions (RFC 5280 sec. 4.2.1.3); contentCommitment is the RFC 5280 rename of
|
|
63
|
+
// the X.509 nonRepudiation bit (1).
|
|
64
|
+
var KU_BIT = {
|
|
65
|
+
digitalSignature: 0, nonRepudiation: 1, contentCommitment: 1, keyEncipherment: 2,
|
|
66
|
+
dataEncipherment: 3, keyAgreement: 4, keyCertSign: 5, cRLSign: 6, encipherOnly: 7, decipherOnly: 8,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// ---- distinguished name encoding (RFC 5280 sec. 4.1.2.4) -------------------
|
|
70
|
+
|
|
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
|
+
function _skiValueOf(caCert) {
|
|
236
|
+
var ext = (caCert.extensions || []).filter(function (e) { return e.oid === OID_SKI; })[0];
|
|
237
|
+
if (ext) { try { return asn1.read.octetString(asn1.decode(ext.value)); } catch (_e) { /* fall through to re-derive from the issuer SPKI */ } }
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
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
|
+
function _akiKeyId(val, ctx) {
|
|
246
|
+
if (Buffer.isBuffer(val)) return val;
|
|
247
|
+
if (val === true) {
|
|
248
|
+
if (ctx.issuerCert) { var ski = _skiValueOf(ctx.issuerCert); if (ski) return ski; }
|
|
249
|
+
return _spkiKeyId(ctx.issuerSpki);
|
|
250
|
+
}
|
|
251
|
+
throw _err("x509/bad-input", "authorityKeyIdentifier must be true (auto-derive from the issuer) or a Buffer key id");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Build the extensions [3] block from the spec object (or pass through an array of pre-encoded
|
|
255
|
+
// Extension DER buffers). Enforces the RFC 5280 CA cross-field gates, then emits a deterministic order.
|
|
256
|
+
function _buildExtensions(extSpec, ctx) {
|
|
257
|
+
if (extSpec == null) return [];
|
|
258
|
+
if (Array.isArray(extSpec)) {
|
|
259
|
+
// Validate each pre-encoded extension, reject a duplicate extnID (RFC 5280 sec. 4.2 -- at most one
|
|
260
|
+
// instance of an extension), and decode basicConstraints + keyUsage to apply the same CA
|
|
261
|
+
// cross-field rules the object form enforces (below), so the array escape hatch cannot bypass them.
|
|
262
|
+
var seenExt = {}, arrCa = false, arrKeyCertSign = false, arrPathLen = false;
|
|
263
|
+
var oidBc = O("basicConstraints"), oidKu = O("keyUsage");
|
|
264
|
+
var arr = extSpec.map(function (e, i) {
|
|
265
|
+
var der = _reqDer(e, "extension");
|
|
266
|
+
_assertValidExtension(der, i);
|
|
267
|
+
var n = asn1.decode(der);
|
|
268
|
+
var extnId = asn1.read.oid(n.children[0]);
|
|
269
|
+
if (seenExt[extnId]) throw _err("x509/bad-input", "duplicate extension " + extnId + " in the extensions array (RFC 5280 sec. 4.2 -- at most one instance of an extension)");
|
|
270
|
+
seenExt[extnId] = true;
|
|
271
|
+
// Fully validate a RECOGNIZED extension by running its real value decoder (which enforces the
|
|
272
|
+
// RFC 5280 sec. 4.2.1 rules and throws a typed x509/* on malformed), and take the CA cross-field
|
|
273
|
+
// inputs from the decoded value; an unrecognized extension is opaque (the caller's own concern).
|
|
274
|
+
var dec = EXT_DECODERS[extnId];
|
|
275
|
+
if (dec) {
|
|
276
|
+
var decoded;
|
|
277
|
+
try { decoded = dec(asn1.read.octetString(n.children[n.children.length - 1])); }
|
|
278
|
+
catch (e) {
|
|
279
|
+
if (e instanceof CertificateError) throw e;
|
|
280
|
+
throw _err("x509/bad-input", "pre-encoded " + (oid.name(extnId) || extnId) + " extension value is malformed", e);
|
|
281
|
+
}
|
|
282
|
+
if (extnId === oidBc) {
|
|
283
|
+
arrCa = decoded.cA === true; arrPathLen = decoded.pathLenConstraint != null;
|
|
284
|
+
// RFC 5280 sec. 4.2.1.9 -- a CA's basicConstraints MUST be critical (3 children = an explicit
|
|
285
|
+
// TRUE critical flag, since _assertValidExtension already rejects an explicit FALSE).
|
|
286
|
+
if (arrCa && n.children.length !== 3) throw _err("x509/bad-input", "a CA certificate's basicConstraints MUST be critical (RFC 5280 sec. 4.2.1.9)");
|
|
287
|
+
}
|
|
288
|
+
else if (extnId === oidKu) { arrKeyCertSign = decoded.keyCertSign === true; }
|
|
289
|
+
}
|
|
290
|
+
return b.raw(der);
|
|
291
|
+
});
|
|
292
|
+
if (arrKeyCertSign && !arrCa) throw _err("x509/bad-input", "keyUsage keyCertSign requires basicConstraints cA=TRUE (RFC 5280 sec. 4.2.1.3)");
|
|
293
|
+
if (arrPathLen && (!arrCa || !arrKeyCertSign)) throw _err("x509/bad-input", "basicConstraints pathLenConstraint requires cA=TRUE and keyUsage keyCertSign (RFC 5280 sec. 4.2.1.9)");
|
|
294
|
+
return arr;
|
|
295
|
+
}
|
|
296
|
+
if (typeof extSpec !== "object") throw _err("x509/bad-input", "extensions must be an object or an array of pre-encoded Extension DER");
|
|
297
|
+
// Reject a typo'd / unsupported extension key at config-time rather than silently dropping it (a
|
|
298
|
+
// misspelled `keyUsag` would otherwise omit keyUsage). A custom extension goes in the array form.
|
|
299
|
+
Object.keys(extSpec).forEach(function (k) {
|
|
300
|
+
if (!KNOWN_EXT_KEYS[k]) throw _err("x509/bad-input", "unknown extension " + JSON.stringify(k) + " in the extensions spec; pass a pre-encoded Extension DER via the array form for a custom extension");
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
var bc = extSpec.basicConstraints;
|
|
304
|
+
if (bc != null) _validateBcSpec(bc);
|
|
305
|
+
var caTrue = !!(bc && bc.cA === true);
|
|
306
|
+
var ku = extSpec.keyUsage;
|
|
307
|
+
var assertsKeyCertSign = Array.isArray(ku) && ku.indexOf("keyCertSign") >= 0;
|
|
308
|
+
// RFC 5280 sec. 4.2.1.3 -- keyCertSign requires basicConstraints cA=TRUE.
|
|
309
|
+
if (assertsKeyCertSign && !caTrue) throw _err("x509/bad-input", "keyUsage keyCertSign requires basicConstraints cA=TRUE (RFC 5280 sec. 4.2.1.3)");
|
|
310
|
+
// RFC 5280 sec. 4.2.1.9 -- pathLenConstraint requires cA=TRUE AND keyCertSign.
|
|
311
|
+
if (bc && bc.pathLen != null) {
|
|
312
|
+
if (!caTrue) throw _err("x509/bad-input", "basicConstraints pathLenConstraint requires cA=TRUE (RFC 5280 sec. 4.2.1.9)");
|
|
313
|
+
if (!assertsKeyCertSign) throw _err("x509/bad-input", "basicConstraints pathLenConstraint requires keyUsage keyCertSign (RFC 5280 sec. 4.2.1.9)");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
var out = [];
|
|
317
|
+
if (extSpec.subjectKeyIdentifier != null) out.push(_ext(O("subjectKeyIdentifier"), false, _extSki(_skiKeyId(extSpec.subjectKeyIdentifier, ctx.spki))));
|
|
318
|
+
if (extSpec.authorityKeyIdentifier != null) out.push(_ext(O("authorityKeyIdentifier"), false, _extAki(_akiKeyId(extSpec.authorityKeyIdentifier, ctx))));
|
|
319
|
+
if (ku != null) out.push(_ext(O("keyUsage"), extSpec.keyUsageCritical !== false, _extKeyUsage(ku)));
|
|
320
|
+
if (extSpec.extendedKeyUsage != null) out.push(_ext(O("extKeyUsage"), !!extSpec.extendedKeyUsageCritical, _extExtKeyUsage(extSpec.extendedKeyUsage)));
|
|
321
|
+
if (bc != null) {
|
|
322
|
+
// RFC 5280 sec. 4.2.1.9 -- a CA certificate's basicConstraints MUST be critical.
|
|
323
|
+
if (bc.cA === true && bc.critical === false) throw _err("x509/bad-input", "a CA certificate's basicConstraints MUST be critical (RFC 5280 sec. 4.2.1.9)");
|
|
324
|
+
out.push(_ext(O("basicConstraints"), bc.critical !== false, _extBasicConstraints(bc)));
|
|
325
|
+
}
|
|
326
|
+
if (extSpec.subjectAltName != null) out.push(_ext(O("subjectAltName"), ctx.subjectEmpty, _extSan(extSpec.subjectAltName)));
|
|
327
|
+
if (extSpec.certificatePolicies != null) out.push(_ext(O("certificatePolicies"), !!extSpec.certificatePoliciesCritical, _extCertPolicies(extSpec.certificatePolicies)));
|
|
328
|
+
return out;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ---- serial + validity + key plumbing --------------------------------------
|
|
332
|
+
|
|
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
|
+
// RFC 5280 sec. 4.1.2.5 -- UTCTime for 1950..2049, GeneralizedTime otherwise. UTCTime's two-digit year
|
|
353
|
+
// only represents 1950..2049 (sec. 4.1.2.5.1), so a pre-1950 date MUST use GeneralizedTime (the same
|
|
354
|
+
// arm as a date from 2050 on); using UTCTime for a pre-1950 year would be unrepresentable.
|
|
355
|
+
function _timeDer(date, which) {
|
|
356
|
+
guard.time.assertValid(date, _err, "x509/bad-input", "certificate " + which);
|
|
357
|
+
var y = date.getUTCFullYear();
|
|
358
|
+
return (y >= 1950 && y <= 2049) ? b.utcTime(date) : b.generalizedTime(date);
|
|
359
|
+
}
|
|
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
|
+
// A supplied issuer certificate MUST be a CA that may sign certificates: basicConstraints present AND
|
|
420
|
+
// critical AND cA=TRUE (RFC 5280 sec. 4.2.1.9), and -- when a keyUsage extension is present -- the
|
|
421
|
+
// keyCertSign bit (sec. 4.2.1.3). Refuse a non-CA issuer rather than mint a certificate that will not
|
|
422
|
+
// chain. Returns the issuer's pathLenConstraint (or null) so the caller can honor it.
|
|
423
|
+
function _assertIssuerIsCa(issuerCert) {
|
|
424
|
+
var exts = issuerCert.extensions || [];
|
|
425
|
+
function ext(name) { return exts.filter(function (x) { return x.oid === O(name); })[0] || null; }
|
|
426
|
+
var bcExt = ext("basicConstraints");
|
|
427
|
+
if (!bcExt) throw _err("x509/bad-input", "the issuer certificate is not a CA -- it has no basicConstraints extension (RFC 5280 sec. 4.2.1.9)");
|
|
428
|
+
if (bcExt.critical !== true) throw _err("x509/bad-input", "the issuer certificate basicConstraints is not marked critical (RFC 5280 sec. 4.2.1.9 requires it on a CA)");
|
|
429
|
+
var bc;
|
|
430
|
+
try { bc = EXT_DECODERS[O("basicConstraints")](bcExt.value); }
|
|
431
|
+
catch (e) { if (e instanceof CertificateError) throw e; throw _err("x509/bad-input", "the issuer certificate basicConstraints is malformed", e); }
|
|
432
|
+
if (bc.cA !== true) throw _err("x509/bad-input", "the issuer certificate is not a CA (basicConstraints cA is not TRUE)");
|
|
433
|
+
var kuExt = ext("keyUsage");
|
|
434
|
+
if (kuExt) {
|
|
435
|
+
var ku;
|
|
436
|
+
try { ku = EXT_DECODERS[O("keyUsage")](kuExt.value); }
|
|
437
|
+
catch (e) { if (e instanceof CertificateError) throw e; throw _err("x509/bad-input", "the issuer certificate keyUsage is malformed", e); }
|
|
438
|
+
if (ku.keyCertSign !== true) throw _err("x509/bad-input", "the issuer certificate keyUsage does not assert keyCertSign -- it cannot sign certificates (RFC 5280 sec. 4.2.1.3)");
|
|
439
|
+
}
|
|
440
|
+
return bc.pathLenConstraint; // BigInt-narrowed integer or null
|
|
441
|
+
}
|
|
442
|
+
// The cA flag + pathLenConstraint of the certificate being issued, from either the object or the
|
|
443
|
+
// pre-encoded array extensions form (used to honor a supplied issuer's pathLenConstraint).
|
|
444
|
+
function _issuedCaInfo(extSpec) {
|
|
445
|
+
if (extSpec == null) return { cA: false, pathLen: null };
|
|
446
|
+
if (!Array.isArray(extSpec)) {
|
|
447
|
+
var bc = extSpec.basicConstraints;
|
|
448
|
+
return { cA: !!(bc && bc.cA === true), pathLen: bc && bc.pathLen != null ? Number(bc.pathLen) : null };
|
|
449
|
+
}
|
|
450
|
+
for (var i = 0; i < extSpec.length; i++) {
|
|
451
|
+
var n = asn1.decode(_reqDer(extSpec[i], "extension"));
|
|
452
|
+
if (asn1.read.oid(n.children[0]) !== O("basicConstraints")) continue;
|
|
453
|
+
var dec = EXT_DECODERS[O("basicConstraints")](asn1.read.octetString(n.children[n.children.length - 1]));
|
|
454
|
+
return { cA: dec.cA === true, pathLen: dec.pathLenConstraint != null ? Number(dec.pathLenConstraint) : null };
|
|
455
|
+
}
|
|
456
|
+
return { cA: false, pathLen: null };
|
|
457
|
+
}
|
|
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
|
+
// Does the extensions spec carry a subjectAltName that will be emitted critical? The object form forces
|
|
486
|
+
// SAN critical when the subject is empty (so any subjectAltName qualifies); the pre-encoded array form
|
|
487
|
+
// is decoded to look for an Extension whose extnID is subjectAltName with a TRUE critical flag.
|
|
488
|
+
function _hasCriticalSan(extSpec) {
|
|
489
|
+
if (extSpec == null) return false;
|
|
490
|
+
if (!Array.isArray(extSpec)) return !!extSpec.subjectAltName;
|
|
491
|
+
var sanOid = O("subjectAltName");
|
|
492
|
+
for (var i = 0; i < extSpec.length; i++) {
|
|
493
|
+
var n = asn1.decode(_reqDer(extSpec[i], "extension"));
|
|
494
|
+
if (n.children.length === 3 && asn1.read.oid(n.children[0]) === sanOid && asn1.read.boolean(n.children[1]) === true) return true;
|
|
495
|
+
}
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// ---- the primitive ---------------------------------------------------------
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* @primitive pki.x509.sign
|
|
503
|
+
* @signature pki.x509.sign(spec, issuer, opts?) -> Promise<Buffer|string>
|
|
504
|
+
* @since 0.3.0
|
|
505
|
+
* @status stable
|
|
506
|
+
* @spec RFC 5280 sec. 4, RFC 9909, RFC 9814
|
|
507
|
+
* @defends forged-certificate-issuance (CWE-347)
|
|
508
|
+
* @related pki.schema.x509.parse, pki.path.validate, pki.cms.sign
|
|
509
|
+
*
|
|
510
|
+
* Build, sign, and DER-encode an X.509 certificate. `spec` describes the certificate to issue --
|
|
511
|
+
* `subject` (a string CN, an array of RDNs, or raw Name DER), `subjectPublicKey` (the SPKI DER of the
|
|
512
|
+
* key being certified), `notBefore` / `notAfter` (`Date`s), an optional `serialNumber`, and an optional
|
|
513
|
+
* `extensions` object. `issuer` is the signing side: `{ key }` alone issues a self-signed certificate
|
|
514
|
+
* (issuer = subject, signed with the subject's own key); `{ name, publicKey, key }` or `{ cert, key }`
|
|
515
|
+
* issues a CA-signed one. The signature algorithm is resolved from the signing key -- RSA (PKCS#1 v1.5
|
|
516
|
+
* or PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, or a composite arm -- so every algorithm the
|
|
517
|
+
* toolkit signs with is available here without a per-algorithm branch.
|
|
518
|
+
*
|
|
519
|
+
* The version is derived from the field set (v3 when extensions are present, else v1). Serial bounds
|
|
520
|
+
* (positive, <= 20 octets), the validity UTCTime/GeneralizedTime cutover, the DER DEFAULT omissions
|
|
521
|
+
* (v1 tag, `critical=FALSE`, `cA=FALSE`), and the CA cross-field rules (keyCertSign and
|
|
522
|
+
* pathLenConstraint require cA=TRUE) are all enforced; a violation throws a typed `CertificateError`.
|
|
523
|
+
*
|
|
524
|
+
* @opts
|
|
525
|
+
* - `pem` (boolean) -- return a PEM `CERTIFICATE` string instead of DER.
|
|
526
|
+
* - `pss` (boolean) -- sign an RSA key with RSASSA-PSS rather than PKCS#1 v1.5.
|
|
527
|
+
* - `digestAlgorithm` (string) -- override the message digest where the algorithm permits a choice.
|
|
528
|
+
* @example
|
|
529
|
+
* var root = await pki.x509.sign(
|
|
530
|
+
* { subject: "Example Root CA", subjectPublicKey: signerSpki,
|
|
531
|
+
* notBefore: new Date("2026-01-01T00:00:00Z"), notAfter: new Date("2036-01-01T00:00:00Z"),
|
|
532
|
+
* extensions: { basicConstraints: { cA: true }, keyUsage: ["keyCertSign", "cRLSign"], subjectKeyIdentifier: true } },
|
|
533
|
+
* { key: signerKeyPkcs8 });
|
|
534
|
+
* pki.schema.x509.parse(root).subject.dn; // "CN=Example Root CA"
|
|
535
|
+
*/
|
|
536
|
+
function sign(spec, issuer, opts) {
|
|
537
|
+
return Promise.resolve().then(function () { return _sign(spec, issuer, opts); });
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function _sign(spec, issuer, opts) {
|
|
541
|
+
opts = opts || {};
|
|
542
|
+
if (!spec || typeof spec !== "object" || Buffer.isBuffer(spec)) throw _err("x509/bad-input", "the certificate spec must be an object");
|
|
543
|
+
issuer = issuer || {};
|
|
544
|
+
if (issuer.key == null) throw _err("x509/bad-input", "a signing key (issuer.key, a PKCS#8 private key) is required");
|
|
545
|
+
|
|
546
|
+
var spki = _reqDer(spec.subjectPublicKey, "spec.subjectPublicKey (the SPKI DER of the certified key)");
|
|
547
|
+
_assertValidSpki(spki, "spec.subjectPublicKey");
|
|
548
|
+
var subjectDer = _encodeName(spec.subject == null ? [] : spec.subject);
|
|
549
|
+
var subjectEmpty = _isEmptyName(subjectDer);
|
|
550
|
+
|
|
551
|
+
// Resolve the issuer name + signing-key SPKI. `{ key }` alone -> self-signed.
|
|
552
|
+
var issuerDer, issuerSpki, issuerCert = null, issuerPathLen = null;
|
|
553
|
+
var selfSigned = issuer.name == null && issuer.cert == null && issuer.publicKey == null;
|
|
554
|
+
if (selfSigned) {
|
|
555
|
+
issuerDer = subjectDer;
|
|
556
|
+
issuerSpki = spki;
|
|
557
|
+
} else if (issuer.cert != null) {
|
|
558
|
+
issuerCert = (Buffer.isBuffer(issuer.cert) || typeof issuer.cert === "string") ? x509.parse(issuer.cert) : issuer.cert;
|
|
559
|
+
if (!issuerCert || !issuerCert.tbsBytes) throw _err("x509/bad-input", "issuer.cert must be a certificate DER/PEM or a parsed certificate");
|
|
560
|
+
issuerPathLen = _assertIssuerIsCa(issuerCert);
|
|
561
|
+
issuerDer = _caSubjectBytes(issuerCert);
|
|
562
|
+
issuerSpki = issuerCert.subjectPublicKeyInfo.bytes;
|
|
563
|
+
} else {
|
|
564
|
+
issuerDer = _encodeName(issuer.name == null ? [] : issuer.name);
|
|
565
|
+
issuerSpki = _reqDer(issuer.publicKey, "issuer.publicKey (the issuer SPKI DER)");
|
|
566
|
+
_assertValidSpki(issuerSpki, "issuer.publicKey");
|
|
567
|
+
}
|
|
568
|
+
// RFC 5280 sec. 4.1.2.4 -- the issuer MUST be a non-empty distinguished name.
|
|
569
|
+
if (_isEmptyName(issuerDer)) throw _err("x509/bad-issuer", "issuer must be a non-empty distinguished name");
|
|
570
|
+
|
|
571
|
+
// Resolve the signature scheme from the SIGNING key's SPKI algorithm (the whole registry, for free).
|
|
572
|
+
var scheme = signScheme.resolveSignScheme(_certLikeFromSpki(issuerSpki), { combinedRsaSig: true, pss: opts.pss, digestAlgorithm: opts.digestAlgorithm }, true, _signE);
|
|
573
|
+
|
|
574
|
+
var serialTlv = _serialInteger(spec.serialNumber);
|
|
575
|
+
// Validate both instants, then reject an inverted window before encoding (a notBefore after notAfter
|
|
576
|
+
// is a nonsensical validity period, RFC 5280 sec. 4.1.2.5).
|
|
577
|
+
guard.time.assertValid(spec.notBefore, _err, "x509/bad-input", "notBefore");
|
|
578
|
+
guard.time.assertValid(spec.notAfter, _err, "x509/bad-input", "notAfter");
|
|
579
|
+
// allow:nan-date-comparison-unguarded -- both operands are guard.time.assertValid'd on the two lines above (an Invalid Date throws before this comparison).
|
|
580
|
+
if (spec.notBefore.getTime() > spec.notAfter.getTime()) throw _err("x509/bad-input", "notBefore must not be after notAfter (RFC 5280 sec. 4.1.2.5)");
|
|
581
|
+
var validityDer = b.sequence([_timeDer(spec.notBefore, "notBefore"), _timeDer(spec.notAfter, "notAfter")]);
|
|
582
|
+
|
|
583
|
+
var exts = _buildExtensions(spec.extensions, { spki: spki, issuerSpki: issuerSpki, issuerCert: issuerCert, subjectEmpty: subjectEmpty });
|
|
584
|
+
// RFC 5280 sec. 4.1.2.6 -- an empty subject requires a critical subjectAltName (recognized in both the
|
|
585
|
+
// object form and a pre-encoded array-form extension).
|
|
586
|
+
if (subjectEmpty && !_hasCriticalSan(spec.extensions)) {
|
|
587
|
+
throw _err("x509/bad-input", "an empty subject requires a critical subjectAltName (RFC 5280 sec. 4.1.2.6)");
|
|
588
|
+
}
|
|
589
|
+
// Honor a supplied issuer's pathLenConstraint (RFC 5280 sec. 4.2.1.9): a CA certificate issued below
|
|
590
|
+
// it consumes one unit of path length, so pathLen=0 forbids issuing a CA below it, and an issued CA's
|
|
591
|
+
// own pathLenConstraint must leave room within the issuer's remaining depth.
|
|
592
|
+
if (issuerPathLen != null) {
|
|
593
|
+
var issued = _issuedCaInfo(spec.extensions);
|
|
594
|
+
// A self-issued CA certificate (subject == issuer, e.g. a CA key rollover) does NOT consume path
|
|
595
|
+
// length (RFC 5280 sec. 6.1 counts only non-self-issued intermediates), so it is permitted even at
|
|
596
|
+
// pathLen 0. Compare canonically (sec. 7.1) via the parsed RDN sequences.
|
|
597
|
+
var selfIssued = guard.name.dnEqual(
|
|
598
|
+
schema.walk(NAME_SCHEMA, asn1.decode(subjectDer), NS).result.rdns,
|
|
599
|
+
schema.walk(NAME_SCHEMA, asn1.decode(issuerDer), NS).result.rdns,
|
|
600
|
+
_err, "x509/bad-input", "issuer/subject DN");
|
|
601
|
+
if (issued.cA && !selfIssued) {
|
|
602
|
+
if (issuerPathLen < 1) throw _err("x509/bad-input", "the issuer certificate pathLenConstraint (0) forbids issuing a non-self-issued CA certificate below it (RFC 5280 sec. 4.2.1.9)");
|
|
603
|
+
if (issued.pathLen != null && issued.pathLen > issuerPathLen - 1) throw _err("x509/bad-input", "the issued CA certificate pathLenConstraint exceeds the issuer's remaining path length (RFC 5280 sec. 4.2.1.9)");
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
// Version is derived from the emitted field set (RFC 5280 sec. 4.1.2.1); the builder never emits
|
|
607
|
+
// unique identifiers, so extensions => v3, otherwise v1 (the [0] tag is omitted under DER DEFAULT).
|
|
608
|
+
var version = exts.length ? 3 : 1;
|
|
609
|
+
|
|
610
|
+
var tbsChildren = [];
|
|
611
|
+
if (version !== 1) tbsChildren.push(b.explicit(0, b.integer(BigInt(version - 1)))); // v2->INTEGER 1, v3->INTEGER 2
|
|
612
|
+
tbsChildren.push(serialTlv);
|
|
613
|
+
tbsChildren.push(scheme.sigAlgId); // signature == signatureAlgorithm (RFC 5280 sec. 4.1.1.2), single source
|
|
614
|
+
tbsChildren.push(issuerDer);
|
|
615
|
+
tbsChildren.push(validityDer);
|
|
616
|
+
tbsChildren.push(subjectDer);
|
|
617
|
+
tbsChildren.push(b.raw(spki));
|
|
618
|
+
if (exts.length) tbsChildren.push(b.explicit(3, b.sequence(exts)));
|
|
619
|
+
var tbsDer = b.sequence(tbsChildren);
|
|
620
|
+
|
|
621
|
+
return signScheme.signOverTbs(scheme, issuer.key, tbsDer, _signE).then(function (sig) {
|
|
622
|
+
// The signature MUST verify under the issuer public key, or the certificate would not chain (the
|
|
623
|
+
// composite arm returns a promise; the classical/PQC path throws synchronously on a mismatch).
|
|
624
|
+
return Promise.resolve(_assertCertVerifies(tbsDer, sig, issuerSpki, scheme)).then(function () {
|
|
625
|
+
var certDer = b.sequence([tbsDer, scheme.sigAlgId, b.bitString(sig, 0)]);
|
|
626
|
+
return opts.pem ? x509.pemEncode(certDer, "CERTIFICATE") : certDer;
|
|
627
|
+
});
|
|
628
|
+
}, function (e) {
|
|
629
|
+
// A signing failure at a well-formed tbs is a bad signing key or a key/algorithm mismatch; keep a
|
|
630
|
+
// typed CertificateError (composite key-shape faults already are), and re-type a raw WebCrypto
|
|
631
|
+
// rejection to x509/bad-input rather than leaking a DOMException from the boundary.
|
|
632
|
+
if (e instanceof CertificateError) throw e;
|
|
633
|
+
throw _err("x509/bad-input", "signing the certificate failed -- the signing key does not match the resolved algorithm or is invalid", e);
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
module.exports = { sign: sign };
|
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:6f299c9a-151b-4a48-9580-bc19e1ab66b0",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-17T06:00:11.553Z",
|
|
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.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.3.0",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.
|
|
25
|
+
"version": "0.3.0",
|
|
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.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.3.0",
|
|
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.
|
|
57
|
+
"ref": "@blamejs/pki@0.3.0",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|