@blamejs/pki 0.3.0 → 0.3.1

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 CHANGED
@@ -4,7 +4,19 @@ 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
7
+ ## v0.3.1 — 2026-07-17
8
+
9
+ pki.csr.sign issues PKCS#10 certification requests -- self-signed by the subject key for proof of possession, over every signature algorithm the toolkit supports.
10
+
11
+ ### Added
12
+
13
+ - pki.csr.sign(spec, key, opts) builds and signs a PKCS#10 certification request -- self-signed by the subject's own key for proof of possession -- and returns DER, or a PEM CERTIFICATE REQUEST with opts.pem. The subject may be empty; requested v3 extensions (subject alternative names, key usage, extended key usage, basic constraints, certificate policies, subject key identifier, or pre-encoded Extension DER) ride in a PKCS#9 extensionRequest attribute a CA copies into the issued certificate, and an optional challengePassword is carried too. The signing algorithm is resolved from the subject key: RSA PKCS#1 v1.5 / PSS, ECDSA, EdDSA, ML-DSA, SLH-DSA, or a composite arm. Request parsing remains pki.schema.csr.parse.
14
+
15
+ ### Fixed
16
+
17
+ - The certificate and certification-request distinguished-name and extension encoders now reject an unrecognized attribute, extended-key-usage purpose, or certificate-policy name at build time with a typed error, rather than emitting a malformed object identifier -- an unknown name in a pki.x509.sign or pki.csr.sign spec fails closed instead of producing an unparseable structure.
18
+
19
+ ## v0.3.0 — 2026-07-16
8
20
 
9
21
  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
22
 
package/README.md CHANGED
@@ -223,6 +223,7 @@ is callable today; nothing below is a stub.
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
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` |
226
+ | `pki.csr` | PKCS#10 certification-request issuance (RFC 2986 / RFC 2985) — `sign(spec, key, opts)` builds and signs a `CertificationRequest`: a `spec` of subject (a common-name string, an array of RDNs, or raw Name DER; may be empty), the public key being certified, an optional `extensionRequest` (requested v3 extensions — subject alternative names, key usage, extended key usage, basic constraints, certificate policies, subject key identifier, or an array of pre-encoded Extension DER — that a CA copies into the issued certificate), and an optional `challengePassword`. `key` (or `{ key }`) is the subject's own private key: the request is self-signed to prove possession of the private half of `subjectPublicKey`, and that proof is verified before the request is returned (what `openssl req -verify` checks). The signature algorithm is resolved from the subject key, so RSA (PKCS#1 v1.5 / PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch. Returns DER, or a PEM `CERTIFICATE REQUEST` with `opts.pem`; malformed input throws a typed `CsrError`. Parsing stays at `pki.schema.csr.parse` — `sign` |
226
227
  | `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` |
227
228
  | `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` |
228
229
  | `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
@@ -41,6 +41,7 @@ var smime = require("./lib/smime");
41
41
  var tsp = require("./lib/tsp-sign");
42
42
  var ocsp = require("./lib/ocsp");
43
43
  var x509 = require("./lib/x509-sign");
44
+ var csr = require("./lib/csr-sign");
44
45
  var merkle = require("./lib/merkle");
45
46
  var shbs = require("./lib/shbs");
46
47
  var hpke = require("./lib/hpke");
@@ -87,6 +88,11 @@ module.exports = {
87
88
  // signature algorithm the registry resolves (RSA/ECDSA/EdDSA/ML-DSA/SLH-DSA/composite).
88
89
  // Parsing lives at pki.schema.x509.parse.
89
90
  x509: x509,
91
+ // `csr` is the PKCS#10 certification-request producing side -- pki.csr.sign builds and
92
+ // signs a CertificationRequest (RFC 2986), self-signed by the subject key for proof of
93
+ // possession, with requested extensions in a PKCS#9 extensionRequest attribute. Parsing
94
+ // lives at pki.schema.csr.parse.
95
+ csr: csr,
90
96
  // `merkle` is the RFC 6962 / RFC 9162 Merkle-tree proof-verification core --
91
97
  // pki.merkle.leafHash / nodeHash / emptyRootHash build the domain-separated
92
98
  // (0x00 leaf / 0x01 node) SHA-256 tree hashes; pki.merkle.verifyInclusion and
@@ -0,0 +1,209 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+
5
+ /**
6
+ * @module pki.csr
7
+ * @nav Signing
8
+ * @title Certification requests
9
+ * @intro The PKCS#10 certification-request producing side. `pki.csr.sign` builds a
10
+ * `CertificationRequestInfo`, signs it with the SUBJECT's own private key (proof of possession -- a
11
+ * CSR has no issuer), and emits a `CertificationRequest` (RFC 2986) that `pki.schema.csr.parse`,
12
+ * OpenSSL, and a CA enrollment pipeline all accept. Requested v3 extensions ride in a PKCS#9
13
+ * `extensionRequest` attribute (RFC 2985) a CA copies into the issued certificate. Parsing lives at
14
+ * `pki.schema.csr.parse`.
15
+ * @spec RFC 2986
16
+ * @card Build and sign a PKCS#10 certification request (proof of possession by the subject key).
17
+ */
18
+ //
19
+ // The signature algorithm resolves from the SUBJECT public key through the shared sign-scheme registry
20
+ // (RSA / ECDSA / EdDSA / ML-DSA / SLH-DSA / composite), so there is no per-algorithm branch here. The
21
+ // distinguished-name / extension / SPKI encoders + the post-sign self-check are the shared lib/pki-build
22
+ // primitives (the same ones pki.x509.sign composes), bound to the csr namespace.
23
+
24
+ var asn1 = require("./asn1-der");
25
+ var oid = require("./oid");
26
+ var csr = require("./schema-csr");
27
+ var signScheme = require("./sign-scheme");
28
+ var pkix = require("./schema-pkix");
29
+ var pkiBuild = require("./pki-build");
30
+ var frameworkError = require("./framework-error");
31
+
32
+ var CsrError = frameworkError.CsrError;
33
+ var b = asn1.build;
34
+ function _err(code, message, cause) { return new CsrError(code, message, cause); }
35
+ function _signE(kind, message, cause) { return new CsrError("csr/" + kind, message, cause); }
36
+ function O(n) { return oid.byName(n); }
37
+
38
+ var NS = pkix.makeNS("csr", CsrError, oid);
39
+ var EXT_DECODERS = pkix.certExtensionDecoders(NS).byOid;
40
+ var _b = pkiBuild.makeBuilder({
41
+ ErrorClass: CsrError, prefix: "csr", O: O, NS: NS,
42
+ NAME_SCHEMA: pkix.name(NS), SPKI_SCHEMA: pkix.spki(NS), EXT_DECODERS: EXT_DECODERS,
43
+ });
44
+
45
+ // The requested-extension object keys (a CSR REQUESTS extensions; it does NOT enforce issuance policy,
46
+ // so there are no CA cross-field gates and no authorityKeyIdentifier auto-derivation).
47
+ var KNOWN_REQ_EXT_KEYS = {
48
+ subjectAltName: 1, keyUsage: 1, keyUsageCritical: 1, extendedKeyUsage: 1, extendedKeyUsageCritical: 1,
49
+ basicConstraints: 1, certificatePolicies: 1, certificatePoliciesCritical: 1, subjectKeyIdentifier: 1,
50
+ };
51
+ // challengePassword DirectoryString charset (PrintableString set); anything else uses UTF8String.
52
+ var PRINTABLE_RE = /^[A-Za-z0-9 '()+,\-./:=?]*$/;
53
+
54
+ // attributes ::= [0] IMPLICIT SET OF Attribute (RFC 2986). The IMPLICIT tag replaces the universal SET,
55
+ // so it is a context-constructed [0] whose DER-sorted members ARE the SET-OF; empty -> A0 00.
56
+ function _implicitSetOf0(members) {
57
+ return b.contextConstructed(0, Buffer.concat(members.slice().sort(Buffer.compare)));
58
+ }
59
+
60
+ // The requested v3 Extensions (a bare SEQUENCE OF Extension, RFC 2985 sec. 5.4.2) from the object form,
61
+ // or an array of pre-encoded Extension DER. Reuses the shared extension-value encoders; no CA gates.
62
+ function _buildRequestedExtensions(extSpec, subjectSpki) {
63
+ if (Array.isArray(extSpec)) {
64
+ if (!extSpec.length) throw _err("csr/bad-input", "extensionRequest must carry at least one extension");
65
+ var seen = {};
66
+ return b.sequence(extSpec.map(function (e, i) {
67
+ var der = _b.reqDer(e, "extension");
68
+ _b.assertValidExtension(der, i);
69
+ var n = asn1.decode(der);
70
+ var extnId = asn1.read.oid(n.children[0]);
71
+ if (seen[extnId]) throw _err("csr/bad-input", "duplicate extension " + extnId + " in extensionRequest (RFC 5280 sec. 4.2)");
72
+ seen[extnId] = true;
73
+ // Fully validate a RECOGNIZED extension via its real RFC 5280 sec. 4.2.1 value decoder (a malformed
74
+ // value fails closed with the decoder's typed code); an unrecognized extension stays opaque.
75
+ var dec = EXT_DECODERS[extnId];
76
+ if (dec) {
77
+ try { dec(asn1.read.octetString(n.children[n.children.length - 1])); }
78
+ catch (err) { if (err instanceof CsrError) throw err; throw _err("csr/bad-input", "pre-encoded " + (oid.name(extnId) || extnId) + " extension value is malformed", err); }
79
+ }
80
+ return b.raw(der);
81
+ }));
82
+ }
83
+ if (!extSpec || typeof extSpec !== "object") throw _err("csr/bad-input", "extensionRequest must be an object or an array of pre-encoded Extension DER");
84
+ Object.keys(extSpec).forEach(function (k) {
85
+ if (!KNOWN_REQ_EXT_KEYS[k]) throw _err("csr/bad-input", "unknown extensionRequest extension " + JSON.stringify(k) + "; pass a pre-encoded Extension DER via the array form for a custom extension");
86
+ });
87
+ var out = [];
88
+ if (extSpec.subjectKeyIdentifier != null) out.push(_b.ext(O("subjectKeyIdentifier"), false, _b.extSki(_b.skiKeyId(extSpec.subjectKeyIdentifier, subjectSpki))));
89
+ if (extSpec.keyUsage != null) out.push(_b.ext(O("keyUsage"), extSpec.keyUsageCritical !== false, _b.extKeyUsage(extSpec.keyUsage)));
90
+ if (extSpec.extendedKeyUsage != null) out.push(_b.ext(O("extKeyUsage"), !!extSpec.extendedKeyUsageCritical, _b.extExtKeyUsage(extSpec.extendedKeyUsage)));
91
+ if (extSpec.basicConstraints != null) { _b.validateBcSpec(extSpec.basicConstraints); out.push(_b.ext(O("basicConstraints"), extSpec.basicConstraints.critical !== false, _b.extBasicConstraints(extSpec.basicConstraints))); }
92
+ if (extSpec.subjectAltName != null) out.push(_b.ext(O("subjectAltName"), false, _b.extSan(extSpec.subjectAltName)));
93
+ if (extSpec.certificatePolicies != null) out.push(_b.ext(O("certificatePolicies"), !!extSpec.certificatePoliciesCritical, _b.extCertPolicies(extSpec.certificatePolicies)));
94
+ if (!out.length) throw _err("csr/bad-input", "extensionRequest must request at least one extension");
95
+ return b.sequence(out);
96
+ }
97
+
98
+ // challengePassword ::= DirectoryString bounded 1..255 (RFC 2985 sec. 5.4.1). PrintableString when the
99
+ // value is in that charset, else UTF8String; never a Teletex/BMP/Universal string for a new value.
100
+ function _challengePassword(pw) {
101
+ if (typeof pw !== "string" || pw.length < 1 || pw.length > 255) throw _err("csr/bad-input", "challengePassword must be a 1..255 character string (RFC 2985 sec. 5.4.1)");
102
+ return PRINTABLE_RE.test(pw) ? b.printable(pw) : b.utf8(pw);
103
+ }
104
+
105
+ /**
106
+ * @primitive pki.csr.sign
107
+ * @signature pki.csr.sign(spec, key, opts?) -> Promise<Buffer|string>
108
+ * @since 0.3.1
109
+ * @status stable
110
+ * @spec RFC 2986, RFC 2985
111
+ * @defends forged-certification-request (CWE-347)
112
+ * @related pki.schema.csr.parse, pki.x509.sign
113
+ *
114
+ * Build, sign, and DER-encode a PKCS#10 certification request. `spec` describes the request -- `subject`
115
+ * (a common-name string, an array of RDNs, or raw Name DER; MAY be empty), `subjectPublicKey` (the SPKI
116
+ * DER of the key being certified), and optional `extensionRequest` (requested v3 extensions -- an object
117
+ * of subjectAltName / keyUsage / extendedKeyUsage / basicConstraints / certificatePolicies /
118
+ * subjectKeyIdentifier, or an array of pre-encoded Extension DER) and `challengePassword`. `key` (or
119
+ * `{ key }`) is the SUBJECT's own PKCS#8 private key / WebCrypto CryptoKey -- the request is self-signed
120
+ * to prove possession of the private half of `subjectPublicKey`, and that proof is verified before the
121
+ * request is returned. The signature algorithm is resolved from the subject key (RSA PKCS#1 v1.5 or PSS,
122
+ * ECDSA, EdDSA, ML-DSA, SLH-DSA, or a composite arm). Returns DER, or a PEM `CERTIFICATE REQUEST` with
123
+ * `opts.pem`. Malformed input throws a typed `CsrError`. Certificate-request parsing is `pki.schema.csr.parse`.
124
+ *
125
+ * @opts
126
+ * - `pem` (boolean) -- return a PEM `CERTIFICATE REQUEST` string instead of DER.
127
+ * - `pss` (boolean) -- sign an RSA key with RSASSA-PSS rather than PKCS#1 v1.5.
128
+ * - `digestAlgorithm` (string) -- override the message digest where the algorithm permits a choice.
129
+ * @example
130
+ * var req = await pki.csr.sign(
131
+ * { subject: "req.example.com", subjectPublicKey: signerSpki,
132
+ * extensionRequest: { subjectAltName: [{ dNSName: "req.example.com" }] } },
133
+ * { key: signerKeyPkcs8 });
134
+ * pki.schema.csr.parse(req).subject.dn; // "CN=req.example.com"
135
+ */
136
+ function sign(spec, key, opts) {
137
+ return Promise.resolve().then(function () { return _sign(spec, key, opts); });
138
+ }
139
+
140
+ function _sign(spec, key, opts) {
141
+ opts = opts || {};
142
+ if (!spec || typeof spec !== "object" || Buffer.isBuffer(spec)) throw _err("csr/bad-input", "the certification-request spec must be an object");
143
+ // Reject a typo'd spec field at config-time rather than silently dropping it (a misspelled `subjcet`
144
+ // would otherwise yield an empty-subject request).
145
+ Object.keys(spec).forEach(function (k) {
146
+ if (k !== "subject" && k !== "subjectPublicKey" && k !== "extensionRequest" && k !== "challengePassword" && k !== "attributes") throw _err("csr/bad-input", "unknown spec field " + JSON.stringify(k));
147
+ });
148
+ // The signing key is the second argument (a PKCS#8 key / CryptoKey / composite key pair), or { key }.
149
+ var signingKey = (key && typeof key === "object" && !Buffer.isBuffer(key) && !(key instanceof Uint8Array) && key.type == null && "key" in key) ? key.key : key;
150
+ if (signingKey == null) throw _err("csr/bad-input", "a signing key (the subject's private key) is required");
151
+
152
+ var subjectSpki = _b.reqDer(spec.subjectPublicKey, "spec.subjectPublicKey (the SPKI DER of the requested key)");
153
+ _b.assertValidSpki(subjectSpki, "spec.subjectPublicKey");
154
+ var subjectDer = _b.encodeName(spec.subject == null ? [] : spec.subject);
155
+
156
+ // attributes: extensionRequest + challengePassword + pre-encoded opaque Attribute DER (the escape
157
+ // hatch). A recognized attribute may appear at most once (RFC 2985 SINGLE VALUE + producer hygiene).
158
+ var attrs = [], seenAttr = {};
159
+ function addAttr(attrType, valueTlv) {
160
+ if (seenAttr[attrType]) throw _err("csr/bad-input", "duplicate " + (oid.name(attrType) || attrType) + " attribute");
161
+ seenAttr[attrType] = true;
162
+ attrs.push(b.sequence([b.oid(attrType), b.set([valueTlv])])); // Attribute ::= SEQUENCE { type, SET OF value }
163
+ }
164
+ if (spec.extensionRequest != null) addAttr(O("extensionRequest"), _buildRequestedExtensions(spec.extensionRequest, subjectSpki));
165
+ if (spec.challengePassword != null) addAttr(O("challengePassword"), _challengePassword(spec.challengePassword));
166
+ if (spec.attributes != null) {
167
+ if (!Array.isArray(spec.attributes)) throw _err("csr/bad-input", "spec.attributes must be an array of pre-encoded Attribute DER");
168
+ spec.attributes.forEach(function (a, i) {
169
+ var der = _b.reqDer(a, "attribute [" + i + "]");
170
+ var n;
171
+ try { n = asn1.decode(der); }
172
+ catch (e) { throw _err("csr/bad-input", "pre-encoded attribute [" + i + "] is not valid DER", e); }
173
+ if (n.tagNumber !== asn1.TAGS.SEQUENCE || n.tagClass !== "universal" || !n.children || n.children.length !== 2 || n.children[1].tagNumber !== asn1.TAGS.SET) throw _err("csr/bad-input", "pre-encoded attribute [" + i + "] must be an Attribute SEQUENCE { type OID, SET OF value }");
174
+ var at;
175
+ try { at = asn1.read.oid(n.children[0]); }
176
+ catch (e) { throw _err("csr/bad-input", "pre-encoded attribute [" + i + "] type is not an OBJECT IDENTIFIER", e); }
177
+ // A type with a dedicated typed spec field must go through that field so its value syntax is always
178
+ // enforced (RFC 2985 sec. 5.4): the opaque escape hatch is for UNRECOGNIZED attribute types only and
179
+ // must never carry a recognized type with an unvalidated value the strict parser would then reject.
180
+ if (at === O("extensionRequest")) throw _err("csr/bad-input", "pass requested extensions via spec.extensionRequest, not a pre-encoded extensionRequest attribute");
181
+ if (at === O("challengePassword")) throw _err("csr/bad-input", "pass the challenge password via spec.challengePassword, not a pre-encoded challengePassword attribute");
182
+ // Attribute ::= SEQUENCE { type, values SET SIZE(1..MAX) }: an empty value SET is malformed and the
183
+ // strict parser rejects it, so reject at build time -- a signed request must always round-trip.
184
+ if (!n.children[1].children || n.children[1].children.length === 0) throw _err("csr/bad-input", "pre-encoded attribute [" + i + "] value SET must contain at least one value (RFC 2986 SET SIZE(1..MAX))");
185
+ if (seenAttr[at]) throw _err("csr/bad-input", "duplicate " + (oid.name(at) || at) + " attribute");
186
+ seenAttr[at] = true;
187
+ attrs.push(b.raw(der));
188
+ });
189
+ }
190
+
191
+ // Resolve the signature scheme from the SUBJECT public key (the request proves possession of its private half).
192
+ var scheme = signScheme.resolveSignScheme(_b.certLikeFromSpki(subjectSpki), { combinedRsaSig: true, pss: opts.pss, digestAlgorithm: opts.digestAlgorithm }, true, _signE);
193
+
194
+ // CertificationRequestInfo ::= SEQUENCE { version INTEGER(0), subject, subjectPKInfo, attributes [0] }.
195
+ var criDer = b.sequence([b.integer(0n), subjectDer, b.raw(subjectSpki), _implicitSetOf0(attrs)]);
196
+
197
+ return signScheme.signOverTbs(scheme, signingKey, criDer, _signE).then(function (sig) {
198
+ // Proof of possession: the signature MUST verify under the subject public key (what openssl req -verify checks).
199
+ return Promise.resolve(_b.assertSignatureVerifies(criDer, sig, subjectSpki, scheme)).then(function () {
200
+ var der = b.sequence([criDer, scheme.sigAlgId, b.bitString(sig, 0)]);
201
+ return opts.pem ? csr.pemEncode(der, "CERTIFICATE REQUEST") : der;
202
+ });
203
+ }, function (e) {
204
+ if (e instanceof CsrError) throw e;
205
+ throw _err("csr/bad-input", "signing the certification request failed -- the signing key does not match the subject public key or is invalid", e);
206
+ });
207
+ }
208
+
209
+ module.exports = { sign: sign };
@@ -0,0 +1,270 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ //
5
+ // @internal -- the ENCODE-direction sibling of schema-pkix.js (which is decode-only). `makeBuilder(ctx)`
6
+ // binds a domain namespace once and returns the shared PKIX producing primitives every signer module
7
+ // composes: the distinguished-name encoder, the GeneralName + RFC 5280 sec. 4.2.1 extension-value
8
+ // encoders, the embedded-input validators (a raw Name / a SubjectPublicKeyInfo / a pre-encoded Extension
9
+ // run through the SAME parser the decoder uses), the sign-scheme bridge, and the post-sign signature
10
+ // self-check (the key-match / proof-of-possession verify). Each is parameterized on the CALLER's error
11
+ // class + code prefix, so pki.x509.sign keeps x509/* codes and pki.csr.sign keeps csr/*.
12
+
13
+ var asn1 = require("./asn1-der");
14
+ var schema = require("./schema-engine");
15
+ var compositeSig = require("./composite-sig");
16
+ var nodeCrypto = require("crypto");
17
+
18
+ var b = asn1.build;
19
+
20
+ // KeyUsage named-bit positions (RFC 5280 sec. 4.2.1.3); contentCommitment is the RFC 5280 rename of the
21
+ // X.509 nonRepudiation bit (1).
22
+ var KU_BIT = {
23
+ digitalSignature: 0, nonRepudiation: 1, contentCommitment: 1, keyEncipherment: 2,
24
+ dataEncipherment: 3, keyAgreement: 4, keyCertSign: 5, cRLSign: 6, encipherOnly: 7, decipherOnly: 8,
25
+ };
26
+
27
+ // makeBuilder(ctx) -> the bound producing primitives. ctx = { ErrorClass, prefix, O, NS, NAME_SCHEMA,
28
+ // SPKI_SCHEMA, EXT_DECODERS }. `E(kind, msg, cause)` builds a `<prefix>/<kind>` typed error.
29
+ function makeBuilder(ctx) {
30
+ var ErrorClass = ctx.ErrorClass, O = ctx.O, NS = ctx.NS;
31
+ var NAME_SCHEMA = ctx.NAME_SCHEMA, SPKI_SCHEMA = ctx.SPKI_SCHEMA;
32
+ function E(kind, message, cause) { return new ErrorClass(ctx.prefix + "/" + kind, message, cause); }
33
+ function code(kind) { return ctx.prefix + "/" + kind; }
34
+
35
+ // ---- distinguished name encoding (RFC 5280 sec. 4.1.2.4) ----
36
+ // countryName is a PrintableString SIZE(2), emailAddress an IA5String; every other new-name attribute
37
+ // is a UTF8String (Teletex/BMP/Universal are backward-compat only and never emitted).
38
+ function atvString(attrName, value) {
39
+ if (attrName === "countryName") {
40
+ if (String(value).length !== 2) throw E("bad-name", "countryName must be a two-letter ISO 3166 code (PrintableString SIZE(2))");
41
+ return b.printable(value);
42
+ }
43
+ if (attrName === "emailAddress") return b.ia5(value);
44
+ return b.utf8(value);
45
+ }
46
+ function encodeAtv(attrName, value) {
47
+ if (value == null || value === "") throw E("bad-name", "the " + attrName + " attribute value must be a non-empty string");
48
+ // oid.byName returns undefined (does not throw) for an unrecognized name -- reject it explicitly.
49
+ var typeOid = O(attrName);
50
+ if (typeOid == null) throw E("bad-name", "unknown distinguished-name attribute " + JSON.stringify(attrName));
51
+ var valueTlv;
52
+ try { valueTlv = atvString(attrName, value); }
53
+ catch (e) { if (e instanceof ErrorClass) throw e; throw E("bad-name", "the " + attrName + " value has characters invalid for its string type", e); }
54
+ return b.sequence([b.oid(typeOid), valueTlv]);
55
+ }
56
+ function encodeRdn(rdnSpec) {
57
+ if (!rdnSpec || typeof rdnSpec !== "object" || Buffer.isBuffer(rdnSpec)) throw E("bad-name", "each RDN must be an object of { attributeName: value }");
58
+ var keys = Object.keys(rdnSpec);
59
+ if (!keys.length) throw E("bad-name", "an RDN must carry at least one attribute");
60
+ return b.set(keys.map(function (k) { return encodeAtv(k, rdnSpec[k]); }));
61
+ }
62
+ // A DN spec -> RDNSequence DER. A string is shorthand for a single commonName RDN; a Buffer is raw
63
+ // pre-encoded Name DER (validated through the parser). An empty array yields an empty RDNSequence.
64
+ function encodeName(spec) {
65
+ if (Buffer.isBuffer(spec)) { assertValidNameDer(spec); return spec; }
66
+ if (typeof spec === "string") spec = [{ commonName: spec }];
67
+ if (!Array.isArray(spec)) throw E("bad-name", "a name must be a string, an array of RDNs, or raw Name DER");
68
+ return b.sequence(spec.map(encodeRdn));
69
+ }
70
+ // Full validation of a raw Name DER: walk it through the exact RDNSequence parser the decoder uses.
71
+ function assertValidNameDer(der) {
72
+ var node;
73
+ try { node = asn1.decode(der); }
74
+ catch (e) { throw E("bad-name", "the raw Name DER is not valid DER", e); }
75
+ try { schema.walk(NAME_SCHEMA, node, NS); }
76
+ catch (e) {
77
+ if (e instanceof ErrorClass || (e && e.name === "Asn1Error")) throw e;
78
+ throw E("bad-name", "the raw Name DER is not a well-formed distinguished name", e);
79
+ }
80
+ }
81
+ function isEmptyName(nameDer) { return asn1.decode(nameDer).children.length === 0; }
82
+
83
+ // ---- GeneralName encoding (RFC 5280 sec. 4.2.1.6) ----
84
+ function ia5Content(s) {
85
+ s = String(s);
86
+ for (var i = 0; i < s.length; i++) {
87
+ if (s.charCodeAt(i) > 0x7F) throw E("bad-input", "value requires 7-bit ASCII (IA5String): " + JSON.stringify(s));
88
+ }
89
+ return Buffer.from(s, "latin1");
90
+ }
91
+ function encodeGeneralName(entry) {
92
+ if (!entry || typeof entry !== "object" || Buffer.isBuffer(entry)) throw E("bad-input", "a GeneralName must be an object with exactly one name form");
93
+ var keys = Object.keys(entry);
94
+ if (keys.length !== 1) throw E("bad-input", "a GeneralName entry must have exactly one form, got " + keys.length);
95
+ var k = keys[0], v = entry[k];
96
+ if (v == null || v === "") throw E("bad-input", "an empty GeneralName value is not permitted (RFC 5280 sec. 4.2.1.6)");
97
+ switch (k) {
98
+ case "rfc822Name": return b.contextPrimitive(1, ia5Content(v));
99
+ case "dNSName": return b.contextPrimitive(2, ia5Content(v));
100
+ case "uniformResourceIdentifier": case "uri": return b.contextPrimitive(6, ia5Content(v));
101
+ case "iPAddress":
102
+ if (!Buffer.isBuffer(v) || (v.length !== 4 && v.length !== 16)) throw E("bad-input", "iPAddress must be a 4- or 16-octet Buffer");
103
+ return b.contextPrimitive(7, v);
104
+ case "directoryName": return b.explicit(4, encodeName(v)); // Name is a CHOICE -> the context tag is EXPLICIT
105
+ default: throw E("bad-input", "unsupported GeneralName form " + JSON.stringify(k) + " (supported: rfc822Name, dNSName, uniformResourceIdentifier, iPAddress, directoryName)");
106
+ }
107
+ }
108
+
109
+ // ---- extension-value encoders (the inverse of certExtensionDecoders) ----
110
+ function extKeyUsage(names) {
111
+ if (!Array.isArray(names) || !names.length) throw E("bad-input", "keyUsage must assert at least one bit (RFC 5280 sec. 4.2.1.3)");
112
+ var positions = names.map(function (n) {
113
+ var pos = KU_BIT[n];
114
+ if (pos == null) throw E("bad-input", "unknown keyUsage bit " + JSON.stringify(n));
115
+ return pos;
116
+ });
117
+ return b.namedBitString(positions);
118
+ }
119
+ function extExtKeyUsage(names) {
120
+ if (!Array.isArray(names) || !names.length) throw E("bad-input", "extendedKeyUsage must list at least one KeyPurposeId");
121
+ return b.sequence(names.map(function (n) {
122
+ var purposeOid = O(n);
123
+ if (purposeOid == null) throw E("bad-input", "unknown extendedKeyUsage purpose " + JSON.stringify(n));
124
+ return b.oid(purposeOid);
125
+ }));
126
+ }
127
+ function validateBcSpec(bc) {
128
+ if (bc.cA != null && typeof bc.cA !== "boolean") throw E("bad-input", "basicConstraints cA must be a boolean");
129
+ if (bc.critical != null && typeof bc.critical !== "boolean") throw E("bad-input", "basicConstraints critical must be a boolean");
130
+ if (bc.pathLen != null) pathLen(bc.pathLen);
131
+ Object.keys(bc).forEach(function (k) {
132
+ if (k !== "cA" && k !== "pathLen" && k !== "critical") throw E("bad-input", "unknown basicConstraints field " + JSON.stringify(k));
133
+ });
134
+ }
135
+ function extBasicConstraints(spec) {
136
+ var children = [];
137
+ if (spec.cA === true) children.push(b.boolean(true)); // cA=FALSE omitted (DER DEFAULT)
138
+ if (spec.pathLen != null) children.push(b.integer(pathLen(spec.pathLen)));
139
+ return b.sequence(children);
140
+ }
141
+ function pathLen(v) {
142
+ if (typeof v !== "number" || !isFinite(v) || v < 0 || (v | 0) !== v) throw E("bad-input", "basicConstraints pathLenConstraint must be a non-negative integer");
143
+ return BigInt(v);
144
+ }
145
+ function extSki(keyid) { return b.octetString(keyid); }
146
+ function extAki(keyid) { return b.sequence([b.contextPrimitive(0, keyid)]); } // keyIdentifier [0] IMPLICIT OCTET STRING
147
+ function extSan(entries) {
148
+ if (!Array.isArray(entries) || !entries.length) throw E("bad-input", "subjectAltName must carry at least one GeneralName");
149
+ return b.sequence(entries.map(encodeGeneralName));
150
+ }
151
+ function extCertPolicies(names) {
152
+ if (!Array.isArray(names) || !names.length) throw E("bad-input", "certificatePolicies must list at least one policy OID");
153
+ var seen = {};
154
+ return b.sequence(names.map(function (n) {
155
+ var pOid = O(n);
156
+ if (pOid == null) throw E("bad-input", "unknown certificate policy " + JSON.stringify(n));
157
+ if (seen[pOid]) throw E("bad-input", "duplicate certificate policy " + JSON.stringify(n) + " (RFC 5280 sec. 4.2.1.4)");
158
+ seen[pOid] = true;
159
+ return b.sequence([b.oid(pOid)]);
160
+ }));
161
+ }
162
+ // Wrap a value in Extension ::= SEQUENCE { extnID, critical?, extnValue }; a FALSE critical is omitted.
163
+ function ext(oidStr, critical, valueDer) {
164
+ var children = [b.oid(oidStr)];
165
+ if (critical) children.push(b.boolean(true));
166
+ children.push(b.octetString(valueDer));
167
+ return b.sequence(children);
168
+ }
169
+
170
+ // The SHA-1 subjectKeyIdentifier (RFC 5280 sec. 4.2.1.2 method 1): SHA-1 of the subjectPublicKey BIT
171
+ // STRING CONTENT (past the unused-bits octet), NOT the whole SPKI or the BIT STRING TLV.
172
+ function spkiKeyId(spkiDer) {
173
+ var keyBytes = asn1.read.bitString(asn1.decode(spkiDer).children[1]).bytes;
174
+ // RFC 5280 sec. 4.2.1.2 method 1 DEFINES the subjectKeyIdentifier as the SHA-1 of the
175
+ // subjectPublicKey; this is a key identifier, not a signature or a collision-resistance use,
176
+ // and the algorithm is fixed by the standard.
177
+ // nosemgrep: pki-weak-hash-md5-sha1
178
+ return nodeCrypto.createHash("sha1").update(keyBytes).digest();
179
+ }
180
+ function skiKeyId(val, spkiDer) {
181
+ if (Buffer.isBuffer(val)) return val;
182
+ if (val === true) return spkiKeyId(spkiDer);
183
+ throw E("bad-input", "subjectKeyIdentifier must be true (auto-derive) or a Buffer key id");
184
+ }
185
+
186
+ // ---- embedded-input validators ----
187
+ function reqDer(v, what) {
188
+ if (Buffer.isBuffer(v)) return v;
189
+ if (v instanceof Uint8Array) return Buffer.from(v);
190
+ throw E("bad-input", what + " must be a DER Buffer");
191
+ }
192
+ // Full validation of an embedded SubjectPublicKeyInfo via the SAME parser the decoder uses.
193
+ function assertValidSpki(spkiDer, what) {
194
+ var node;
195
+ try { node = asn1.decode(spkiDer); }
196
+ catch (e) { throw E("bad-input", what + " is not valid DER", e); }
197
+ try { schema.walk(SPKI_SCHEMA, node, NS); }
198
+ catch (e) {
199
+ if (e instanceof ErrorClass || (e && e.name === "Asn1Error")) throw e;
200
+ throw E("bad-input", what + " is not a well-formed SubjectPublicKeyInfo", e);
201
+ }
202
+ }
203
+ // A pre-encoded Extension ::= SEQUENCE { extnID OID, critical BOOLEAN OPTIONAL, extnValue OCTET STRING };
204
+ // an explicit critical=FALSE is non-canonical (DER DEFAULT) and rejected.
205
+ function assertValidExtension(der, idx) {
206
+ var n;
207
+ try { n = asn1.decode(der); }
208
+ catch (e) { throw E("bad-input", "pre-encoded extension [" + idx + "] is not valid DER", e); }
209
+ if (n.tagNumber !== asn1.TAGS.SEQUENCE || n.tagClass !== "universal" || !n.children || n.children.length < 2 || n.children.length > 3) throw E("bad-input", "pre-encoded extension [" + idx + "] must be an Extension SEQUENCE { extnID, critical?, extnValue }");
210
+ try { asn1.read.oid(n.children[0]); }
211
+ catch (e) { throw E("bad-input", "pre-encoded extension [" + idx + "] extnID is not an OBJECT IDENTIFIER", e); }
212
+ if (n.children.length === 3) {
213
+ var crit;
214
+ try { crit = asn1.read.boolean(n.children[1]); }
215
+ catch (e) { throw E("bad-input", "pre-encoded extension [" + idx + "] critical must be a BOOLEAN", e); }
216
+ if (crit !== true) throw E("bad-input", "pre-encoded extension [" + idx + "] critical=FALSE must be omitted (DER DEFAULT)");
217
+ }
218
+ var last = n.children[n.children.length - 1];
219
+ if (last.tagNumber !== asn1.TAGS.OCTET_STRING || last.tagClass !== "universal") throw E("bad-input", "pre-encoded extension [" + idx + "] extnValue must be an OCTET STRING");
220
+ }
221
+ // Synthesize the parsed-cert shape resolveSignScheme reads (subjectPublicKeyInfo.algorithm) from a raw SPKI.
222
+ function certLikeFromSpki(spkiDer) {
223
+ var spki = asn1.decode(spkiDer);
224
+ if (!spki.children || !spki.children.length) throw E("bad-input", "the signing key SPKI is not a SubjectPublicKeyInfo");
225
+ var alg = spki.children[0];
226
+ var keyOid;
227
+ try { keyOid = asn1.read.oid(alg.children[0]); }
228
+ catch (e) { throw E("bad-input", "the signing key SPKI algorithm is not an OID", e); }
229
+ return { subjectPublicKeyInfo: { algorithm: { oid: keyOid, parameters: alg.children.length > 1 ? alg.children[1].bytes : undefined } } };
230
+ }
231
+
232
+ // Confirm the produced signature verifies under `spki` -- key-type-agnostic (a PKCS#8 key, a WebCrypto
233
+ // CryptoKey, or any signer), where deriving-and-comparing the public key cannot (a non-extractable
234
+ // CryptoKey has no exportable public half). This is the x509 chain self-check AND the CSR proof of
235
+ // possession. Composite verifies both components. Returns a promise for composite, sync-throws for classical.
236
+ function assertSignatureVerifies(preimage, sig, spki, scheme) {
237
+ if (scheme.composite) {
238
+ return compositeSig.compositeVerify(spki, sig, preimage, scheme.composite, ErrorClass, code("unsupported-algorithm"), code("bad-input")).then(function (r) {
239
+ if (!r.ok) throw E("bad-input", "the composite signing key does not correspond to the public key -- the signature would not verify");
240
+ });
241
+ }
242
+ var pub;
243
+ try { pub = nodeCrypto.createPublicKey({ key: spki, format: "der", type: "spki" }); }
244
+ catch (e) { throw E("bad-input", "the public key could not be imported for the signature self-check", e); }
245
+ var s = scheme.sign, ok;
246
+ try {
247
+ if (s.name === "ECDSA") ok = nodeCrypto.verify(scheme.digest, preimage, { key: pub, dsaEncoding: "der" }, sig);
248
+ else if (s.name === "RSA-PSS") ok = nodeCrypto.verify(scheme.digest, preimage, { key: pub, padding: nodeCrypto.constants.RSA_PKCS1_PSS_PADDING, saltLength: s.saltLength }, sig);
249
+ else if (s.name === "RSASSA-PKCS1-v1_5") ok = nodeCrypto.verify(scheme.digest, preimage, pub, sig);
250
+ // allow:eddsa-verify-without-loworder-gate -- a self-check that OUR just-produced signature verifies
251
+ // under the key the caller controls, not a security verify of an untrusted EdDSA signature, so the
252
+ // low-order-point gate (a forged-signature defense) does not apply.
253
+ else ok = nodeCrypto.verify(null, preimage, pub, sig); // Ed25519 / Ed448 / ML-DSA / SLH-DSA
254
+ } catch (e) { throw E("bad-input", "the signature self-check could not run against the public key", e); }
255
+ if (!ok) throw E("bad-input", "the signing key does not correspond to the public key -- the signature would not verify");
256
+ }
257
+
258
+ return {
259
+ E: E, code: code, KU_BIT: KU_BIT,
260
+ encodeName: encodeName, isEmptyName: isEmptyName, encodeGeneralName: encodeGeneralName,
261
+ extKeyUsage: extKeyUsage, extExtKeyUsage: extExtKeyUsage, validateBcSpec: validateBcSpec,
262
+ extBasicConstraints: extBasicConstraints, pathLen: pathLen, extSki: extSki, extAki: extAki,
263
+ extSan: extSan, extCertPolicies: extCertPolicies, ext: ext,
264
+ spkiKeyId: spkiKeyId, skiKeyId: skiKeyId,
265
+ reqDer: reqDer, assertValidSpki: assertValidSpki, assertValidExtension: assertValidExtension,
266
+ certLikeFromSpki: certLikeFromSpki, assertSignatureVerifies: assertSignatureVerifies,
267
+ };
268
+ }
269
+
270
+ module.exports = { makeBuilder: makeBuilder, KU_BIT: KU_BIT };
package/lib/x509-sign.js CHANGED
@@ -26,11 +26,11 @@ var asn1 = require("./asn1-der");
26
26
  var oid = require("./oid");
27
27
  var x509 = require("./schema-x509");
28
28
  var signScheme = require("./sign-scheme");
29
- var compositeSig = require("./composite-sig");
30
29
  var guard = require("./guard-all");
31
30
  var frameworkError = require("./framework-error");
32
31
  var schema = require("./schema-engine");
33
32
  var pkix = require("./schema-pkix");
33
+ var pkiBuild = require("./pki-build");
34
34
 
35
35
  var CertificateError = frameworkError.CertificateError;
36
36
  // The x509 schema namespace + Name parser -- the SAME RDNSequence parser pki.schema.x509.parse uses, so
@@ -59,189 +59,25 @@ var KNOWN_EXT_KEYS = {
59
59
  certificatePolicies: 1, certificatePoliciesCritical: 1,
60
60
  };
61
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; }
62
+ // The shared PKIX producing primitives (lib/pki-build.js), bound to the x509 namespace so they keep the
63
+ // frozen x509/* codes. Thin local aliases let the x509 call sites read unchanged. csr-sign binds the same
64
+ // builder to its own namespace (Hard rule #5 -- one encode definition, no per-format hand-roll).
65
+ var _b = pkiBuild.makeBuilder({ ErrorClass: CertificateError, prefix: "x509", O: O, NS: NS, NAME_SCHEMA: NAME_SCHEMA, SPKI_SCHEMA: SPKI_SCHEMA, EXT_DECODERS: EXT_DECODERS });
66
+ var _encodeName = _b.encodeName, _isEmptyName = _b.isEmptyName, _reqDer = _b.reqDer,
67
+ _assertValidSpki = _b.assertValidSpki, _assertValidExtension = _b.assertValidExtension,
68
+ _certLikeFromSpki = _b.certLikeFromSpki, _assertCertVerifies = _b.assertSignatureVerifies,
69
+ _ext = _b.ext, _extBasicConstraints = _b.extBasicConstraints, _validateBcSpec = _b.validateBcSpec,
70
+ _extKeyUsage = _b.extKeyUsage, _extExtKeyUsage = _b.extExtKeyUsage, _extSki = _b.extSki,
71
+ _extAki = _b.extAki, _extSan = _b.extSan, _extCertPolicies = _b.extCertPolicies,
72
+ _skiKeyId = _b.skiKeyId, _spkiKeyId = _b.spkiKeyId;
127
73
 
128
- // ---- GeneralName encoding (RFC 5280 sec. 4.2.1.6) --------------------------
74
+ // ---- issuer-side extension helpers (subjectKeyIdentifier lookup + authorityKeyIdentifier) ----
129
75
 
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
76
  function _skiValueOf(caCert) {
236
77
  var ext = (caCert.extensions || []).filter(function (e) { return e.oid === OID_SKI; })[0];
237
78
  if (ext) { try { return asn1.read.octetString(asn1.decode(ext.value)); } catch (_e) { /* fall through to re-derive from the issuer SPKI */ } }
238
79
  return null;
239
80
  }
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
81
  function _akiKeyId(val, ctx) {
246
82
  if (Buffer.isBuffer(val)) return val;
247
83
  if (val === true) {
@@ -357,57 +193,6 @@ function _timeDer(date, which) {
357
193
  var y = date.getUTCFullYear();
358
194
  return (y >= 1950 && y <= 2049) ? b.utcTime(date) : b.generalizedTime(date);
359
195
  }
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
196
  // The raw subject Name TLV of a parsed CA certificate (byte-identical, so the child issuer chains to
412
197
  // the CA subject exactly). The subject follows the optional version [0], serial, signature, issuer,
413
198
  // and validity in the tbsCertificate.
@@ -455,33 +240,6 @@ function _issuedCaInfo(extSpec) {
455
240
  }
456
241
  return { cA: false, pathLen: null };
457
242
  }
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
243
  // Does the extensions spec carry a subjectAltName that will be emitted critical? The object form forces
486
244
  // SAN critical when the subject is empty (so any subjectAltName qualifies); the pre-encoded array form
487
245
  // is decoded to look for an Extension whose extnID is subjectAltName with a TRUE critical flag.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
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:6f299c9a-151b-4a48-9580-bc19e1ab66b0",
5
+ "serialNumber": "urn:uuid:d4ed0583-0ef7-424e-90b1-6a5b7b80e23d",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-17T06:00:11.553Z",
8
+ "timestamp": "2026-07-17T07:12:45.777Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/pki@0.3.0",
22
+ "bom-ref": "@blamejs/pki@0.3.1",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.3.0",
25
+ "version": "0.3.1",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
29
- "purl": "pkg:npm/%40blamejs/pki@0.3.0",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.3.1",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/pki@0.3.0",
57
+ "ref": "@blamejs/pki@0.3.1",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]