@blamejs/pki 0.3.2 → 0.3.4

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,6 +4,24 @@ 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.4 — 2026-07-17
8
+
9
+ pki.schema.c509.encode produces C509 CBOR certificates -- a DER X.509 certificate compresses to a compact, byte-exact-invertible type-3 C509, and a deterministic-CBOR encoder joins pki.cbor.
10
+
11
+ ### Added
12
+
13
+ - pki.schema.c509.encode(input) encodes a C509 certificate to deterministic-CBOR bytes -- a DER X.509 v3 certificate to a compact type-3 C509 (byte-exact-invertible, so the original signature verifies), or a pki.schema.c509.parse result re-emitted to its native array. Canonical deterministic CBOR with the registry integer shorthands and the C509 compressions; a certificate outside the invertible covered set throws a typed C509Error. Certificate parsing remains pki.schema.c509.parse.
14
+ - pki.cbor.build is a deterministic-CBOR encoder (RFC 8949 section 4.2) -- the byte-exact inverse of pki.cbor.decode: shortest-form heads, definite lengths, sorted and unique map keys, over unsigned and negative integers, byte and text strings, arrays, maps, tags, and the tagged bignum / epoch-time / object-identifier leaves. Encoded output always re-decodes through the strict decoder.
15
+
16
+ ## v0.3.3 — 2026-07-17
17
+
18
+ pki.crmf.build issues RFC 4211 certificate request messages -- a CertReqMessages with a signature proof of possession, over every algorithm the toolkit supports.
19
+
20
+ ### Added
21
+
22
+ - pki.crmf.build(spec, key, opts) builds and DER-encodes an RFC 4211 CertReqMessages -- a CertTemplate of the requested certificate fields plus a POPOSigningKey proof of possession signed with the requester's key (or a raVerified proof, opted into without a key) -- and returns DER, or a PEM block with opts.pem. The signing algorithm is resolved from the requested public key: RSA PKCS#1 v1.5 / PSS, ECDSA, EdDSA, ML-DSA, SLH-DSA, or a composite arm. Requested v3 extensions, registration controls, and regInfo ride in the message; pass an array of specs for a batch. Certificate-request-message parsing remains pki.schema.crmf.parse.
23
+ - pki.asn1.build.implicit(tag, tlv) retags an encoded universal TLV as a context-class IMPLICIT [tag], preserving the source's primitive/constructed bit and content -- the single home for the IMPLICIT tag replacement the CertTemplate and other context-tagged structures compose.
24
+
7
25
  ## v0.3.2 — 2026-07-17
8
26
 
9
27
  pki.attrcert.sign issues RFC 5755 attribute certificates -- an Attribute Authority binds a holder to privilege attributes and signs with any algorithm the toolkit supports.
package/README.md CHANGED
@@ -204,7 +204,7 @@ is callable today; nothing below is a stub.
204
204
  | `pki.webcrypto` | A W3C WebCrypto (`SubtleCrypto`) engine over `node:crypto` — `sign`/`verify`/`encrypt`/`decrypt`/`deriveBits`/`digest`/`generateKey`/`importKey`/`exportKey` across RSA, ECDSA, ECDH, Ed25519/Ed448, AES, HMAC, HKDF, PBKDF2, SHA — **and** post-quantum ML-DSA-44/65/87 and SLH-DSA signatures, plus ML-KEM-512/768/1024 key generation and certificate/PKCS#8 import — the RFC 9935 seed / expandedKey / both private-key CHOICE is validated fail-closed, so an OpenSSL-legacy bare-seed or an internally inconsistent key is rejected with a typed error (KEM encapsulation lands with CMS KEM-decrypt). Zero-dependency, OpenSSL-interoperable |
205
205
  | `pki.schema` | The schema family — `parse` detects which PKI format DER / PEM encodes and routes to the right parser, `all` enumerates the registered formats, and the engine + per-format members are grouped here |
206
206
  | `pki.schema.x509` | Parse DER / PEM certificates into structured, validated fields, with named + partly-decoded extensions — including the RFC 3739 / ETSI EN 319 412-5 qualified-certificate `qcStatements` (EU-qualified declaration, reliance limit, QSCD flag, certificate type, retention, PDS URLs, country of qualification; unknown statements preserved opaque) and the Microsoft Active Directory Certificate Services enrollment extensions (certificate template, CA version, previous-CA-certificate hash, application policies), fail-closed — `parse`, `pemDecode`, `pemEncode` |
207
- | `pki.schema.c509` | Parse C509 CBOR-encoded certificates (draft-ietf-cose-cbor-encoded-cert) — the compact CBOR profile of X.509, decoded fail-closed under deterministic CBOR; an explicit `parse` call (CBOR, not DER, so not auto-routed) |
207
+ | `pki.schema.c509` | Parse **and encode** C509 CBOR-encoded certificates (draft-ietf-cose-cbor-encoded-cert) — the compact CBOR profile of X.509, decoded fail-closed under deterministic CBOR; an explicit `parse` call (CBOR, not DER, so not auto-routed). `encode(input)` is the byte-exact inverse: a DER X.509 v3 certificate forward-transforms to a compact type-3 C509 whose reconstruction reproduces the original DER byte for byte (so the original signature still verifies), or a `parse` result re-emits its native array — canonical deterministic CBOR with the registry integer shorthands and the C509 compressions; a certificate outside the invertible set throws a typed `C509Error` |
208
208
  | `pki.schema.crl` | Parse DER / PEM X.509 CRLs per RFC 5280 §5 — revoked serials with real-`Date` revocation times, named + partly-decoded extensions, fail-closed — `parse`, `pemDecode`, `pemEncode` |
209
209
  | `pki.schema.csr` | Parse DER / PEM PKCS#10 certification requests per RFC 2986 — subject DN, public key, requested attributes, signature, fail-closed — `parse`, `pemDecode`, `pemEncode` |
210
210
  | `pki.schema.pkcs8` | Parse DER / PEM PKCS#8 private keys per RFC 5208 / 5958 — algorithm, raw key bytes, attributes, optional public key, fail-closed; encrypted keys recognized (not decrypted) — `parse`, `parseEncrypted`, `pemDecode`, `pemEncode` |
@@ -225,6 +225,7 @@ is callable today; nothing below is a stub.
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
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` |
227
227
  | `pki.attrcert` | RFC 5755 attribute-certificate issuance — `sign(spec, issuer, opts)` builds and signs an `AttributeCertificate` as an Attribute Authority: a `spec` of `holder` (exactly one of an entity name, a `baseCertificateID` public-key-certificate reference, a `fromCertificate` binding derived from a certificate, or an object digest), the validity window (GeneralizedTime), an optional serial (positive, ≤ 20 octets; randomly generated when omitted), the `attributes` (the privilege syntaxes — role, clearance, group, chargingIdentity, accessIdentity, authenticationInfo — or pre-encoded Attribute DER), and optional `extensions` (auditIdentity, targetInformation, noRevAvail, aaControls, acProxying, authorityKeyIdentifier, or pre-encoded Extension DER, each with its RFC 5755 criticality). An attribute certificate is never self-signed — the `issuer` is the signing AA, supplied as `{ cert, key }` or `{ name, publicKey, key }`. The signature algorithm is resolved from the AA 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, and the signature is verified under the AA public key before the certificate is returned. Returns DER, or a PEM `ATTRIBUTE CERTIFICATE` with `opts.pem`; malformed input throws a typed `AttrCertError`. Parsing stays at `pki.schema.attrcert.parse` — `sign` |
228
+ | `pki.crmf` | RFC 4211 certificate-request-message issuance — `build(spec, key, opts)` assembles a `CertReqMessages`: a `spec` of `certReqId` (default 0; the RFC 9483 `-1` sentinel allowed), a `certTemplate` of the requested certificate fields (`subject`, `publicKey` — the SPKI DER of the key being certified — `validity`, requested `extensions`, an optional `version` 2), optional `controls` and `regInfo` (regToken / authenticator / utf8Pairs / oldCertID / protocolEncrKey, or pre-encoded `AttributeTypeAndValue` DER), and an optional `pop` selector. `key` (or `{ key }`) is the requester's private key — the message carries a `POPOSigningKey` proof of possession signed with the private half of `certTemplate.publicKey` (verified before the message is returned), exactly as a PKCS#10 CSR proves possession; a complete template signs the `CertRequest`, an incomplete one signs a `POPOSigningKeyInput`. The signature algorithm is resolved from the requested public 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. `key` is optional for a `raVerified` proof. Pass an array of specs for a batch; the CA-assigned template fields are never emitted. Returns DER, or a PEM block with `opts.pem`; malformed input throws a typed `CrmfError`. Parsing stays at `pki.schema.crmf.parse` — `build` |
228
229
  | `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` |
229
230
  | `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` |
230
231
  | `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
@@ -43,6 +43,7 @@ var ocsp = require("./lib/ocsp");
43
43
  var x509 = require("./lib/x509-sign");
44
44
  var csr = require("./lib/csr-sign");
45
45
  var attrcert = require("./lib/attrcert-sign");
46
+ var crmf = require("./lib/crmf-sign");
46
47
  var merkle = require("./lib/merkle");
47
48
  var shbs = require("./lib/shbs");
48
49
  var hpke = require("./lib/hpke");
@@ -98,6 +99,10 @@ module.exports = {
98
99
  // builds and signs an AttributeCertificate binding a Holder to privilege attributes as an
99
100
  // Attribute Authority (never self-signed). Parsing lives at pki.schema.attrcert.parse.
100
101
  attrcert: attrcert,
102
+ // `crmf` is the RFC 4211 certificate-request-message producing side -- pki.crmf.build
103
+ // assembles a CertReqMessages (a CertTemplate + a signature proof of possession) for a CMP
104
+ // or EST enrollment. Parsing lives at pki.schema.crmf.parse.
105
+ crmf: crmf,
101
106
  // `merkle` is the RFC 6962 / RFC 9162 Merkle-tree proof-verification core --
102
107
  // pki.merkle.leafHash / nodeHash / emptyRootHash build the domain-separated
103
108
  // (0x00 leaf / 0x01 node) SHA-256 tree hashes; pki.merkle.verifyInclusion and
package/lib/asn1-der.js CHANGED
@@ -989,6 +989,15 @@ var build = {
989
989
  explicit: function (tagNumber, inner) { return encodeTLV(CLASS_CONTEXT, true, tagNumber, _asBuffer(inner, "build.explicit")); },
990
990
  contextPrimitive: function (tagNumber, content) { return encodeTLV(CLASS_CONTEXT, false, tagNumber, _asBuffer(content, "build.contextPrimitive")); },
991
991
  contextConstructed: function (tagNumber, content) { return encodeTLV(CLASS_CONTEXT, true, tagNumber, _asBuffer(content, "build.contextConstructed")); },
992
+ // IMPLICIT [tag] retag of an already-encoded UNIVERSAL TLV: replace the source's tag with a context-class
993
+ // [tag] identifier, PRESERVING the source's primitive/constructed bit and content octets (X.680 sec. 30.6).
994
+ // A single home for the tag replacement so no format module re-inlines the P/C-bit handling (a decode of
995
+ // the source validates it is exactly one well-formed TLV; its content is the trailing `length` octets).
996
+ implicit: function (tagNumber, tlv) {
997
+ var buf = _asBuffer(tlv, "build.implicit");
998
+ var node = decode(buf);
999
+ return encodeTLV(CLASS_CONTEXT, node.constructed, tagNumber, buf.slice(buf.length - node.length));
1000
+ },
992
1001
  raw: function (buf) { return _asBuffer(buf, "build.raw"); },
993
1002
  };
994
1003
 
package/lib/cbor-det.js CHANGED
@@ -624,8 +624,111 @@ function readOid(node) {
624
624
  return asn1.decodeOidContent(inner.content);
625
625
  }
626
626
 
627
+ // ---- deterministic-CBOR encoder (RFC 8949 sec. 4.2) -- the byte-exact inverse of decode ----
628
+ // Every emitted head is the shortest form (the exact mirror of decode's cbor/non-minimal-argument reject),
629
+ // every length is definite, and map keys are sorted bytewise + unique -- so decode(build.x(...)) round-trips
630
+ // or the builder produced a shape its own strict decoder rejects. Nested items are pre-encoded CBOR item
631
+ // Buffers (the asn1.build pattern); the CBOR-encoded PKI surfaces (C509, COSE) compose these leaves.
632
+ function _cbItem(x, who) {
633
+ if (!Buffer.isBuffer(x)) throw new CborError("cbor/not-buffer", who + ": expected a pre-encoded CBOR item Buffer");
634
+ return x;
635
+ }
636
+ // Validate an assembled container decodes: well-formed nested items (no malformed head, indefinite length,
637
+ // stray break, or trailing bytes), AND within the decoder's depth cap -- a container adds ONE nesting
638
+ // level, so a child AT the cap would overflow the output. Decoding the whole output in one pass catches
639
+ // both, so the encoder never produces bytes its own strict decoder rejects.
640
+ function _assertDecodes(out, who) {
641
+ try { decode(out); } catch (e) { throw new CborError("cbor/bad-item", who + ": produced bytes the strict decoder rejects (" + (e && e.code) + ")", e); }
642
+ return out;
643
+ }
644
+ // Mirror the decoder's total-size cap (constants.LIMITS.CBOR_MAX_BYTES) so a string leaf never emits an
645
+ // item the strict reader would reject as too large -- the encode/decode round trip stays closed under the
646
+ // default limits.
647
+ function _capString(out, who) {
648
+ if (out.length > constants.LIMITS.CBOR_MAX_BYTES) throw new CborError("cbor/too-large", who + ": encoded " + out.length + " bytes exceeds cap " + constants.LIMITS.CBOR_MAX_BYTES);
649
+ return out;
650
+ }
651
+ // Reject a JS string carrying an unpaired UTF-16 surrogate: Buffer.from(..., "utf8") would silently
652
+ // substitute U+FFFD, so the emitted text string would not be the input (and read.text enforces well-formed
653
+ // UTF-8). Fail closed to match the decoder rather than corrupt the text.
654
+ function _assertWellFormedText(str, who) {
655
+ for (var i = 0; i < str.length; i++) {
656
+ var c = str.charCodeAt(i);
657
+ if (c >= 0xd800 && c <= 0xdbff) { var n = str.charCodeAt(i + 1); if (!(n >= 0xdc00 && n <= 0xdfff)) throw new CborError("cbor/bad-utf8", who + ": the string has an unpaired high surrogate"); i++; }
658
+ else if (c >= 0xdc00 && c <= 0xdfff) throw new CborError("cbor/bad-utf8", who + ": the string has an unpaired low surrogate");
659
+ }
660
+ }
661
+ // The initial byte + minimal-width argument for major type `mt` and unsigned `argument`.
662
+ function _head(mt, argument, who) {
663
+ var a = typeof argument === "bigint" ? argument : BigInt(argument);
664
+ if (a < 0n) throw new CborError("cbor/bad-argument", who + ": a CBOR head argument must be non-negative");
665
+ if (a > 0xffffffffffffffffn) throw new CborError("cbor/bad-argument", who + ": a CBOR head argument exceeds 64 bits");
666
+ var mtb = mt << 5, b;
667
+ if (a <= 23n) return Buffer.from([mtb | Number(a)]);
668
+ if (a < 256n) return Buffer.from([mtb | 24, Number(a)]);
669
+ if (a < 65536n) { b = Buffer.alloc(3); b[0] = mtb | 25; b.writeUInt16BE(Number(a), 1); return b; }
670
+ if (a < 4294967296n) { b = Buffer.alloc(5); b[0] = mtb | 26; b.writeUInt32BE(Number(a), 1); return b; }
671
+ b = Buffer.alloc(9); b[0] = mtb | 27; b.writeBigUInt64BE(a, 1); return b;
672
+ }
673
+ var build = {
674
+ uint: function (n) { var a = typeof n === "bigint" ? n : BigInt(n); if (a < 0n) throw new CborError("cbor/bad-argument", "build.uint: value must be non-negative"); return _head(0, a, "build.uint"); },
675
+ nint: function (n) { var a = typeof n === "bigint" ? n : BigInt(n); if (a >= 0n) throw new CborError("cbor/bad-argument", "build.nint: value must be negative"); return _head(1, -1n - a, "build.nint"); },
676
+ int: function (n) { var a = typeof n === "bigint" ? n : BigInt(n); return a >= 0n ? _head(0, a, "build.int") : _head(1, -1n - a, "build.int"); },
677
+ byteString: function (buf) { var b = _asBuffer(buf, "build.byteString"); return _capString(Buffer.concat([_head(2, b.length, "build.byteString"), b]), "build.byteString"); },
678
+ textString: function (s) { var str = String(s); _assertWellFormedText(str, "build.textString"); var b = Buffer.from(str, "utf8"); return _capString(Buffer.concat([_head(3, b.length, "build.textString"), b]), "build.textString"); },
679
+ array: function (items) {
680
+ if (!Array.isArray(items)) throw new CborError("cbor/bad-argument", "build.array: expected an array of pre-encoded items");
681
+ var parts = items.map(function (x, i) { return _cbItem(x, "build.array[" + i + "]"); });
682
+ return _assertDecodes(Buffer.concat([_head(4, items.length, "build.array")].concat(parts)), "build.array");
683
+ },
684
+ map: function (pairs) {
685
+ if (!Array.isArray(pairs)) throw new CborError("cbor/bad-argument", "build.map: expected an array of [key, value] pairs");
686
+ var enc = pairs.map(function (p, i) { if (!Array.isArray(p) || p.length !== 2) throw new CborError("cbor/bad-argument", "build.map[" + i + "]: each entry is a [key, value] pair"); return [_cbItem(p[0], "build.map key"), _cbItem(p[1], "build.map value")]; });
687
+ // RFC 8949 sec. 4.2.1 Core Deterministic: keys sorted in the BYTEWISE lexicographic order of their
688
+ // encodings -- NOT the length-first order of sec. 4.2.3 (the older RFC 7049 "Canonical CBOR" variant).
689
+ // This MUST match the decoder, which enforces bytewise order (_profile "deterministic" -> Buffer.compare
690
+ // at :70); a length-first sort here would emit a map the strict decoder then rejects as unsorted.
691
+ enc.sort(function (a, b) { return Buffer.compare(a[0], b[0]); });
692
+ for (var i = 1; i < enc.length; i++) { if (Buffer.compare(enc[i - 1][0], enc[i][0]) === 0) throw new CborError("cbor/duplicate-map-key", "build.map: duplicate map key"); }
693
+ var flat = [_head(5, pairs.length, "build.map")];
694
+ enc.forEach(function (p) { flat.push(p[0], p[1]); });
695
+ return _assertDecodes(Buffer.concat(flat), "build.map");
696
+ },
697
+ tag: function (tagNum, inner) { return _assertDecodes(Buffer.concat([_head(6, tagNum, "build.tag"), _cbItem(inner, "build.tag content")]), "build.tag"); },
698
+ boolean: function (v) { return Buffer.from([v ? 0xf5 : 0xf4]); },
699
+ nullValue: function () { return Buffer.from([0xf6]); },
700
+ raw: function (buf) { return _assertDecodes(_asBuffer(buf, "build.raw"), "build.raw"); },
701
+ // Tagged leaves -- the inverse of read.biguint / read.time / read.oid.
702
+ biguint: function (n) {
703
+ var a = typeof n === "bigint" ? n : BigInt(n);
704
+ if (a < 0n) throw new CborError("cbor/bad-argument", "build.biguint: value must be non-negative");
705
+ var hex = a.toString(16); if (hex.length % 2) hex = "0" + hex;
706
+ var mag = a === 0n ? Buffer.alloc(0) : Buffer.from(hex, "hex");
707
+ if (mag.length <= 8) throw new CborError("cbor/bad-argument", "build.biguint: a value that fits 8 bytes must use build.uint (RFC 8949 preferred serialization)");
708
+ // Mirror read.biguint's cap so the tagged bignum builder never emits a value the strict reader rejects.
709
+ if (mag.length > constants.LIMITS.CBOR_MAX_BIGUINT_BYTES) throw new CborError("cbor/biguint-too-large", "build.biguint: " + mag.length + " bytes exceeds cap " + constants.LIMITS.CBOR_MAX_BIGUINT_BYTES);
710
+ return build.tag(2, build.byteString(mag));
711
+ },
712
+ time: function (v) {
713
+ var ms;
714
+ if (v instanceof Date) {
715
+ ms = v.getTime();
716
+ if (Number.isNaN(ms)) throw new CborError("cbor/bad-time", "build.time: an Invalid Date has no epoch value");
717
+ // read.time reads only an integer (second-granularity) tag-1, so a sub-second Date cannot round-trip:
718
+ // reject it rather than silently truncate the milliseconds.
719
+ if (ms % 1000 !== 0) throw new CborError("cbor/bad-time", "build.time: a Date with sub-second precision cannot be a second-granularity CBOR epoch; round to whole seconds");
720
+ }
721
+ var secs = v instanceof Date ? BigInt(Math.floor(ms / 1000)) : (typeof v === "bigint" ? v : BigInt(v));
722
+ // Mirror read.time's bound so build.time never emits a tag-1 the strict reader would reject as out of range.
723
+ if (secs < -_MAX_EPOCH_SECONDS || secs > _MAX_EPOCH_SECONDS) throw new CborError("cbor/bad-time", "build.time: epoch seconds " + secs + " are outside the representable Date range");
724
+ return build.tag(1, build.int(secs));
725
+ },
726
+ oid: function (dotted) { return build.tag(111, build.byteString(asn1.encodeOidContent(dotted))); },
727
+ };
728
+
627
729
  module.exports = {
628
730
  decode: decode,
731
+ build: build,
629
732
  read: {
630
733
  uint: readUint,
631
734
  nint: readNint,
@@ -0,0 +1,275 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+
5
+ /**
6
+ * @module pki.crmf
7
+ * @nav Signing
8
+ * @title Certificate request messages
9
+ * @intro The RFC 4211 certificate-request-message producing side. `pki.crmf.build` assembles a
10
+ * `CertReqMessages` -- one or more `CertReqMsg`, each a `CertRequest` (a `CertTemplate` of the requested
11
+ * certificate fields plus optional controls) paired with a proof of possession. The common proof is a
12
+ * `POPOSigningKey` signature over the `CertRequest`, made with the private half of the key being
13
+ * certified (the requester proves possession, exactly as a PKCS#10 CSR does). The message drops into a
14
+ * CMP (RFC 9810) or EST enrollment body. Parsing lives at `pki.schema.crmf.parse`.
15
+ * @spec RFC 4211
16
+ * @card Build a CRMF CertReqMessages with a signature proof of possession.
17
+ */
18
+ //
19
+ // RFC 4211 App. B / RFC 5912 sec. 10 are DEFINITIONS IMPLICIT TAGS: a CertTemplate [0]..[9] field tag
20
+ // REPLACES the base universal tag (built via asn1.build.implicit, preserving the primitive/constructed
21
+ // bit); the shipped parser encodes issuer [3] / subject [5] Name IMPLICITLY (the dominant CMP/EST wire
22
+ // form), so the builder does too. The two EXPLICIT exceptions are the OptionalValidity notBefore [0] /
23
+ // notAfter [1] Time (a genuine UTCTime/GeneralizedTime CHOICE). The signature scheme resolves from the
24
+ // requested publicKey through the shared sign-scheme registry; the Name / extension / SPKI encoders and
25
+ // the post-sign self-check are the shared lib/pki-build primitives, bound to the crmf namespace.
26
+
27
+ var asn1 = require("./asn1-der");
28
+ var oid = require("./oid");
29
+ var crmf = require("./schema-crmf");
30
+ var signScheme = require("./sign-scheme");
31
+ var pkix = require("./schema-pkix");
32
+ var pkiBuild = require("./pki-build");
33
+ var frameworkError = require("./framework-error");
34
+
35
+ var CrmfError = frameworkError.CrmfError;
36
+ var b = asn1.build;
37
+ function _err(code, message, cause) { return new CrmfError(code, message, cause); }
38
+ function _signE(kind, message, cause) { return new CrmfError("crmf/" + kind, message, cause); }
39
+ function O(n) { return oid.byName(n); }
40
+
41
+ var NS = pkix.makeNS("crmf", CrmfError, oid);
42
+ var EXT_DECODERS = pkix.certExtensionDecoders(NS).byOid;
43
+ var _b = pkiBuild.makeBuilder({
44
+ ErrorClass: CrmfError, prefix: "crmf", O: O, NS: NS,
45
+ NAME_SCHEMA: pkix.name(NS), SPKI_SCHEMA: pkix.spki(NS), EXT_DECODERS: EXT_DECODERS,
46
+ });
47
+
48
+ var KNOWN_SPEC_KEYS = { certReqId: 1, certTemplate: 1, controls: 1, regInfo: 1, pop: 1 };
49
+ var KNOWN_TEMPLATE_KEYS = { version: 1, subject: 1, publicKey: 1, validity: 1, extensions: 1, issuer: 1 };
50
+ // The controls (RFC 4211 sec. 6) and regInfo (sec. 7) are DISJOINT AttributeTypeAndValue namespaces --
51
+ // regToken/authenticator/oldCertID/protocolEncrKey are controls; utf8Pairs is regInfo -- so the object
52
+ // form validates each key against its own field's registry (a control name is not a valid regInfo, and
53
+ // vice versa). The key name equals the registered OID name; complex values (pkiPublicationInfo, certReq)
54
+ // ride the pre-encoded escape hatch. Each entry is the value encoder for that OID.
55
+ function _ctlUtf8(v) { return b.utf8(String(v)); }
56
+ function _ctlSpki(v) { var k = _b.reqDer(v, "protocolEncrKey (an SPKI DER)"); _b.assertValidSpki(k, "protocolEncrKey"); return b.raw(k); }
57
+ var CONTROL_VALUE = { regToken: _ctlUtf8, authenticator: _ctlUtf8, oldCertID: function (v) { return _encodeCertId(v); }, protocolEncrKey: _ctlSpki };
58
+ var REGINFO_VALUE = { utf8Pairs: _ctlUtf8 };
59
+
60
+ // ---- CertTemplate + POP structural encoders (byte-exact inverses of schema-crmf.js) ----
61
+
62
+ // OptionalValidity [4] IMPLICIT SEQUENCE { notBefore [0] EXPLICIT Time, notAfter [1] EXPLICIT Time } --
63
+ // at least one present (RFC 4211 sec. 5). Time is a CHOICE so [0]/[1] are EXPLICIT.
64
+ function _encodeOptionalValidity(validity) {
65
+ if (!validity || typeof validity !== "object" || Buffer.isBuffer(validity)) throw _err("crmf/bad-validity", "validity must be an object { notBefore?, notAfter? }");
66
+ var nb = validity.notBefore, na = validity.notAfter;
67
+ if (nb == null && na == null) throw _err("crmf/bad-validity", "validity must contain notBefore or notAfter (RFC 4211 sec. 5)");
68
+ var parts = [];
69
+ // timeDer validates each instant (guard.time.assertValid throws on an Invalid Date) BEFORE the
70
+ // inverted-window comparison, so getTime() below cannot be NaN.
71
+ if (nb != null) parts.push(b.explicit(0, _b.timeDer(nb, "validity notBefore")));
72
+ if (na != null) parts.push(b.explicit(1, _b.timeDer(na, "validity notAfter")));
73
+ // allow:nan-date-comparison-unguarded -- both instants passed timeDer's guard.time.assertValid above.
74
+ if (nb != null && na != null && nb.getTime() > na.getTime()) throw _err("crmf/bad-validity", "notBefore must not be after notAfter");
75
+ return b.implicit(4, b.sequence(parts));
76
+ }
77
+ // CertTemplate ::= SEQUENCE { [0..9] all IMPLICIT OPTIONAL }. A REQUEST omits serialNumber [1] / signingAlg
78
+ // [2] / issuerUID [7] / subjectUID [8] (CA-assigned or deprecated, RFC 4211 sec. 5) -- the builder never
79
+ // emits them. Fields are emitted in ascending tag order.
80
+ function _encodeCertTemplate(tpl) {
81
+ if (!tpl || typeof tpl !== "object" || Buffer.isBuffer(tpl)) throw _err("crmf/bad-cert-template", "certTemplate must be an object");
82
+ Object.keys(tpl).forEach(function (k) { if (!KNOWN_TEMPLATE_KEYS[k]) throw _err("crmf/bad-input", "unknown certTemplate field " + JSON.stringify(k)); });
83
+ var fields = [];
84
+ if (tpl.version != null) {
85
+ if (tpl.version !== 2) throw _err("crmf/bad-version", "certTemplate version MUST be 2 (v3) if supplied (RFC 4211 sec. 5)");
86
+ fields.push(b.implicit(0, b.integer(2n))); // version [0]
87
+ }
88
+ // issuer [3] / subject [5] are EXPLICIT: Name is a CHOICE, and X.680 sec. 31.2.7 forces a context tag on
89
+ // a CHOICE to EXPLICIT even under the module's IMPLICIT TAGS default (the [3]/[5] wraps the RDNSequence
90
+ // SEQUENCE, it does not replace its tag). The parser accepts both forms; this is the conformant one.
91
+ if (tpl.issuer != null) fields.push(b.explicit(3, _b.encodeName(tpl.issuer))); // issuer [3] EXPLICIT
92
+ if (tpl.validity != null) fields.push(_encodeOptionalValidity(tpl.validity)); // validity [4] IMPLICIT
93
+ if (tpl.subject != null) fields.push(b.explicit(5, _b.encodeName(tpl.subject))); // subject [5] EXPLICIT
94
+ var spki = null;
95
+ if (tpl.publicKey != null) {
96
+ spki = _b.reqDer(tpl.publicKey, "certTemplate.publicKey (the SPKI DER of the requested key)");
97
+ _b.assertValidSpki(spki, "certTemplate.publicKey");
98
+ fields.push(b.implicit(6, spki)); // publicKey [6]
99
+ }
100
+ if (tpl.extensions != null) fields.push(b.implicit(9, _b.requestedExtensions(tpl.extensions, spki))); // extensions [9]
101
+ return { der: b.sequence(fields), spki: spki, complete: tpl.subject != null && tpl.publicKey != null };
102
+ }
103
+
104
+ // Controls / regInfo ::= SEQUENCE SIZE(1..MAX) OF AttributeTypeAndValue { type OID, value ANY }. Object
105
+ // form maps recognized names to typed value encoders; any other (or a pre-encoded AttributeTypeAndValue
106
+ // DER array) rides the escape hatch, shape-validated.
107
+ function _buildAttrTypeAndValues(spec, code, label, valueMap) {
108
+ if (Array.isArray(spec)) {
109
+ if (!spec.length) throw _err(code, label + " must carry at least one entry");
110
+ var seenA = {};
111
+ return b.sequence(spec.map(function (e, i) {
112
+ var der = _b.reqDer(e, label + " [" + i + "]");
113
+ var n;
114
+ try { n = asn1.decode(der); } catch (err) { throw _err("crmf/bad-input", "pre-encoded " + label + " [" + i + "] is not valid DER", err); }
115
+ if (n.tagNumber !== asn1.TAGS.SEQUENCE || n.tagClass !== "universal" || !n.children || n.children.length !== 2) throw _err("crmf/bad-input", "pre-encoded " + label + " [" + i + "] must be a SEQUENCE { type OID, value }");
116
+ var t;
117
+ try { t = asn1.read.oid(n.children[0]); } catch (err) { throw _err("crmf/bad-input", "pre-encoded " + label + " [" + i + "] type is not an OBJECT IDENTIFIER", err); }
118
+ if (seenA[t]) throw _err(code, "duplicate " + label + " type " + (oid.name(t) || t));
119
+ seenA[t] = true;
120
+ return b.raw(der);
121
+ }));
122
+ }
123
+ if (!spec || typeof spec !== "object") throw _err("crmf/bad-input", label + " must be an object or an array of pre-encoded AttributeTypeAndValue DER");
124
+ var out = [], seen = {};
125
+ Object.keys(spec).forEach(function (k) {
126
+ var enc = valueMap[k];
127
+ if (!enc) throw _err("crmf/bad-input", "unknown " + label + " " + JSON.stringify(k) + "; pass a pre-encoded AttributeTypeAndValue DER via the array form for a " + label + " entry outside " + Object.keys(valueMap).join("/"));
128
+ var typeOid = O(k);
129
+ if (seen[typeOid]) throw _err(code, "duplicate " + label + " type " + k);
130
+ seen[typeOid] = true;
131
+ out.push(b.sequence([b.oid(typeOid), enc(spec[k])]));
132
+ });
133
+ if (!out.length) throw _err(code, label + " must carry at least one entry");
134
+ return b.sequence(out);
135
+ }
136
+ // certReqId is a signed INTEGER, value UNCONSTRAINED (the RFC 9483 -1 sentinel and 0 are both legal). A
137
+ // number must be a safe integer (a fractional or > 2^53 number loses precision through BigInt); a BigInt
138
+ // or a decimal / 0x-hex string carries an arbitrary value.
139
+ function _certReqId(v) {
140
+ if (v == null) return 0n;
141
+ if (typeof v === "bigint") return v;
142
+ if (typeof v === "number") { if (!Number.isSafeInteger(v)) throw _err("crmf/bad-input", "certReqId number must be a safe integer (pass a BigInt or a string for a larger value)"); return BigInt(v); }
143
+ if (typeof v === "string") { try { return BigInt(v); } catch (e) { throw _err("crmf/bad-input", "certReqId string must be a decimal or 0x-hex integer", e); } }
144
+ throw _err("crmf/bad-input", "certReqId must be a BigInt, a safe integer, or a string");
145
+ }
146
+ // CertId ::= SEQUENCE { issuer GeneralName, serialNumber INTEGER } (RFC 4211 sec. 6.4, oldCertID value).
147
+ function _encodeCertId(id) {
148
+ if (!id || typeof id !== "object" || id.issuer == null || id.serialNumber == null) throw _err("crmf/bad-input", "oldCertID must be { issuer: GeneralName, serialNumber }");
149
+ return b.sequence([_b.encodeGeneralName(id.issuer), _b.serialInteger(id.serialNumber)]);
150
+ }
151
+
152
+ // ProofOfPossession. The signature arm (the default when a key is given): when the template carries BOTH
153
+ // subject and publicKey (complete), sign the CertRequest DER and OMIT poposkInput; otherwise build a
154
+ // POPOSigningKeyInput (authInfo sender [0] GeneralName + the requested publicKey), sign its SEQUENCE, and
155
+ // carry it as poposkInput [0] (RFC 4211 sec. 4.1). raVerified is emitted only on an explicit opt-in.
156
+ function _buildProofOfPossession(pop, certReqDer, template, signingKey, opts) {
157
+ if (pop != null && (typeof pop !== "object" || Buffer.isBuffer(pop))) throw _err("crmf/bad-input", "spec.pop must be an object (e.g. { type: 'signature' } or { type: 'raVerified', raVerified: true })");
158
+ var mode = (pop && pop.type) || (signingKey != null ? "signature" : null);
159
+ if (mode == null) return null; // no POP requested and no key -> omit popo (an RA supplies it out of band)
160
+ if (mode === "raVerified") {
161
+ if (!(pop && pop.raVerified === true)) throw _err("crmf/bad-popo", "raVerified must be explicitly opted into (pop: { type: 'raVerified', raVerified: true }) -- a requester does not normally assert it (RFC 4211 sec. 4)");
162
+ return b.implicit(0, b.nullValue()); // raVerified [0] IMPLICIT NULL
163
+ }
164
+ if (mode !== "signature") throw _err("crmf/bad-popo", "unsupported proof-of-possession type " + JSON.stringify(mode) + " (supported: 'signature', 'raVerified')");
165
+ if (signingKey == null) throw _err("crmf/bad-input", "a signature proof of possession requires the requester's private key");
166
+ if (template.spki == null) throw _err("crmf/bad-input", "a signature proof of possession requires certTemplate.publicKey");
167
+ var scheme = signScheme.resolveSignScheme(_b.certLikeFromSpki(template.spki), { combinedRsaSig: true, pss: opts.pss, digestAlgorithm: opts.digestAlgorithm }, true, _signE);
168
+ var signedRegion, poposkInputField = null;
169
+ if (template.complete) {
170
+ signedRegion = certReqDer; // sec. 4.1: complete template -> sign the CertRequest, poposkInput omitted
171
+ } else {
172
+ // POPOSigningKeyInput ::= SEQUENCE { authInfo CHOICE { sender [0] GeneralName, publicKeyMAC }, publicKey }.
173
+ var sender = pop && pop.sender;
174
+ if (sender == null) throw _err("crmf/bad-popo", "an incomplete template (missing subject or publicKey) requires pop.sender (a GeneralName) for the POPOSigningKeyInput authInfo (RFC 4211 sec. 4.1)");
175
+ var poposkSeq = b.sequence([b.explicit(0, _b.encodeGeneralName(sender)), b.raw(template.spki)]);
176
+ signedRegion = poposkSeq;
177
+ poposkInputField = b.implicit(0, poposkSeq); // poposkInput [0] IMPLICIT POPOSigningKeyInput
178
+ }
179
+ return signScheme.signOverTbs(scheme, signingKey, signedRegion, _signE).then(function (sig) {
180
+ return Promise.resolve(_b.assertSignatureVerifies(signedRegion, sig, template.spki, scheme)).then(function () {
181
+ var popoChildren = [];
182
+ if (poposkInputField) popoChildren.push(poposkInputField);
183
+ popoChildren.push(scheme.sigAlgId);
184
+ popoChildren.push(b.bitString(sig, 0));
185
+ return b.implicit(1, b.sequence(popoChildren)); // signature [1] IMPLICIT POPOSigningKey
186
+ });
187
+ }, function (e) {
188
+ if (e instanceof CrmfError) throw e;
189
+ throw _err("crmf/bad-input", "signing the proof of possession failed -- the key does not match the requested public key or is invalid", e);
190
+ });
191
+ }
192
+
193
+ /**
194
+ * @primitive pki.crmf.build
195
+ * @signature pki.crmf.build(spec, key?, opts?) -> Promise<Buffer|string>
196
+ * @since 0.3.3
197
+ * @status experimental
198
+ * @spec RFC 4211
199
+ * @defends forged-certificate-request (CWE-347)
200
+ * @related pki.schema.crmf.parse, pki.csr.sign
201
+ *
202
+ * Build and DER-encode an RFC 4211 `CertReqMessages`. `spec` describes one certificate request message (or
203
+ * pass `spec.messages` -- an array of specs -- for a batch): `certReqId` (an integer, default 0; the RFC
204
+ * 9483 `-1` sentinel is allowed), `certTemplate` (the requested certificate fields -- `subject`, `publicKey`
205
+ * (the SPKI DER of the key being certified), `validity` ({ notBefore, notAfter } Dates), `extensions` (an
206
+ * object of subjectAltName / keyUsage / extendedKeyUsage / basicConstraints / certificatePolicies /
207
+ * subjectKeyIdentifier, or pre-encoded Extension DER), and an optional `version` (2)), optional `controls`
208
+ * and `regInfo` (an object of regToken / authenticator / utf8Pairs / oldCertID / protocolEncrKey, or
209
+ * pre-encoded AttributeTypeAndValue DER), and an optional `pop` selector. `key` (or `{ key }`) is the
210
+ * REQUESTER's private key -- the private half of `certTemplate.publicKey`; the message carries a
211
+ * `POPOSigningKey` proof of possession signed with it (verified before the message is returned), exactly
212
+ * as a PKCS#10 CSR proves possession. The signature algorithm is resolved from the requested public key
213
+ * (RSA PKCS#1 v1.5 / PSS, ECDSA, EdDSA, ML-DSA, SLH-DSA, or a composite arm). `key` is optional -- omit it
214
+ * for a `raVerified` proof (opt in with `pop: { type: 'raVerified', raVerified: true }`). Returns DER, or a
215
+ * PEM block with `opts.pem` (the label is required). Malformed input throws a typed `CrmfError`.
216
+ * Certificate-request-message parsing is `pki.schema.crmf.parse`.
217
+ *
218
+ * @opts
219
+ * - `pem` (string) -- return a PEM block with this label instead of DER (e.g. "CERTIFICATE REQUEST MESSAGE").
220
+ * - `pss` (boolean) -- sign an RSA key with RSASSA-PSS rather than PKCS#1 v1.5.
221
+ * - `digestAlgorithm` (string) -- override the message digest where the algorithm permits a choice.
222
+ * @example
223
+ * var msg = await pki.crmf.build(
224
+ * { certReqId: 0, certTemplate: { subject: "device-42", publicKey: signerSpki } },
225
+ * { key: signerKeyPkcs8 });
226
+ * pki.schema.crmf.parse(msg).messages[0].certReq.certTemplate.subject.dn; // "CN=device-42"
227
+ */
228
+ function build(spec, key, opts) {
229
+ return Promise.resolve().then(function () { return _build(spec, key, opts); });
230
+ }
231
+
232
+ function _buildCertReqMsg(spec, key, opts) {
233
+ if (!spec || typeof spec !== "object" || Buffer.isBuffer(spec)) throw _err("crmf/bad-input", "each certificate-request-message spec must be an object");
234
+ Object.keys(spec).forEach(function (k) { if (!KNOWN_SPEC_KEYS[k]) throw _err("crmf/bad-input", "unknown spec field " + JSON.stringify(k)); });
235
+ if (spec.certTemplate == null) throw _err("crmf/bad-input", "spec.certTemplate is required");
236
+
237
+ var signingKey = (key && typeof key === "object" && !Buffer.isBuffer(key) && !(key instanceof Uint8Array) && key.type == null && "key" in key) ? key.key : key;
238
+ var template = _encodeCertTemplate(spec.certTemplate);
239
+ var certReqChildren = [b.integer(_certReqId(spec.certReqId)), template.der];
240
+ if (spec.controls != null) certReqChildren.push(_buildAttrTypeAndValues(spec.controls, "crmf/bad-controls", "controls", CONTROL_VALUE));
241
+ var certReqDer = b.sequence(certReqChildren);
242
+
243
+ return Promise.resolve(_buildProofOfPossession(spec.pop, certReqDer, template, signingKey, opts)).then(function (popoDer) {
244
+ var msgChildren = [certReqDer];
245
+ if (popoDer) msgChildren.push(popoDer);
246
+ if (spec.regInfo != null) msgChildren.push(_buildAttrTypeAndValues(spec.regInfo, "crmf/bad-reg-info", "regInfo", REGINFO_VALUE));
247
+ return b.sequence(msgChildren); // CertReqMsg
248
+ });
249
+ }
250
+
251
+ function _build(spec, key, opts) {
252
+ opts = opts || {};
253
+ if (!spec || typeof spec !== "object" || Buffer.isBuffer(spec)) throw _err("crmf/bad-input", "the certificate-request-message spec must be an object");
254
+ var specs;
255
+ if (spec.messages != null) {
256
+ // Batch form: the envelope carries ONLY `messages` -- reject a stray field so a request spec written at
257
+ // the wrong nesting level (e.g. certTemplate alongside messages) is not silently dropped.
258
+ if (!Array.isArray(spec.messages)) throw _err("crmf/bad-input", "spec.messages must be an array of certificate-request-message specs");
259
+ Object.keys(spec).forEach(function (k) { if (k !== "messages") throw _err("crmf/bad-input", "unknown batch-envelope field " + JSON.stringify(k) + " -- a batch spec carries only 'messages'"); });
260
+ specs = spec.messages;
261
+ } else {
262
+ specs = [spec];
263
+ }
264
+ if (!specs.length) throw _err("crmf/bad-input", "at least one certificate request message is required (RFC 4211 sec. 3)");
265
+ return Promise.all(specs.map(function (s) { return _buildCertReqMsg(s, key, opts); })).then(function (msgs) {
266
+ var der = b.sequence(msgs); // CertReqMessages ::= SEQUENCE SIZE(1..MAX) OF CertReqMsg
267
+ if (opts.pem != null) {
268
+ if (typeof opts.pem !== "string" || !opts.pem) throw _err("crmf/bad-input", "opts.pem must be a non-empty PEM label string");
269
+ return crmf.pemEncode(der, opts.pem);
270
+ }
271
+ return der;
272
+ });
273
+ }
274
+
275
+ module.exports = { build: build };
package/lib/csr-sign.js CHANGED
@@ -42,12 +42,6 @@ var _b = pkiBuild.makeBuilder({
42
42
  NAME_SCHEMA: pkix.name(NS), SPKI_SCHEMA: pkix.spki(NS), EXT_DECODERS: EXT_DECODERS,
43
43
  });
44
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
45
  // challengePassword DirectoryString charset (PrintableString set); anything else uses UTF8String.
52
46
  var PRINTABLE_RE = /^[A-Za-z0-9 '()+,\-./:=?]*$/;
53
47
 
@@ -57,43 +51,9 @@ function _implicitSetOf0(members) {
57
51
  return b.contextConstructed(0, Buffer.concat(members.slice().sort(Buffer.compare)));
58
52
  }
59
53
 
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
- }
54
+ // The requested v3 Extensions (a bare SEQUENCE OF Extension, RFC 2985 sec. 5.4.2) -- the shared builder
55
+ // primitive (a CSR REQUESTS extensions; no CA gates), object form or a pre-encoded Extension DER array.
56
+ function _buildRequestedExtensions(extSpec, subjectSpki) { return _b.requestedExtensions(extSpec, subjectSpki); }
97
57
 
98
58
  // challengePassword ::= DirectoryString bounded 1..255 (RFC 2985 sec. 5.4.1). PrintableString when the
99
59
  // value is in that charset, else UTF8String; never a Teletex/BMP/Universal string for a new value.
package/lib/pki-build.js CHANGED
@@ -13,6 +13,8 @@
13
13
  var asn1 = require("./asn1-der");
14
14
  var schema = require("./schema-engine");
15
15
  var compositeSig = require("./composite-sig");
16
+ var guard = require("./guard-all");
17
+ var oid = require("./oid");
16
18
  var nodeCrypto = require("crypto");
17
19
 
18
20
  var b = asn1.build;
@@ -31,6 +33,17 @@ function makeBuilder(ctx) {
31
33
  var NAME_SCHEMA = ctx.NAME_SCHEMA, SPKI_SCHEMA = ctx.SPKI_SCHEMA;
32
34
  function E(kind, message, cause) { return new ErrorClass(ctx.prefix + "/" + kind, message, cause); }
33
35
  function code(kind) { return ctx.prefix + "/" + kind; }
36
+ // A full-code error factory (guard.* helpers invoke E as `E(fullCode, msg)`, not `E(kind, msg)`).
37
+ function rawErr(fullCode, message, cause) { return new ErrorClass(fullCode, message, cause); }
38
+
39
+ // RFC 5280 sec. 4.1.2.5 -- UTCTime for 1950..2049, GeneralizedTime otherwise (UTCTime's two-digit year
40
+ // only represents that window). The shared cutover so a certificate / CRMF / any validity encoder cannot
41
+ // drift on the boundary. `which` labels the instant in a validation error.
42
+ function timeDer(date, which) {
43
+ guard.time.assertValid(date, rawErr, code("bad-input"), which);
44
+ var y = date.getUTCFullYear();
45
+ return (y >= 1950 && y <= 2049) ? b.utcTime(date) : b.generalizedTime(date);
46
+ }
34
47
 
35
48
  // ---- distinguished name encoding (RFC 5280 sec. 4.1.2.4) ----
36
49
  // countryName is a PrintableString SIZE(2), emailAddress an IA5String; every other new-name attribute
@@ -226,6 +239,55 @@ function makeBuilder(ctx) {
226
239
  var last = n.children[n.children.length - 1];
227
240
  if (last.tagNumber !== asn1.TAGS.OCTET_STRING || last.tagClass !== "universal") throw E("bad-input", "pre-encoded extension [" + idx + "] extnValue must be an OCTET STRING");
228
241
  }
242
+ // The requested v3 Extensions (a bare SEQUENCE OF Extension) from an object of the recognized keys, or
243
+ // an array of pre-encoded Extension DER (shape- + recognized-value-validated). Shared by every enrollment
244
+ // request that carries requested extensions (a PKCS#10 extensionRequest attribute, a CRMF CertTemplate
245
+ // extensions [9]); a request REQUESTS extensions, so there are no CA cross-field gates. `spki` feeds the
246
+ // subjectKeyIdentifier auto-derive.
247
+ var REQ_EXT_KEYS = {
248
+ subjectAltName: 1, keyUsage: 1, keyUsageCritical: 1, extendedKeyUsage: 1, extendedKeyUsageCritical: 1,
249
+ basicConstraints: 1, certificatePolicies: 1, certificatePoliciesCritical: 1, subjectKeyIdentifier: 1,
250
+ };
251
+ function requestedExtensions(extSpec, spki) {
252
+ var EXT_DECODERS = ctx.EXT_DECODERS;
253
+ if (Array.isArray(extSpec)) {
254
+ if (!extSpec.length) throw E("bad-input", "the requested extensions list must carry at least one extension");
255
+ var seen = {};
256
+ return b.sequence(extSpec.map(function (e, i) {
257
+ var der = reqDer(e, "extension");
258
+ assertValidExtension(der, i);
259
+ var n = asn1.decode(der);
260
+ var extnId = asn1.read.oid(n.children[0]);
261
+ if (seen[extnId]) throw E("bad-input", "duplicate requested extension " + extnId + " (RFC 5280 sec. 4.2)");
262
+ seen[extnId] = true;
263
+ var dec = EXT_DECODERS && EXT_DECODERS[extnId];
264
+ if (dec) {
265
+ try { dec(asn1.read.octetString(n.children[n.children.length - 1])); }
266
+ catch (err) { if (err instanceof ErrorClass) throw err; throw E("bad-input", "pre-encoded " + (oid.name(extnId) || extnId) + " extension value is malformed", err); }
267
+ }
268
+ return b.raw(der);
269
+ }));
270
+ }
271
+ if (!extSpec || typeof extSpec !== "object") throw E("bad-input", "requested extensions must be an object or an array of pre-encoded Extension DER");
272
+ Object.keys(extSpec).forEach(function (k) {
273
+ if (!REQ_EXT_KEYS[k]) throw E("bad-input", "unknown requested extension " + JSON.stringify(k) + "; pass a pre-encoded Extension DER via the array form for a custom extension");
274
+ });
275
+ var out = [];
276
+ if (extSpec.subjectKeyIdentifier != null) {
277
+ // Auto-deriving the SKI (subjectKeyIdentifier: true) hashes the subject public key, so it needs one;
278
+ // reject the request when no public key is available (supply a Buffer key id instead).
279
+ if (extSpec.subjectKeyIdentifier === true && spki == null) throw E("bad-input", "subjectKeyIdentifier auto-derive (true) requires the public key -- supply a Buffer key id, or include the public key");
280
+ out.push(ext(O("subjectKeyIdentifier"), false, extSki(skiKeyId(extSpec.subjectKeyIdentifier, spki))));
281
+ }
282
+ if (extSpec.keyUsage != null) out.push(ext(O("keyUsage"), extSpec.keyUsageCritical !== false, extKeyUsage(extSpec.keyUsage)));
283
+ if (extSpec.extendedKeyUsage != null) out.push(ext(O("extKeyUsage"), !!extSpec.extendedKeyUsageCritical, extExtKeyUsage(extSpec.extendedKeyUsage)));
284
+ if (extSpec.basicConstraints != null) { validateBcSpec(extSpec.basicConstraints); out.push(ext(O("basicConstraints"), extSpec.basicConstraints.critical !== false, extBasicConstraints(extSpec.basicConstraints))); }
285
+ if (extSpec.subjectAltName != null) out.push(ext(O("subjectAltName"), false, extSan(extSpec.subjectAltName)));
286
+ if (extSpec.certificatePolicies != null) out.push(ext(O("certificatePolicies"), !!extSpec.certificatePoliciesCritical, extCertPolicies(extSpec.certificatePolicies)));
287
+ if (!out.length) throw E("bad-input", "the requested extensions object must request at least one extension");
288
+ return b.sequence(out);
289
+ }
290
+
229
291
  // A positive, non-zero INTEGER of at most 20 content octets (RFC 5280 sec. 4.1.2.2 / RFC 5755 sec.
230
292
  // 4.2.5 -- the identical serial rule for a certificate and an attribute certificate). A random 20-octet
231
293
  // positive serial is generated when none is supplied. Accepts a BigInt, a safe integer, a decimal /
@@ -287,7 +349,8 @@ function makeBuilder(ctx) {
287
349
  return {
288
350
  E: E, code: code, KU_BIT: KU_BIT,
289
351
  encodeName: encodeName, isEmptyName: isEmptyName, encodeGeneralName: encodeGeneralName,
290
- encodeGeneralNames: encodeGeneralNames, serialInteger: serialInteger,
352
+ encodeGeneralNames: encodeGeneralNames, serialInteger: serialInteger, timeDer: timeDer,
353
+ requestedExtensions: requestedExtensions,
291
354
  extKeyUsage: extKeyUsage, extExtKeyUsage: extExtKeyUsage, validateBcSpec: validateBcSpec,
292
355
  extBasicConstraints: extBasicConstraints, pathLen: pathLen, extSki: extSki, extAki: extAki,
293
356
  extSan: extSan, extCertPolicies: extCertPolicies, ext: ext,
package/lib/schema-all.js CHANGED
@@ -298,7 +298,7 @@ module.exports = {
298
298
  x509: { parse: x509.parse, pemDecode: x509.pemDecode, pemEncode: x509.pemEncode },
299
299
  // C509 is CBOR, not DER: an explicit-call surface only, NOT added to FORMATS / the detect-and-route
300
300
  // parse() below (which detect DER shapes). No pemDecode/pemEncode -- C509 is binary CBOR.
301
- c509: { parse: c509.parse },
301
+ c509: { parse: c509.parse, encode: c509.encode },
302
302
  crl: { parse: crl.parse, pemDecode: crl.pemDecode, pemEncode: crl.pemEncode },
303
303
  csr: { parse: csr.parse, pemDecode: csr.pemDecode, pemEncode: csr.pemEncode },
304
304
  pkcs8: { parse: pkcs8.parse, parseEncrypted: pkcs8.parseEncrypted, pemDecode: pkcs8.pemDecode, pemEncode: pkcs8.pemEncode },
@@ -17,6 +17,8 @@
17
17
  var cbor = require("./cbor-det");
18
18
  var asn1 = require("./asn1-der");
19
19
  var oid = require("./oid");
20
+ var x509 = require("./schema-x509");
21
+ var pkix = require("./schema-pkix");
20
22
  var constants = require("./constants");
21
23
  var frameworkError = require("./framework-error");
22
24
  var validator = require("./validator-all");
@@ -452,14 +454,16 @@ function parse(input) {
452
454
  signatureValue: signatureValue,
453
455
  };
454
456
 
455
- // Native (type-2) signed region (sec. 3.1.12): the RAW bytes of the CBOR-sequence elements 0..9 (NOT
456
- // the outer array head, NOT the signature) -- a zero-copy subarray a native verifier hashes.
457
- if (type === 2) {
458
- var b0 = f[0].bytes, b9 = f[9].bytes;
459
- var start = b0.byteOffset - input.byteOffset;
460
- var end = b9.byteOffset - input.byteOffset + b9.length;
461
- result.signedData = input.subarray(start, end);
462
- }
457
+ // The RAW bytes of CBOR-array elements 0..9 (NOT the outer array head, NOT the signature) -- a zero-copy
458
+ // subarray. For a native (type-2) certificate this is the signed region a verifier hashes (sec. 3.1.12);
459
+ // surfaced as `_fieldBytes` for BOTH types so encode() re-emits a parsed certificate byte-for-byte (a
460
+ // re-derivation from the decoded values could differ on a canonical-equivalent form, breaking a type-2
461
+ // signature or a type-3 DER reconstruction). `signedData` keeps its type-2-only signed-region meaning.
462
+ // From the DECODED root's own bytes (root.bytes == array head + all 11 elements), not offset arithmetic
463
+ // on `input` (which breaks when input is a Uint8Array the codec normalized to a different buffer): the
464
+ // fields region is root.bytes minus the 1-byte array(11) head and minus the trailing signatureValue.
465
+ result._fieldBytes = root.bytes.subarray(1, root.bytes.length - f[10].bytes.length);
466
+ if (type === 2) result.signedData = result._fieldBytes;
463
467
 
464
468
  // Type-3 is an invertible re-encoding of a DER X.509 certificate: reconstruct the original DER
465
469
  // byte-for-byte so the original signature verifies and x509.parse recovers the certificate.
@@ -476,4 +480,325 @@ function matches(node) {
476
480
  (Number(node.children[0].argument) === 2 || Number(node.children[0].argument) === 3);
477
481
  }
478
482
 
479
- module.exports = { parse: parse, matches: matches };
483
+ // ---- the encode (the producing side; draft-20 sec. 3) -----------------------
484
+ // The byte-exact inverse of parse/reconstruct: emit the 11-element deterministic-CBOR C509 array via the
485
+ // cbor.build.* emitter. Two inputs, dispatched structurally: a DER X.509 certificate (Buffer/PEM) -> the
486
+ // FLAGSHIP type-3 forward transform (parse(encode(der)).reconstructedDer == der, so the original signature
487
+ // verifies); a c509.parse result -> re-emit its native array. Signing-free (mirrors ct.encodeSctList).
488
+
489
+ // The registry INVERSE tables -- the canonical int per name (the lossy forward map is resolved to ONE
490
+ // choice: EXT_BY_INT maps both 2 and 7 to keyUsage, so keyUsage encodes to the canonical draft int 2).
491
+ var SIG_ALG_TO_INT = { ecdsaWithSHA256: 0, ecdsaWithSHA384: 1, ecdsaWithSHA512: 2 };
492
+ var PK_ALG_TO_INT = { rsaEncryption: 0, "ecPublicKey|prime256v1": 1, "ecPublicKey|secp384r1": 2, "ecPublicKey|secp521r1": 3 };
493
+ var ATTR_TO_INT = { commonName: 1, surname: 2, serialNumber: 3, countryName: 4, localityName: 6, stateOrProvinceName: 7, organizationName: 8, organizationalUnitName: 9, title: 10 };
494
+ var EXT_TO_INT = { subjectKeyIdentifier: 1, keyUsage: 2, subjectAltName: 3, basicConstraints: 4, authorityKeyIdentifier: 10 };
495
+
496
+ // A non-negative BigInt -> its minimal big-endian ~biguint bytes (the leading 0x00 sign octet omitted).
497
+ function _minBytes(n) {
498
+ if (n < 0n) throw _err("c509/bad-serial", "a ~biguint value must be non-negative");
499
+ if (n === 0n) return Buffer.alloc(0);
500
+ var hex = n.toString(16); if (hex.length % 2) hex = "0" + hex;
501
+ return Buffer.from(hex, "hex");
502
+ }
503
+ // A C509 AlgorithmIdentifier -> int (registry) | ~oid (bare bytes) | [~oid, params]. `key` selects the row.
504
+ function _encAlgorithm(alg, toInt, key) {
505
+ var i = toInt[key];
506
+ if (i !== undefined && !(alg.parameters && alg.parameters.length)) return cbor.build.int(BigInt(i));
507
+ var oidBytes = cbor.build.byteString(asn1.encodeOidContent(alg.oid)); // ~oid: bare BER OID content
508
+ if (alg.parameters && alg.parameters.length) return cbor.build.array([oidBytes, cbor.build.byteString(alg.parameters)]);
509
+ return oidBytes;
510
+ }
511
+ // A SpecialText attribute value -> CBOR (text | tag-48 EUI). v1 encodes the text + eui64 forms.
512
+ function _encSpecialText(rdn) {
513
+ if (rdn.eui64) return cbor.build.tag(48, cbor.build.byteString(rdn.eui64));
514
+ return cbor.build.textString(String(rdn.value));
515
+ }
516
+ // A Name -> CBOR: null (issuer only) | a bare SpecialText single utf8 commonName | an array of RDN pairs.
517
+ function _encName(name, isSubject) {
518
+ if (name === null || name === undefined) {
519
+ if (!isSubject) return cbor.build.nullValue(); // issuer == subject (self-signed)
520
+ throw _err("c509/bad-name", "the subject Name is required");
521
+ }
522
+ var rdns = name.rdns || [];
523
+ if (rdns.length === 1 && rdns[0].type === "commonName" && !rdns[0].printable) return _encSpecialText(rdns[0]);
524
+ var items = [];
525
+ rdns.forEach(function (rdn) {
526
+ var ai = ATTR_TO_INT[rdn.type];
527
+ if (ai === undefined) throw _err("c509/bad-name", "attribute type " + rdn.type + " has no C509 registry int");
528
+ items.push(cbor.build.int(BigInt(rdn.printable ? -ai : ai))); // sign selects printableString
529
+ items.push(_encSpecialText(rdn));
530
+ });
531
+ return cbor.build.array(items);
532
+ }
533
+ // subjectPublicKey -> CBOR: EC point byte string, or an RSA ~biguint modulus ([modulus, exponent] when e != 65537).
534
+ function _encSpk(r) {
535
+ if (r.rsaPublicKey) {
536
+ var mod = cbor.build.byteString(_minBytes(r.rsaPublicKey.modulus));
537
+ if (r.rsaPublicKey.exponent === 65537n) return mod;
538
+ return cbor.build.array([mod, cbor.build.byteString(_minBytes(r.rsaPublicKey.exponent))]);
539
+ }
540
+ if (!Buffer.isBuffer(r.subjectPublicKey)) throw _err("c509/bad-spki", "the subjectPublicKey bytes are missing");
541
+ return cbor.build.byteString(r.subjectPublicKey);
542
+ }
543
+ // extensions -> CBOR: the keyUsage int-shortcut (a lone keyUsage), else an array of [extID, extValue] pairs.
544
+ function _encExtensions(exts) {
545
+ if (exts.length === 1 && exts[0].name === "keyUsage" && typeof exts[0].keyUsageBits === "number") {
546
+ return cbor.build.int(BigInt(exts[0].critical ? -exts[0].keyUsageBits : exts[0].keyUsageBits));
547
+ }
548
+ var items = [];
549
+ exts.forEach(function (ext) {
550
+ var ei = EXT_TO_INT[ext.name];
551
+ if (ei !== undefined) {
552
+ items.push(cbor.build.int(BigInt(ext.critical ? -ei : ei)));
553
+ // a registered-int extension carries its extnValue DER bytes as a bare byte string.
554
+ if (!Buffer.isBuffer(ext.value)) throw _err("c509/non-invertible", "extension " + ext.name + " has no byte-string value to encode");
555
+ items.push(cbor.build.byteString(ext.value));
556
+ } else {
557
+ items.push(cbor.build.byteString(asn1.encodeOidContent(ext.oid))); // ~oid extension id
558
+ if (!Buffer.isBuffer(ext.value)) throw _err("c509/non-invertible", "extension " + (ext.oid || ext.name) + " has no byte-string value to encode");
559
+ var bs = cbor.build.byteString(ext.value);
560
+ items.push(ext.critical ? cbor.build.array([bs]) : bs); // critical ~oid value wraps in a 1-element array
561
+ }
562
+ });
563
+ return cbor.build.array(items);
564
+ }
565
+ // forward-declared below; the DER X.509 -> type-3 C509 structured result.
566
+ var _derToType3;
567
+ // A hand-built (no _fieldBytes) result must carry the structured fields the encode below reads; a missing or
568
+ // wrong-typed field fails closed with a typed verdict rather than a raw property-access crash.
569
+ function _requireResultShape(r) {
570
+ if (r.certificateType == null) throw _err("c509/bad-input", "a C509 result must carry certificateType");
571
+ if (r.serialNumber == null && r.serialNumberHex == null) throw _err("c509/bad-input", "a C509 result must carry serialNumber or serialNumberHex");
572
+ if (!r.signatureAlgorithm || typeof r.signatureAlgorithm.name !== "string") throw _err("c509/bad-input", "a C509 result must carry signatureAlgorithm.name");
573
+ if (!r.subjectPublicKeyAlgorithm || typeof r.subjectPublicKeyAlgorithm.name !== "string") throw _err("c509/bad-input", "a C509 result must carry subjectPublicKeyAlgorithm.name");
574
+ if (!r.validity || !(r.validity.notBefore instanceof Date) || (r.validity.notAfter !== null && !(r.validity.notAfter instanceof Date))) throw _err("c509/bad-input", "a C509 result must carry validity.notBefore (Date) and notAfter (Date or null)");
575
+ if (!Array.isArray(r.extensions)) throw _err("c509/bad-input", "a C509 result must carry an extensions array");
576
+ if (!Buffer.isBuffer(r.signatureValue)) throw _err("c509/bad-input", "a C509 result must carry a Buffer signatureValue");
577
+ }
578
+ // A validity Date -> its C509 ~time (a non-negative CBOR epoch uint). A pre-epoch date cannot be
579
+ // represented (the parser accepts only an unwrapped major-type-0 integer) and fails closed here.
580
+ function _validityUint(date, label) {
581
+ var secs = Math.floor(date.getTime() / 1000);
582
+ if (!isFinite(secs) || secs < 0) throw _err("c509/bad-validity", label + " is before the Unix epoch or not a valid date; C509 ~time is a non-negative CBOR epoch");
583
+ return cbor.build.uint(BigInt(secs));
584
+ }
585
+ // A structured C509 result -> the 11-element deterministic-CBOR array.
586
+ function _encodeC509Array(r) {
587
+ // Re-emit a PARSED certificate's raw fields (elements 0..9) VERBATIM -- re-deriving from the decoded
588
+ // values could differ on a canonical-equivalent form (a byte-string attribute value, a registry alias)
589
+ // and break a type-2 native signature (which covers these bytes) or a type-3 DER reconstruction (which
590
+ // depends on the field values). Both types preserve the exact bytes; only a hand-built result (no
591
+ // _fieldBytes) re-derives from the structured values below.
592
+ if (Buffer.isBuffer(r._fieldBytes)) {
593
+ if (!Buffer.isBuffer(r.signatureValue)) throw _err("c509/bad-input", "a re-emitted certificate must carry a Buffer signatureValue");
594
+ var out = Buffer.concat([Buffer.from([0x8b]), r._fieldBytes, cbor.build.byteString(r.signatureValue)]); // array(11) head + fields 0..9 + signatureValue
595
+ parse(out); // fail closed: a caller-mutated _fieldBytes must still re-parse as a valid C509, else parse throws a typed c509/* verdict
596
+ return out;
597
+ }
598
+ _requireResultShape(r);
599
+ var pkKey = r.subjectPublicKeyAlgorithm.curve ? r.subjectPublicKeyAlgorithm.name + "|" + r.subjectPublicKeyAlgorithm.curve : r.subjectPublicKeyAlgorithm.name;
600
+ var arr = cbor.build.array([
601
+ cbor.build.int(BigInt(r.certificateType)),
602
+ cbor.build.byteString(r.serialNumberHex != null ? Buffer.from(r.serialNumberHex, "hex") : _minBytes(r.serialNumber)),
603
+ _encAlgorithm(r.signatureAlgorithm, SIG_ALG_TO_INT, r.signatureAlgorithm.name),
604
+ _encName(r.issuer, false),
605
+ _validityUint(r.validity.notBefore, "validityNotBefore"),
606
+ r.validity.notAfter === null ? cbor.build.nullValue() : _validityUint(r.validity.notAfter, "validityNotAfter"),
607
+ _encName(r.subject, true),
608
+ _encAlgorithm(r.subjectPublicKeyAlgorithm, PK_ALG_TO_INT, pkKey),
609
+ _encSpk(r),
610
+ _encExtensions(r.extensions),
611
+ cbor.build.byteString(r.signatureValue),
612
+ ]);
613
+ parse(arr); // fail closed: a hand-built result must re-parse as a valid C509 (as the verbatim path checks), else parse throws a typed c509/* verdict
614
+ return arr;
615
+ }
616
+
617
+ /**
618
+ * @primitive pki.schema.c509.encode
619
+ * @signature pki.schema.c509.encode(input[, opts]) -> Buffer
620
+ * @since 0.3.4
621
+ * @status experimental
622
+ * @spec draft-ietf-cose-cbor-encoded-cert, RFC 8949, RFC 9090
623
+ * @related pki.schema.c509.parse
624
+ *
625
+ * Encode a C509 certificate to its deterministic-CBOR bytes -- the producing-side inverse of
626
+ * `pki.schema.c509.parse`. `input` is either a DER X.509 v3 certificate (a Buffer or PEM string), which is
627
+ * forward-transformed to a **type-3** C509 (a compact CBOR re-encoding whose signature is copied from the
628
+ * source and re-expressed as a fixed-width r||s, so `parse(encode(der)).reconstructedDer` reproduces the
629
+ * original DER byte for byte and the original signature still verifies), or a `pki.schema.c509.parse`
630
+ * result object, which is re-emitted to its native deterministic-CBOR array. The emission is canonical
631
+ * deterministic CBOR (RFC 8949 sec. 4.2) -- shortest-form heads, definite lengths, sorted map keys, and the
632
+ * registry integer shorthand for every registered algorithm / attribute / extension. It is signing-free (a
633
+ * byte transform, like `pki.ct.encodeSctList`); a shape outside the covered set throws a typed `C509Error`.
634
+ *
635
+ * The fixed-width ECDSA r||s is sized by the ISSUER's signing curve, which a leaf certificate does not
636
+ * carry. It is resolved authoritatively (never a magnitude guess, and matching issuer/subject Names are not
637
+ * taken as proof of self-signing): from `opts.issuerCurve`, or from the RFC 5480 standard digest<->curve
638
+ * pairing the signature algorithm implies. A certificate signed with a non-standard digest/curve pairing
639
+ * (its r/s wider than the digest's standard curve) fails closed -- supply the issuer curve via
640
+ * `opts.issuerCurve`.
641
+ *
642
+ * @opts
643
+ * - `issuerCurve` (string) -- the ISSUER's ECDSA signing curve "P-256" / "P-384" / "P-521" (or the OID
644
+ * names prime256v1 / secp384r1 / secp521r1); authoritative, overrides the resolution above. Consulted
645
+ * only for the DER -> type-3 path; ignored when re-emitting a parse result.
646
+ *
647
+ * @example
648
+ * var cbor = pki.schema.c509.encode(signerCertDer); // a DER cert -> a compact type-3 C509
649
+ * pki.schema.c509.parse(cbor).certificateType; // 3
650
+ */
651
+ // A commonName string that is a MAC / EUI address ("HH-HH-HH-FF-FE-HH-HH-HH") -> the C509 tag-48 byte
652
+ // value (draft-20 sec. 3.2.3): an FF-FE-in-the-middle EUI-64 collapses to its 6-byte EUI-48, else the
653
+ // 8-byte EUI-64 verbatim. A non-MAC commonName returns null (encoded as text). The exact inverse of
654
+ // _macToEui64String, so the reconstruction rebuilds the identical DER commonName string.
655
+ function _euiFromCn(value) {
656
+ if (!/^[0-9A-F]{2}(-[0-9A-F]{2}){7}$/.test(value)) return null;
657
+ var bytes = Buffer.from(value.replace(/-/g, ""), "hex");
658
+ if (bytes[3] === 0xff && bytes[4] === 0xfe) return Buffer.concat([bytes.subarray(0, 3), bytes.subarray(5)]);
659
+ return bytes;
660
+ }
661
+ // A DER Name -> the C509 structured rdns, decoding each attribute's string type (PrintableString ->
662
+ // printable, UTF8String -> utf8; the C509 int sign carries this). Single-attribute RDNs only (v1).
663
+ function _c509NameFromDer(nameBytes) {
664
+ var node = asn1.decode(nameBytes);
665
+ var rdns = [];
666
+ (node.children || []).forEach(function (rdnSet) {
667
+ if (!rdnSet.children || rdnSet.children.length !== 1) throw _err("c509/non-invertible", "a C509 Name requires single-attribute RDNs");
668
+ var attr = rdnSet.children[0];
669
+ var attrName = oid.name(asn1.read.oid(attr.children[0]));
670
+ if (attrName == null || ATTR_TO_INT[attrName] === undefined) throw _err("c509/non-invertible", "attribute type " + attrName + " has no C509 registry integer");
671
+ var valNode = attr.children[1];
672
+ var value = asn1.read.string(valNode);
673
+ var eui = attrName === "commonName" ? _euiFromCn(value) : null;
674
+ if (eui) rdns.push({ type: attrName, value: value, eui64: eui }); // tag-48 MAC commonName
675
+ else rdns.push({ type: attrName, value: value, printable: valNode.tagNumber === asn1.TAGS.PRINTABLE_STRING });
676
+ });
677
+ return { rdns: rdns };
678
+ }
679
+ // A keyUsage extnValue (a DER KeyUsage BIT STRING) -> the C509 integer whose bit i is the named bit i.
680
+ function _keyUsageBitsFromDer(extnValue) {
681
+ var bs;
682
+ try {
683
+ bs = asn1.read.bitString(asn1.decode(extnValue));
684
+ } catch (_e) {
685
+ return null; // a malformed keyUsage BIT STRING cannot take the int shortcut -> encode it as a raw extension
686
+ }
687
+ var total = bs.bytes.length * 8 - bs.unusedBits, value = 0;
688
+ for (var bit = 0; bit < total && bit < 31; bit++) { if (bs.bytes[bit >> 3] & (0x80 >> (bit & 7))) value |= (1 << bit); }
689
+ return value > 0 && value <= 0x1ff ? value : null;
690
+ }
691
+ var _NO_EXPIRY = Date.UTC(9999, 11, 31, 23, 59, 59);
692
+ // The C509 (OID/node) curve names <-> the WebCrypto namedCurve the P1363 converter expects, and the
693
+ // RFC 5480 standard digest<->curve pairing an ECDSA signature algorithm implies.
694
+ var NODE_TO_WEBCRYPTO = { "prime256v1": "P-256", "secp384r1": "P-384", "secp521r1": "P-521" };
695
+ var WEBCRYPTO_FIELD_BYTES = { "P-256": 32, "P-384": 48, "P-521": 66 };
696
+ var SIG_ALG_TO_CURVE = { "ecdsaWithSHA256": "P-256", "ecdsaWithSHA384": "P-384", "ecdsaWithSHA512": "P-521" };
697
+ // A DER INTEGER magnitude byte length: the content past any leading sign/pad octet.
698
+ function _magBytes(intNode) {
699
+ var c = intNode.content, i = 0;
700
+ while (i < c.length - 1 && c[i] === 0x00) i++;
701
+ return c.length - i;
702
+ }
703
+ // The max r/s magnitude width of a DER ECDSA signature (validating its two-INTEGER shape).
704
+ function _sigMagWidth(derSig) {
705
+ var n;
706
+ try { n = asn1.decode(derSig); } catch (e) { throw _err("c509/bad-signature", "the ECDSA issuer signature is not valid DER", e); }
707
+ if (n.tagNumber !== asn1.TAGS.SEQUENCE || n.tagClass !== "universal" || !n.children || n.children.length !== 2) throw _err("c509/bad-signature", "the ECDSA issuer signature must be a SEQUENCE of two INTEGERs");
708
+ for (var i = 0; i < 2; i++) {
709
+ var ch = n.children[i];
710
+ if (ch.tagNumber !== asn1.TAGS.INTEGER || ch.tagClass !== "universal" || ch.constructed) throw _err("c509/bad-signature", "the ECDSA issuer signature r and s must be universal INTEGERs");
711
+ }
712
+ return Math.max(_magBytes(n.children[0]), _magBytes(n.children[1]));
713
+ }
714
+ var CURVE_ORDER = ["P-256", "P-384", "P-521"]; // ascending field size
715
+ // The smallest supported curve whose field holds a magnitude, or null if none does.
716
+ function _minCurveForMag(mag) {
717
+ for (var i = 0; i < CURVE_ORDER.length; i++) { if (mag <= WEBCRYPTO_FIELD_BYTES[CURVE_ORDER[i]]) return CURVE_ORDER[i]; }
718
+ return null;
719
+ }
720
+ // The ISSUER signature's fixed-width r||s is sized by the ISSUER's signing curve. The DER ECDSA signature
721
+ // does NOT carry the curve and the r/s magnitudes are only a lower bound (a P-384 signature with small r/s
722
+ // is byte-indistinguishable from a P-256 one), so the curve is resolved from an AUTHORITATIVE source, never
723
+ // a guess: (1) an explicit opts.issuerCurve, or (2) the RFC 5480 standard digest<->curve pairing the
724
+ // signature algorithm implies -- and ONLY when that pairing is the smallest curve the magnitude admits.
725
+ // Matching issuer/subject Names are NOT treated as proof of self-signing (a self-issued certificate may be
726
+ // cross-signed by a different key on a different curve). A signature whose r/s exceed the digest's standard
727
+ // curve (a larger key) OR whose magnitude also fits a SMALLER curve (a smaller key signing with a larger
728
+ // digest) leaves the curve undetermined: it fails closed and directs the caller to opts.issuerCurve.
729
+ function _resolveIssuerSigCurve(c, opts) {
730
+ var mag = _sigMagWidth(c.signatureValue.bytes), curve;
731
+ if (opts && opts.issuerCurve != null) {
732
+ curve = String(opts.issuerCurve);
733
+ if (NODE_TO_WEBCRYPTO[curve]) curve = NODE_TO_WEBCRYPTO[curve];
734
+ if (!WEBCRYPTO_FIELD_BYTES[curve]) throw _err("c509/bad-input", "opts.issuerCurve must be P-256 / P-384 / P-521 (or prime256v1 / secp384r1 / secp521r1); got " + opts.issuerCurve);
735
+ } else {
736
+ curve = SIG_ALG_TO_CURVE[c.signatureAlgorithm.name];
737
+ if (!curve) throw _err("c509/non-invertible", "cannot resolve the issuer signing curve for signature algorithm " + c.signatureAlgorithm.name);
738
+ var minCurve = _minCurveForMag(mag);
739
+ if (!minCurve) throw _err("c509/non-invertible", "the ECDSA signature r/s width " + mag + " exceeds every supported curve field");
740
+ if (WEBCRYPTO_FIELD_BYTES[curve] < mag) throw _err("c509/non-invertible", "the issuer signed with a non-standard digest/curve pairing (r/s width " + mag + " exceeds the " + curve + " field implied by " + c.signatureAlgorithm.name + "); pass opts.issuerCurve (P-256 / P-384 / P-521)");
741
+ if (WEBCRYPTO_FIELD_BYTES[curve] > WEBCRYPTO_FIELD_BYTES[minCurve]) throw _err("c509/non-invertible", "signature algorithm " + c.signatureAlgorithm.name + " does not uniquely determine the issuer curve (r/s width " + mag + " also fits " + minCurve + "); pass opts.issuerCurve (P-256 / P-384 / P-521)");
742
+ }
743
+ if (mag > WEBCRYPTO_FIELD_BYTES[curve]) throw _err("c509/non-invertible", "the ECDSA signature r/s width " + mag + " does not fit the resolved " + curve + " field");
744
+ return curve;
745
+ }
746
+ // Compress an uncompressed SEC1 EC point (0x04||X||Y) to the C509 marker form (draft-20 sec. 3.2.2): the
747
+ // sign-of-Y marker 0xFE (Y even) / 0xFD (Y odd) followed by X. The inverse of webcrypto.decompressEcPoint,
748
+ // so the type-3 reconstruction recovers the exact original point. A non-0x04 point is kept verbatim.
749
+ function _compressEcPoint(point, coordLen) {
750
+ if (!point.length || point[0] !== 0x04) return point;
751
+ if (point.length !== 1 + 2 * coordLen) throw _err("c509/non-invertible", "uncompressed EC point length " + point.length + " does not match the curve field size");
752
+ var x = point.subarray(1, 1 + coordLen), y = point.subarray(1 + coordLen);
753
+ return Buffer.concat([Buffer.from([(y[y.length - 1] & 1) ? 0xfd : 0xfe]), x]);
754
+ }
755
+ // A DER X.509 v3 certificate -> the type-3 C509 structured result (the inverse of _reconstructDer). Only
756
+ // the reconstruction's covered set is invertible; encode() self-verifies the byte-exact round trip.
757
+ _derToType3 = function (input, opts) {
758
+ var c;
759
+ try { c = x509.parse(input); } catch (e) { throw _err("c509/bad-input", "the input is not a valid X.509 certificate", e); }
760
+ if (!/^ecdsa/i.test(c.signatureAlgorithm.name || "")) throw _err("c509/non-invertible", "type-3 C509 encoding covers only ECDSA-signed certificates; got " + (c.signatureAlgorithm.name || "an unregistered algorithm"));
761
+ if (c.subjectPublicKeyInfo.algorithm.name !== "ecPublicKey") throw _err("c509/non-invertible", "type-3 C509 encoding covers only EC (ecPublicKey) certificates in v1; got " + (c.subjectPublicKeyInfo.algorithm.name || "an unregistered algorithm"));
762
+ var curveOid = asn1.read.oid(asn1.decode(c.subjectPublicKeyInfo.algorithm.parameters));
763
+ var curve = oid.name(curveOid);
764
+ var coordLen = EC_FIELD_BYTES[curve];
765
+ if (!coordLen) throw _err("c509/non-invertible", "unsupported EC subject curve " + (curve || curveOid));
766
+ var sigCurve = _resolveIssuerSigCurve(c, opts); // the ISSUER signing curve, resolved authoritatively (opts / digest pairing)
767
+ var spkiNode = asn1.decode(c.subjectPublicKeyInfo.bytes);
768
+ return {
769
+ certificateType: 3,
770
+ serialNumber: c.serialNumber, // no serialNumberHex -> the encoder uses the minimal ~biguint magnitude
771
+ signatureAlgorithm: { name: c.signatureAlgorithm.name, oid: c.signatureAlgorithm.oid },
772
+ issuer: _c509NameFromDer(c.issuer.bytes),
773
+ validity: { notBefore: c.validity.notBefore, notAfter: c.validity.notAfter.getTime() === _NO_EXPIRY ? null : c.validity.notAfter },
774
+ subject: _c509NameFromDer(c.subject.bytes),
775
+ subjectPublicKeyAlgorithm: { name: "ecPublicKey", oid: c.subjectPublicKeyInfo.algorithm.oid, curve: curve },
776
+ subjectPublicKey: _compressEcPoint(asn1.read.bitString(spkiNode.children[1]).bytes, coordLen), // 0x04||X||Y -> C509 compressed marker
777
+ rsaPublicKey: null,
778
+ extensions: (c.extensions || []).map(function (e) {
779
+ var ext = { name: e.name, oid: e.oid, critical: !!e.critical, value: e.value };
780
+ if (e.name === "keyUsage") { var bits = _keyUsageBitsFromDer(e.value); if (bits != null) ext.keyUsageBits = bits; } // enable the int-shortcut
781
+ return ext;
782
+ }),
783
+ signatureValue: validator.sig.ecdsaDerToP1363(c.signatureValue.bytes, sigCurve, C509Error, "c509/bad-signature"),
784
+ };
785
+ };
786
+
787
+ function encode(input, opts) {
788
+ if (input && typeof input === "object" && !Buffer.isBuffer(input) && input.certificateType != null) {
789
+ return _encodeC509Array(input); // a parse result -> re-emit its native array
790
+ }
791
+ if (!Buffer.isBuffer(input) && typeof input !== "string") throw _err("c509/bad-input", "encode input must be a DER/PEM X.509 certificate or a c509.parse result");
792
+ // Normalize input to DER through the SAME shared coercion x509.parse uses (a PEM string, a PEM-armored
793
+ // Buffer, or raw DER all collapse to the certificate DER), so the self-verify compares against exactly the
794
+ // bytes _derToType3 processed -- never a Buffer of PEM text.
795
+ var origDer = pkix.coerceToDer(input, { pemLabel: "CERTIFICATE", PemError: frameworkError.PemError, ErrorClass: C509Error, prefix: "c509" });
796
+ var encoded = _encodeC509Array(_derToType3(origDer, opts));
797
+ // The type-3 transform MUST invert back to the original DER byte-for-byte (so the original signature
798
+ // verifies) -- self-verify it, failing closed on any edge the reconstruction cannot reproduce.
799
+ var recon = parse(encoded).reconstructedDer;
800
+ if (!recon || Buffer.compare(recon, origDer) !== 0) throw _err("c509/non-invertible", "the type-3 C509 does not reconstruct the source certificate byte-for-byte");
801
+ return encoded;
802
+ }
803
+
804
+ module.exports = { parse: parse, matches: matches, encode: encode };
package/lib/x509-sign.js CHANGED
@@ -165,14 +165,9 @@ function _buildExtensions(extSpec, ctx) {
165
165
 
166
166
  // ---- serial + validity + key plumbing --------------------------------------
167
167
 
168
- // RFC 5280 sec. 4.1.2.5 -- UTCTime for 1950..2049, GeneralizedTime otherwise. UTCTime's two-digit year
169
- // only represents 1950..2049 (sec. 4.1.2.5.1), so a pre-1950 date MUST use GeneralizedTime (the same
170
- // arm as a date from 2050 on); using UTCTime for a pre-1950 year would be unrepresentable.
171
- function _timeDer(date, which) {
172
- guard.time.assertValid(date, _err, "x509/bad-input", "certificate " + which);
173
- var y = date.getUTCFullYear();
174
- return (y >= 1950 && y <= 2049) ? b.utcTime(date) : b.generalizedTime(date);
175
- }
168
+ // RFC 5280 sec. 4.1.2.5 UTCTime/GeneralizedTime cutover -- the shared builder primitive (pki-build), a
169
+ // thin wrapper here so the certificate validity label reads "certificate notBefore/notAfter".
170
+ function _timeDer(date, which) { return _b.timeDer(date, "certificate " + which); }
176
171
  // A supplied issuer certificate MUST be a CA that may sign certificates: basicConstraints present AND
177
172
  // critical AND cA=TRUE (RFC 5280 sec. 4.2.1.9), and -- when a keyUsage extension is present -- the
178
173
  // keyCertSign bit (sec. 4.2.1.3). Refuse a non-CA issuer rather than mint a certificate that will not
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
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:3787cd37-e9d0-465d-bed3-0f6822edb52c",
5
+ "serialNumber": "urn:uuid:4cfd38cc-6772-42e6-bed2-46e04c2a950e",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-17T08:27:56.310Z",
8
+ "timestamp": "2026-07-17T17:26:06.040Z",
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.2",
22
+ "bom-ref": "@blamejs/pki@0.3.4",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.3.2",
25
+ "version": "0.3.4",
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.2",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.3.4",
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.2",
57
+ "ref": "@blamejs/pki@0.3.4",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]