@blamejs/pki 0.2.14 → 0.2.16

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,28 @@ 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.2.16 — 2026-07-14
8
+
9
+ Post-quantum ML-DSA joins CMS SignedData: pki.cms.sign and pki.cms.verify now sign and verify with ML-DSA-44/65/87 (RFC 9882), freely mixed with the classical signers in one message.
10
+
11
+ ### Added
12
+
13
+ - pki.cms.sign and pki.cms.verify sign and verify a CMS SignedData with the post-quantum ML-DSA-44, ML-DSA-65, and ML-DSA-87 (RFC 9882): pure mode, empty context, AlgorithmIdentifier parameters absent, over attached or detached content and single or multiple signers -- freely mixed with RSA, RSASSA-PSS, ECDSA, and EdDSA signers in one message. The signer identifier is issuerAndSerialNumber or subjectKeyIdentifier, and the output is a DER Buffer or PEM.
14
+ - The CMS message-digest algorithm for an ML-DSA signer is held to the parameter set's security strength (RFC 9882 section 3.3): SHA-512 by default and SHAKE256 optional, with SHA-256 accepted only for ML-DSA-44. A below-strength digest is refused fail-closed on both signing and verification, so a weak message digest cannot cap the collision resistance of a strong ML-DSA signature.
15
+
16
+ ### Changed
17
+
18
+ - CMS signature verification now requires a one-shot signer (EdDSA or ML-DSA, where a single algorithm identifier names both the key and the signature) to present a signer certificate whose public-key algorithm matches the SignerInfo signatureAlgorithm; a disagreement fails closed with a typed error rather than surfacing an opaque import failure.
19
+
20
+ ## v0.2.15 — 2026-07-13
21
+
22
+ CMS SignedData signing arrives as pki.cms.sign, and RFC 3161 timestamp token creation as pki.tsp.sign -- the producing sides of the CMS and timestamp verifiers, emitting exactly what pki.cms.verify and OpenSSL cms -verify accept.
23
+
24
+ ### Added
25
+
26
+ - pki.cms.sign(content, signers, opts) produces a CMS SignedData (RFC 5652 section 5): attached or detached content, one or many signers, RSA (PKCS#1 v1.5 and, with opts.pss, RSASSA-PSS), ECDSA (P-256/384/521), Ed25519, and Ed448. Each signer is { cert, key } (a PEM/DER certificate and a WebCrypto CryptoKey or PKCS#8 key); the signed attributes (content-type, message-digest, signing-time, plus opts.additionalSignedAttributes) are canonical DER and the signature covers the exact section 5.4 preimage. opts selects detached content, the encapsulated content type, the signer identifier (issuerAndSerialNumber or subjectKeyIdentifier), certificate embedding, and DER or PEM output.
27
+ - pki.tsp.sign(messageImprint, tsa, opts) creates an RFC 3161 TimeStampToken over a message imprint ({ hashAlgorithm, hashedMessage }): a CMS SignedData whose content is a TSTInfo carrying the imprint, the TSA policy, a serial number, and genTime, with optional accuracy, nonce, and ordering, and the RFC 3161 section 2.4.2 / RFC 5816 signing-certificate (ESSCertIDv2) attribute binding the token to the TSA certificate. It composes pki.cms.sign, so any supported TSA key algorithm works.
28
+
7
29
  ## v0.2.14 — 2026-07-13
8
30
 
9
31
  CMS SignedData signature verification arrives as pki.cms.verify -- verifying a signed message (S/MIME, timestamps, code signing) over the exact RFC 5652 preimage, for attached and detached content, one or many signers, across RSA, RSASSA-PSS, ECDSA, and EdDSA.
package/README.md CHANGED
@@ -221,7 +221,8 @@ is callable today; nothing below is a stub.
221
221
  | `pki.schema.smime` | Decode S/MIME ESS signed-attribute values (RFC 5035 / RFC 8551) — `parseSigningCertificate` / `parseSigningCertificateV2` bind a signature to its signing certificate (cert hash, hash algorithm, issuer `GeneralNames` + serial), `parseSmimeCapabilities` decodes the ordered capability list, and `decodeAttribute` OID-dispatches a CMS attribute (enforcing the single-value rule, recognize-and-defer for unknown types). A companion decoder for CMS signed attributes, not an auto-routed format, fail-closed — `parseSigningCertificate`, `parseSigningCertificateV2`, `parseSmimeCapabilities`, `decodeAttribute` |
222
222
  | `pki.schema.engine` | The declarative ASN.1 structure-schema engine every format parser composes — `walk` / `encode` / `embeddedDer` plus the schema combinators |
223
223
  | `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` |
224
- | `pki.cms` | RFC 5652 §5 CMS SignedData signature verification — `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. Attached and detached content, single and multiple signers, RSA / RSASSA-PSS / ECDSA / EdDSA. 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`. Fail-closed with typed `cms/*` errors — `verify` |
224
+ | `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 and the post-quantum ML-DSA-44/65/87 per RFC 9882); 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`. Fail-closed with typed `cms/*` errors — `sign`, `verify` |
225
+ | `pki.tsp` | RFC 3161 timestamp token creation — `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. The producing side of `pki.schema.tsp.parseToken`; SHA-2 imprints, and any `pki.cms.sign` TSA key — `sign` |
225
226
  | `pki.ct` | RFC 6962 Certificate Transparency SCTs — `parseSctList` decodes the `SignedCertificateTimestampList` a certificate or OCSP response carries in the SCT extension (a TLS-presentation-language payload inside the §3.3 double DER wrap) into per-SCT log id, exact `timestamp` (BigInt), named signature algorithm, and raw signature; `reconstructSignedData` rebuilds the exact `digitally-signed` preimage; `verifySct` verifies an SCT signature against a log's public key by reconstructing the signed data, routing an ECDSA signature through the strict DER-conformance gate, and verifying through the crypto engine — resolving true or false, and throwing a typed error on a structural fault. Structure decoded, crypto fail-closed — `parseSctList`, `reconstructSignedData`, `verifySct` |
226
227
  | `pki.merkle` | RFC 6962 / RFC 9162 Merkle-tree proof verification — `leafHash` / `nodeHash` / `emptyRootHash` build the domain-separated (0x00 leaf / 0x01 node) SHA-256 tree hashes; `verifyInclusion` folds an audit proof back to a root and `verifyConsistency` reconstructs both the old and new root (the append-only guarantee), each constant-time-compared to a trusted checkpoint root. Fail-closed on bad geometry, sync hashing, transport-free — `leafHash`, `nodeHash`, `emptyRootHash`, `verifyInclusion`, `verifyConsistency` |
227
228
  | `pki.trust` | Mozilla / CCADB trust-store ingestion — `parseCertdata` reads the NSS `certdata.txt` object stream and `parseCcadbCsv` the CCADB CSV export into one identical constraint-carrying anchor shape: the per-purpose trust bits (only `CKT_NSS_TRUSTED_DELEGATOR` grants) and the per-purpose distrust-after dates the bare root list omits. Certificate and trust objects pair by byte-exact issuer + serial (never adjacency) and are cross-checked against the parsed DER, so metadata can never attach to the wrong root; `anchor()` hands an entry to `pki.path.validate({ trustAnchor, checkPurpose })`. Offline, fail-closed, bounded — `parseCertdata`, `parseCcadbCsv`, `anchor` |
@@ -233,12 +234,12 @@ is callable today; nothing below is a stub.
233
234
  | `pki.lint` | Certificate linting — the zlint / pkilint of JavaScript. `certificate(pem \| der \| parsed, opts)` walks a parsed certificate and emits graded, advisory findings — each with a stable id, a severity (`fatal` > `error` > `warn` > `notice`), a source, a spec-clause citation, and a message — against the RFC 5280 profile plus a representative CA/Browser Forum TLS BR subset (serial sign/size, validity ordering + the the SC081v3 reducing validity schedule, keyCertSign coherence, unknown critical extensions, empty-subject SAN, SKI/AKI presence, SAN required + CN-in-SAN, dNSName syntax, serverAuth EKU, weak keys). Unlike every other entry the DATA path never throws: hostile bytes return a `fatal` `lint/unparseable` finding (with the strict parser's code) so a whole directory lints without a try/catch; only config-time misuse throws a typed `LintError`. `certificate`, `rules`, `profiles` |
234
235
  | `pki.C` / `pki.constants` | Version-stable constants — functional scale helpers (`C.TIME.*`, `C.BYTES.*`), codec `LIMITS`, `version` |
235
236
  | `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError` / `Asn1Error` / `OidError` / `PemError` / `CertificateError` / `CrlError` / `CsrError` / `Pkcs8Error` / `CmsError` / `OcspError` / `TspError` / `AttrCertError` / `CrmfError` / `Pkcs12Error` / `CmpError` / `PathError` / `CtError` / `JoseError` / `AcmeError` / `WebauthnError` / `LintError`, each carrying a stable `code` in `domain/reason` form |
236
- | `pki` CLI | `pki version`, `pki oid <dotted\|name>`, `pki parse <cert>`, `pki inspect <cert>`, `pki lint <cert>`, `pki convert <file> --to der\|pem`, `pki verify <cert>... --anchor <cert>` |
237
+ | `pki` CLI | `pki version`, `pki oid <dotted\|name>`, `pki parse <cert>`, `pki inspect <cert>`, `pki lint <cert>`, `pki convert <file> --to der\|pem`, `pki verify <cert>... --anchor <cert>`, `pki sign <file> --cert <c> --key <k>` |
237
238
 
238
239
  ### CLI
239
240
 
240
241
  ```sh
241
- pki version # @blamejs/pki v0.1.0
242
+ pki version # the installed @blamejs/pki version
242
243
  pki oid 1.2.840.113549.1.1.11 # sha256WithRSAEncryption
243
244
  pki oid sha256 # 2.16.840.1.101.3.4.2.1
244
245
  pki parse cert.pem # structured JSON summary of a certificate
@@ -247,12 +248,15 @@ pki lint cert.pem # graded conformance findings; exit 1 o
247
248
  pki lint cert.pem --json --profile cabf-tls
248
249
  pki convert cert.pem --to der > cert.der # transcode between PEM and DER (round-trips)
249
250
  pki verify leaf.pem --anchor root.pem --time 2026-01-01T00:00:00Z # RFC 5280 path validation
251
+ pki sign msg.txt --cert signer.pem --key signer-key.pem --out msg.p7s # CMS SignedData (pki.cms.sign)
252
+ pki sign msg.txt --cert signer.pem --key signer-key.pem --detached --pem # detached, PEM to stdout
250
253
  ```
251
254
 
252
- `inspect`/`lint`/`convert`/`verify` are thin front-ends over `pki.inspect`, `pki.lint`, the
253
- per-format PEM codecs, and `pki.path.validate` — the CLI never does anything the library
254
- API can't. `lint` exits non-zero when any `error`/`fatal` finding is present; `verify` exits
255
- non-zero when the path does not validate.
255
+ `inspect`/`lint`/`convert`/`verify`/`sign` are thin front-ends over `pki.inspect`, `pki.lint`, the
256
+ per-format PEM codecs, `pki.path.validate`, and `pki.cms.sign` — the CLI never does anything the
257
+ library API can't. `lint` exits non-zero when any `error`/`fatal` finding is present; `verify`
258
+ exits non-zero when the path does not validate; `sign` reads a PKCS#8 DER/PEM private key and its
259
+ certificate and writes a DER (or `--pem`) SignedData to `--out` or stdout.
256
260
 
257
261
  ### What's coming
258
262
 
@@ -338,7 +342,8 @@ path validation), `pki.trust` (trust anchors), `pki.ct` (Certificate Transparenc
338
342
  SCTs), `pki.hpke` (RFC 9180), `pki.shbs` (HSS/LMS stateful hash signatures),
339
343
  `pki.merkle` (RFC 9162 transparency proofs), `pki.sigstore` (offline npm-provenance
340
344
  verification), `pki.webauthn` (WebAuthn / passkey attestation verification),
341
- `pki.cms` (RFC 5652 SignedData signature verification), and the
345
+ `pki.cms` (RFC 5652 SignedData signing + signature verification), `pki.tsp` (RFC 3161
346
+ timestamp token creation), and the
342
347
  `jose` / `acme` / `est` enrollment surfaces. Each composes
343
348
  the shared structure, foundation, and crypto layers directly. Alongside the schema
344
349
  engine, the fail-closed **guard family** (`guard-*`) centralizes each CVE-class
package/bin/pki.js CHANGED
@@ -15,11 +15,13 @@
15
15
  * [--label LABEL]
16
16
  * pki verify <cert>... --anchor <cert> validate an ordered certification path (anchor->target)
17
17
  * [--time ISO]
18
+ * pki sign <file> --cert <c> --key <k> produce a CMS SignedData over the file (pki.cms.sign)
19
+ * [--detached] [--pss] [--digest D] [--pem] [--out F]
18
20
  *
19
21
  * The CLI is a thin operator convenience over the library surface: it validates its
20
22
  * arguments (entry-point tier — bad input exits non-zero with a message) and never does
21
- * anything the public API cannot. inspect / lint / convert / verify compose pki.inspect,
22
- * pki.lint, the per-format PEM codecs, and pki.path.validate respectively.
23
+ * anything the public API cannot. inspect / lint / convert / verify / sign compose pki.inspect,
24
+ * pki.lint, the per-format PEM codecs, pki.path.validate, and pki.cms.sign respectively.
23
25
  */
24
26
 
25
27
  var fs = require("node:fs");
@@ -34,7 +36,7 @@ function fail(msg) {
34
36
  // rest are positionals in `_`. A value-taking flag whose value is absent or is itself another
35
37
  // flag is a usage error (never silently coerced to `true`, which would make e.g. `--time`
36
38
  // parse as `new Date(true)` = a real 1970 timestamp). No clustering, no `=value`.
37
- var VALUE_FLAGS = { to: 1, profile: 1, severity: 1, label: 1, anchor: 1, time: 1 };
39
+ var VALUE_FLAGS = { to: 1, profile: 1, severity: 1, label: 1, anchor: 1, time: 1, cert: 1, key: 1, digest: 1, out: 1 };
38
40
  function parseArgs(argv) {
39
41
  var out = { _: [] };
40
42
  for (var i = 0; i < argv.length; i++) {
@@ -200,7 +202,28 @@ function cmdVerify(args) {
200
202
  }, function (e) { return fail(e.code + ": " + e.message); });
201
203
  }
202
204
 
203
- var USAGE = "usage: pki <version|oid|parse|inspect|lint|convert|verify> [args]\n";
205
+ // pki sign <content-file> --cert <cert> --key <key> -- produce a CMS SignedData over the file
206
+ // via pki.cms.sign. The signer key is a PKCS#8 DER or PEM private key; the certificate is DER or
207
+ // PEM. Output is a DER Buffer (or a PEM block with --pem) to --out or stdout.
208
+ function cmdSign(args) {
209
+ var contentFile = args._[0];
210
+ if (!contentFile || !args.cert || !args.key) {
211
+ return fail("usage: pki sign <content-file> --cert <cert> --key <key.pkcs8> [--detached] [--pss] [--digest sha256|sha384|sha512] [--pem] [--out <file>]");
212
+ }
213
+ var content = readFileBytes(contentFile);
214
+ var signer = { cert: _asPemOrDer(readFileBytes(args.cert)), key: _asPemOrDer(readFileBytes(args.key)) };
215
+ if (args.pss) signer.pss = true;
216
+ if (args.digest) signer.digestAlgorithm = args.digest;
217
+ return pki.cms.sign(content, signer, { detached: !!args.detached, pem: !!args.pem }).then(function (out) {
218
+ if (args.out) { try { fs.writeFileSync(args.out, out); } catch (e) { return fail("cannot write " + args.out + ": " + e.message); } }
219
+ else { process.stdout.write(out); } // process.exitCode (0) lets Node flush a piped stdout
220
+ }, function (e) { return fail((e.code || "cms/sign-error") + ": " + e.message); });
221
+ }
222
+ // A PEM file (its first byte is '-') is passed to pki.cms.sign as a string; a DER file as its
223
+ // Buffer. cms.sign accepts either for the certificate and needs a string for a PEM private key.
224
+ function _asPemOrDer(buf) { return buf[0] === 0x2d ? buf.toString("latin1") : buf; }
225
+
226
+ var USAGE = "usage: pki <version|oid|parse|inspect|lint|convert|verify|sign> [args]\n";
204
227
 
205
228
  function main(argv) {
206
229
  var cmd = argv[0];
@@ -212,6 +235,7 @@ function main(argv) {
212
235
  case "lint": return cmdLint(parseArgs(argv.slice(1)));
213
236
  case "convert": return cmdConvert(parseArgs(argv.slice(1)));
214
237
  case "verify": return cmdVerify(parseArgs(argv.slice(1)));
238
+ case "sign": return cmdSign(parseArgs(argv.slice(1)));
215
239
  case undefined: case "help": case "--help": case "-h":
216
240
  process.stdout.write(USAGE);
217
241
  return;
package/index.js CHANGED
@@ -37,6 +37,7 @@ var schema = require("./lib/schema-all");
37
37
  var path = require("./lib/path-validate");
38
38
  var ct = require("./lib/ct");
39
39
  var cms = require("./lib/cms-verify");
40
+ var tsp = require("./lib/tsp-sign");
40
41
  var merkle = require("./lib/merkle");
41
42
  var shbs = require("./lib/shbs");
42
43
  var hpke = require("./lib/hpke");
@@ -75,6 +76,7 @@ module.exports = {
75
76
  // surfaced raw for external verification (pki.ct.reconstructSignedData).
76
77
  ct: ct,
77
78
  cms: cms,
79
+ tsp: tsp,
78
80
  // `merkle` is the RFC 6962 / RFC 9162 Merkle-tree proof-verification core --
79
81
  // pki.merkle.leafHash / nodeHash / emptyRootHash build the domain-separated
80
82
  // (0x00 leaf / 0x01 node) SHA-256 tree hashes; pki.merkle.verifyInclusion and
@@ -0,0 +1,363 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ //
5
+ // @internal -- the pki.cms.sign implementation. So the pki.cms namespace has ONE @module home,
6
+ // the operator-facing @module pki.cms + the @primitive pki.cms.sign documentation block live in
7
+ // cms-verify.js, which re-exports this sign function.
8
+ //
9
+ // CMS SignedData signing (RFC 5652 sec. 5), the producing side of pki.cms.verify: composes the
10
+ // strict asn1 build layer (canonical DER, and build.set SET-OF-sorts the signed attributes for
11
+ // free), the WebCrypto sign surface over node:crypto, and the shared validator.sig.rawToEcdsaDer
12
+ // DER-ECDSA home -- emitting exactly the shapes cms.verify checks (NULL params for RSA, absent
13
+ // for ECDSA/EdDSA, the RSASSA-PSS params SEQUENCE), with the sign->verify round-trip (and OpenSSL
14
+ // cms -verify) as the guard.
15
+
16
+ var nodeCrypto = require("crypto");
17
+ var asn1 = require("./asn1-der");
18
+ var oid = require("./oid");
19
+ var x509 = require("./schema-x509");
20
+ var pkcs8 = require("./schema-pkcs8");
21
+ var pkix = require("./schema-pkix");
22
+ var webcrypto = require("./webcrypto");
23
+ var subtle = webcrypto.webcrypto.subtle;
24
+ var validator = require("./validator-all");
25
+ var frameworkError = require("./framework-error");
26
+
27
+ var CmsError = frameworkError.CmsError;
28
+ var b = asn1.build;
29
+ function _err(code, message, cause) { return new CmsError(code, message, cause); }
30
+ function O(name) { return oid.byName(name); }
31
+
32
+ // A digest-algorithm name -> the WebCrypto hash (sign path). SHAKE256 has no WebCrypto hash, so
33
+ // the message digest is always computed with node:crypto (below), uniform across the family.
34
+ var HASH = { sha256: "SHA-256", sha384: "SHA-384", sha512: "SHA-512" };
35
+ var NODE_DIGEST = { sha256: "sha256", sha384: "sha384", sha512: "sha512", shake256: "shake256" };
36
+ var SHAKE_OUT = { shake256: 64 }; // RFC 8419 sec. 2.3 -- Ed448 uses SHAKE256 with 512-bit output
37
+ var PSS_SALT = { "SHA-256": 32, "SHA-384": 48, "SHA-512": 64 };
38
+ var ECDSA_ALG = { sha256: "ecdsaWithSHA256", sha384: "ecdsaWithSHA384", sha512: "ecdsaWithSHA512" };
39
+ var HASH_NAME_BY_OID = {};
40
+ HASH_NAME_BY_OID[O("sha256")] = "sha256";
41
+ HASH_NAME_BY_OID[O("sha384")] = "sha384";
42
+ HASH_NAME_BY_OID[O("sha512")] = "sha512";
43
+ var EC_BY_CURVE_OID = {};
44
+ EC_BY_CURVE_OID[O("prime256v1")] = { curve: "P-256", coordLen: 32 };
45
+ EC_BY_CURVE_OID[O("secp384r1")] = { curve: "P-384", coordLen: 48 };
46
+ EC_BY_CURVE_OID[O("secp521r1")] = { curve: "P-521", coordLen: 66 };
47
+ // ML-DSA (RFC 9882): the signer cert SPKI algorithm OID -> its WebCrypto name. Registry-keyed
48
+ // (Hard rule #2, no dotted-decimal literal). The same OID identifies both the key and the signature.
49
+ var MLDSA_BY_OID = {};
50
+ MLDSA_BY_OID[O("id-ml-dsa-44")] = "ML-DSA-44";
51
+ MLDSA_BY_OID[O("id-ml-dsa-65")] = "ML-DSA-65";
52
+ MLDSA_BY_OID[O("id-ml-dsa-87")] = "ML-DSA-87";
53
+ // The message-digest algorithms suitable for each ML-DSA parameter set (RFC 9882 sec. 3.3 /
54
+ // Table 1): a digest MUST offer at least the parameter set's collision strength (lambda) and at
55
+ // least 2*lambda bits of output. SHA-512 is suitable for all sets; SHAKE256 (512-bit output) too.
56
+ // Below-strength digests downgrade the signature to the weaker of the two (the whichever-is-lower
57
+ // rule), so they are refused fail-closed on sign and verify -- the RFC's "verifiers MAY reject".
58
+ var MLDSA_SUITABLE_DIGEST = {
59
+ "ML-DSA-44": { sha256: 1, sha384: 1, sha512: 1, shake256: 1 },
60
+ "ML-DSA-65": { sha384: 1, sha512: 1, shake256: 1 },
61
+ "ML-DSA-87": { sha512: 1, shake256: 1 },
62
+ };
63
+
64
+ var OID_DATA = O("data");
65
+ var OID_SIGNED_DATA = O("signedData");
66
+ var OID_SKI = O("subjectKeyIdentifier");
67
+
68
+ // An AlgorithmIdentifier { OID } (absent parameters) or { OID, NULL }.
69
+ function _algId(name, shape) { return shape === "null" ? b.sequence([b.oid(O(name)), b.nullValue()]) : b.sequence([b.oid(O(name))]); }
70
+ // The RSASSA-PSS AlgorithmIdentifier with the params SEQUENCE cms.verify's _resolvePss accepts:
71
+ // an explicit SHA-2 hashAlgorithm, MGF1 keyed to the same hash, the hash-length saltLength, and
72
+ // the default trailerField (omitted). RFC 4055.
73
+ function _pssAlgId(digestName) {
74
+ var hashAlg = b.sequence([b.oid(O(digestName)), b.nullValue()]);
75
+ var mgf = b.sequence([b.oid(O("mgf1")), hashAlg]);
76
+ var params = b.sequence([b.explicit(0, hashAlg), b.explicit(1, mgf), b.explicit(2, b.integer(BigInt(PSS_SALT[HASH[digestName]])))]);
77
+ return b.sequence([b.oid(O("rsassaPss")), params]);
78
+ }
79
+
80
+ // An id-RSASSA-PSS SubjectPublicKeyInfo MAY pin the permitted hash in its RSASSA-PSS-params
81
+ // (RFC 4055 sec. 1.2 / sec. 3.1): a key generated for SHA-384 cannot sign under SHA-256. Read the
82
+ // pinned digest name from the SPKI algorithm parameters so signing honors the key's restriction
83
+ // instead of hard-coding SHA-256. Absent params (an unrestricted key) or an unrecognized hash
84
+ // returns null -- the caller then falls back to its opts.digestAlgorithm or the SHA-256 default.
85
+ function _pssHashFromSpki(cert) {
86
+ var params = cert.subjectPublicKeyInfo.algorithm.parameters;
87
+ if (params == null) return null; // an unrestricted id-RSASSA-PSS key
88
+ // params is the raw parameters TLV the strict codec already validated when it parsed the cert,
89
+ // so re-decoding a single valid TLV cannot throw. Read the pinned hash structurally, defaulting
90
+ // to null (caller falls back) whenever the shape is not the expected RSASSA-PSS-params.
91
+ var node = asn1.decode(params);
92
+ if (node.tagClass !== "universal" || node.tagNumber !== asn1.TAGS.SEQUENCE || !node.children) return null;
93
+ var hashField = node.children.filter(function (c) { return c.tagClass === "context" && c.tagNumber === 0; })[0];
94
+ if (!hashField || !hashField.children || !hashField.children[0] || !hashField.children[0].children) return null;
95
+ var oidNode = hashField.children[0].children[0]; // [0] EXPLICIT AlgorithmIdentifier { hash OID, ... }
96
+ if (!oidNode || oidNode.tagClass !== "universal" || oidNode.tagNumber !== asn1.TAGS.OBJECT_IDENTIFIER) return null;
97
+ var pinnedOid = asn1.read.oid(oidNode);
98
+ var name = HASH_NAME_BY_OID[pinnedOid];
99
+ // A recognized SHA-2 pin resolves to its name; an unsupported pinned hash (e.g. SHA-3) fails
100
+ // closed -- we cannot honor the key's restriction, so we never silently sign under a different
101
+ // digest the key forbids.
102
+ if (!name) throw _err("cms/unsupported-algorithm", "the id-RSASSA-PSS signer key pins an unsupported hash algorithm (" + pinnedOid + ")");
103
+ return name;
104
+ }
105
+
106
+ // The message digest of the content under the digest algorithm (SHA-2 or SHAKE256).
107
+ function _digest(digestName, content) {
108
+ var h = SHAKE_OUT[digestName]
109
+ ? nodeCrypto.createHash(NODE_DIGEST[digestName], { outputLength: SHAKE_OUT[digestName] })
110
+ : nodeCrypto.createHash(NODE_DIGEST[digestName]);
111
+ return h.update(content).digest();
112
+ }
113
+
114
+ // Resolve the sign scheme from the signer certificate's public-key algorithm + per-signer opts:
115
+ // the digest, the digestAlgorithm and signatureAlgorithm AlgorithmIdentifiers (with the exact
116
+ // parameter shape cms.verify requires), and the WebCrypto import + sign algorithms.
117
+ function _scheme(cert, so, noSignedAttrs) {
118
+ var alg = cert.subjectPublicKeyInfo.algorithm;
119
+ var keyOid = alg.oid;
120
+ if (keyOid === O("rsaEncryption") || keyOid === O("rsassaPss")) {
121
+ var isPssKey = keyOid === O("rsassaPss");
122
+ // An id-RSASSA-PSS key MAY pin its permitted hash in the SPKI params (RFC 4055 sec. 1.2): honor
123
+ // that over the SHA-256 default so Node/OpenSSL does not reject the otherwise-valid signer with
124
+ // ERR_OSSL_DIGEST_NOT_ALLOWED. An explicit digestAlgorithm that contradicts the pin is rejected
125
+ // fail-closed rather than silently signed under a digest the key forbids.
126
+ var pinned = isPssKey ? _pssHashFromSpki(cert) : null;
127
+ if (pinned && so.digestAlgorithm && so.digestAlgorithm !== pinned) throw _err("cms/bad-input", "the signer key restricts the RSASSA-PSS digest to " + pinned + ", but digestAlgorithm " + JSON.stringify(so.digestAlgorithm) + " was requested");
128
+ var d = so.digestAlgorithm || pinned || "sha256";
129
+ if (!HASH[d]) throw _err("cms/unsupported-algorithm", "unsupported RSA digest algorithm " + JSON.stringify(d));
130
+ // A general rsaEncryption key signs PKCS#1 v1.5 by default, or RSASSA-PSS when opts.pss is set.
131
+ if (so.pss || isPssKey) return { digest: d, digestAlgId: _algId(d, "absent"), sigAlgId: _pssAlgId(d), imp: { name: "RSA-PSS", hash: HASH[d] }, sign: { name: "RSA-PSS", saltLength: PSS_SALT[HASH[d]] }, ecdsaDer: false };
132
+ return { digest: d, digestAlgId: _algId(d, "absent"), sigAlgId: _algId("rsaEncryption", "null"), imp: { name: "RSASSA-PKCS1-v1_5", hash: HASH[d] }, sign: { name: "RSASSA-PKCS1-v1_5" }, ecdsaDer: false };
133
+ }
134
+ if (keyOid === O("ecPublicKey")) {
135
+ var curveOid;
136
+ try { curveOid = asn1.read.oid(asn1.decode(alg.parameters)); }
137
+ catch (e) { throw _err("cms/unsupported-algorithm", "the signer EC key parameters are not a named-curve OID", e); }
138
+ var ec = EC_BY_CURVE_OID[curveOid];
139
+ if (!ec) throw _err("cms/unsupported-algorithm", "the signer key is on an unsupported EC curve");
140
+ var de = so.digestAlgorithm || "sha256";
141
+ if (!HASH[de]) throw _err("cms/unsupported-algorithm", "unsupported ECDSA digest algorithm " + JSON.stringify(de));
142
+ return { digest: de, digestAlgId: _algId(de, "absent"), sigAlgId: _algId(ECDSA_ALG[de], "absent"), imp: { name: "ECDSA", namedCurve: ec.curve }, sign: { name: "ECDSA", hash: HASH[de] }, ecdsaDer: true, coordLen: ec.coordLen };
143
+ }
144
+ if (keyOid === O("Ed25519") || keyOid === O("Ed448")) {
145
+ var name = keyOid === O("Ed25519") ? "Ed25519" : "Ed448";
146
+ var dd = so.digestAlgorithm || (name === "Ed25519" ? "sha512" : "shake256");
147
+ if (!NODE_DIGEST[dd]) throw _err("cms/unsupported-algorithm", "unsupported " + name + " digest algorithm " + JSON.stringify(dd));
148
+ return { digest: dd, digestAlgId: _algId(dd, "absent"), sigAlgId: _algId(name, "absent"), imp: { name: name }, sign: { name: name }, ecdsaDer: false };
149
+ }
150
+ if (MLDSA_BY_OID[keyOid]) {
151
+ // ML-DSA (RFC 9882): a one-shot post-quantum signature, pure mode, empty context. The signature
152
+ // covers the same sec. 5.4 preimage as every other scheme; digestAlgorithm names the message-
153
+ // digest algorithm (SHA-512 by default -- the sec. 3.3 MUST / interop default), which must be
154
+ // suitable for the parameter set (Q1 -- enforced on sign, refusing a below-strength digest).
155
+ var mlName = MLDSA_BY_OID[keyOid];
156
+ // RFC 9882 sec. 3.3: without signed attributes the digestAlgorithm has no meaning, but the
157
+ // signer MUST still emit SHA-512 to avoid an interoperability failure -- so force it (ignoring
158
+ // any caller digestAlgorithm) rather than carry a value a strict peer would reject. With signed
159
+ // attributes the digest must be suitable for the parameter set (Q1 -- enforced on sign).
160
+ var md;
161
+ if (noSignedAttrs) {
162
+ md = "sha512";
163
+ } else {
164
+ md = so.digestAlgorithm || "sha512";
165
+ if (!NODE_DIGEST[md]) throw _err("cms/unsupported-algorithm", "unsupported ML-DSA message digest " + JSON.stringify(md));
166
+ if (!MLDSA_SUITABLE_DIGEST[mlName][md]) throw _err("cms/unsupported-algorithm", "the " + md + " message digest is below the security strength of " + mlName + " (RFC 9882 sec. 3.3)");
167
+ }
168
+ return { digest: md, digestAlgId: _algId(md, "absent"), sigAlgId: _algId(oid.name(keyOid), "absent"), imp: { name: mlName }, sign: { name: mlName }, ecdsaDer: false };
169
+ }
170
+ throw _err("cms/unsupported-algorithm", "unsupported signer key algorithm " + keyOid);
171
+ }
172
+
173
+ // The raw issuer Name TLV from a parsed certificate (byte-identical to the cert, so the sid the
174
+ // verifier canonically compares matches exactly). The issuer is the Name after the optional
175
+ // version [0] and the serial + signature AlgorithmIdentifier in the tbsCertificate.
176
+ function _issuerBytes(cert) {
177
+ var tbs = asn1.decode(cert.tbsBytes);
178
+ var hasVersion = tbs.children[0].tagClass === "context" && tbs.children[0].tagNumber === 0;
179
+ return tbs.children[hasVersion ? 3 : 2].bytes;
180
+ }
181
+ // The cert's subjectKeyIdentifier extension value (the raw key id), or throws.
182
+ function _skiValue(cert) {
183
+ var ext = (cert.extensions || []).filter(function (e) { return e.oid === OID_SKI; })[0];
184
+ if (!ext) throw _err("cms/no-ski", "a subjectKeyIdentifier signer identifier requires the signer certificate to carry an SKI extension");
185
+ try { return asn1.read.octetString(asn1.decode(ext.value)); }
186
+ catch (e) { throw _err("cms/no-ski", "the signer certificate's subjectKeyIdentifier extension value is not an OCTET STRING", e); }
187
+ }
188
+
189
+ // A caller-supplied CryptoKey carries its OWN algorithm (name, and for RSA the baked-in hash,
190
+ // for ECDSA the curve). A PKCS#8 key is imported under the resolved scheme, so it always agrees;
191
+ // but a pre-imported CryptoKey whose algorithm disagrees with the certificate's scheme would
192
+ // silently sign under a different hash than the digestAlgorithm advertises -- an inconsistent,
193
+ // non-verifiable CMS. Reject the mismatch fail-closed.
194
+ function _assertKeyMatchesScheme(key, imp) {
195
+ var ka = key.algorithm || {};
196
+ if (ka.name !== imp.name) throw _err("cms/bad-input", "the signer CryptoKey algorithm (" + ka.name + ") does not match the certificate's key algorithm (" + imp.name + ")");
197
+ if (imp.hash && (!ka.hash || ka.hash.name !== imp.hash)) throw _err("cms/bad-input", "the signer CryptoKey hash (" + (ka.hash && ka.hash.name) + ") does not match the signing digest (" + imp.hash + ")");
198
+ if (imp.namedCurve && ka.namedCurve !== imp.namedCurve) throw _err("cms/bad-input", "the signer CryptoKey curve (" + ka.namedCurve + ") does not match the certificate curve (" + imp.namedCurve + ")");
199
+ }
200
+ // Import the signer private key: a CryptoKey passed through, or a PKCS#8 DER Buffer / PEM string.
201
+ function _importKey(key, imp) {
202
+ if (key && typeof key === "object" && !Buffer.isBuffer(key) && !(key instanceof Uint8Array) && key.type === "private") {
203
+ _assertKeyMatchesScheme(key, imp);
204
+ return Promise.resolve(key);
205
+ }
206
+ var der;
207
+ if (Buffer.isBuffer(key)) der = key;
208
+ else if (key instanceof Uint8Array) der = Buffer.from(key);
209
+ else if (typeof key === "string") { try { der = pkcs8.pemDecode(key); } catch (e) { throw _err("cms/bad-input", "the signer PEM private key could not be decoded", e); } }
210
+ else throw _err("cms/bad-input", "a signer key must be a CryptoKey, a PKCS#8 DER Buffer, or a PKCS#8 PEM string");
211
+ return subtle.importKey("pkcs8", der, imp, false, ["sign"]);
212
+ }
213
+
214
+ // Build one SignerInfo (RFC 5652 sec. 5.3) and sign it. Resolves the SignerInfo (as a build
215
+ // node) plus its digestAlgorithm AlgorithmIdentifier and the signer certificate DER (for the
216
+ // SignedData digestAlgorithms + certificates sets).
217
+ function _buildSignerInfo(signer, content, eContentType, opts) {
218
+ var so = signer || {};
219
+ var certDer = _normCertDer(so.cert);
220
+ var cert = x509.parse(certDer);
221
+ var scheme = _scheme(cert, so, opts.signedAttributes === false);
222
+ var useSki = opts.sid === "ski";
223
+ var sid = useSki
224
+ ? b.contextPrimitive(0, _skiValue(cert)) // [0] IMPLICIT SubjectKeyIdentifier
225
+ : b.sequence([b.raw(_issuerBytes(cert)), b.integer(cert.serialNumber)]); // IssuerAndSerialNumber
226
+ var version = useSki ? 3 : 1;
227
+
228
+ return _importKey(so.key, scheme.imp).then(function (priv) {
229
+ return Promise.resolve().then(function () {
230
+ if (opts.signedAttributes === false) return content; // sign the content directly (no signed attributes)
231
+ // Signed attributes (RFC 5652 sec. 5.3): content-type == eContentType, message-digest ==
232
+ // digest(content), and (by default) signing-time. build.set canonical-DER SET-OF-sorts them.
233
+ // Each attribute type appears AT MOST ONCE across the whole set (RFC 5652 sec. 5.3);
234
+ // `seenTypes` catches a caller-supplied attribute that duplicates a built-in or another.
235
+ var seenTypes = {};
236
+ function _pushAttr(typeOid, values) {
237
+ if (seenTypes[typeOid]) throw _err("cms/bad-input", "signedAttrs must not repeat an attribute type (RFC 5652 sec. 5.3): " + typeOid);
238
+ seenTypes[typeOid] = 1;
239
+ attrs.push(b.sequence([b.oid(typeOid), b.set(values)]));
240
+ }
241
+ var attrs = [];
242
+ _pushAttr(O("contentType"), [b.oid(eContentType)]);
243
+ _pushAttr(O("messageDigest"), [b.octetString(_digest(scheme.digest, content))]);
244
+ if (opts.signingTime !== false) _pushAttr(O("signingTime"), [_timeValue(opts.signingTime)]);
245
+ // Caller-supplied signed attributes (e.g. an RFC 3161 signing-certificate attribute): each
246
+ // { type: <OID name or dotted string>, values: [<DER value Buffer>] }. build.set sorts them in.
247
+ (opts.additionalSignedAttributes || []).forEach(function (a) {
248
+ var vals = (a.values || []).map(function (v) { return _toBuf(v, "a signed attribute value"); });
249
+ if (!vals.length) throw _err("cms/bad-input", "a signed attribute must carry at least one value (RFC 5652 -- Attribute values is SET SIZE (1..MAX))");
250
+ _pushAttr(/^\d+(\.\d+)+$/.test(a.type) ? a.type : O(a.type), vals);
251
+ });
252
+ var setOf = b.set(attrs); // SET OF (tag 0x31) -- the exact bytes the signature covers (sec. 5.4)
253
+ var wire = Buffer.from(setOf); wire[0] = 0xA0; // the on-wire [0] IMPLICIT tag
254
+ return { setOf: setOf, wire: wire };
255
+ }).then(function (toSign) {
256
+ var signedBytes = toSign.setOf ? toSign.setOf : toSign; // SET-OF form for signing (sec. 5.4)
257
+ return subtle.sign(scheme.sign, priv, signedBytes).then(function (sigRaw) {
258
+ var sig = Buffer.from(sigRaw);
259
+ if (scheme.ecdsaDer) sig = validator.sig.rawToEcdsaDer(sig, scheme.coordLen);
260
+ var fields = [b.integer(BigInt(version)), sid, scheme.digestAlgId];
261
+ if (toSign.wire) fields.push(toSign.wire); // [0] IMPLICIT signedAttrs
262
+ fields.push(scheme.sigAlgId, b.octetString(sig));
263
+ return { si: b.sequence(fields), digestAlgId: scheme.digestAlgId, version: version, certDer: certDer };
264
+ });
265
+ });
266
+ });
267
+ }
268
+
269
+ // A signing-time Time value: UTCTime before 2050, GeneralizedTime from 2050 (RFC 5652 sec. 11.3 /
270
+ // RFC 5280 sec. 4.1.2.5). A caller Date overrides; false omits the attribute (handled above).
271
+ function _timeValue(when) {
272
+ var d = (when instanceof Date) ? when : new Date();
273
+ return d.getUTCFullYear() < 2050 ? b.utcTime(d) : b.generalizedTime(d);
274
+ }
275
+
276
+ // Normalize a signer certificate input to its raw DER (DER Buffer / PEM string / Uint8Array).
277
+ // The same bytes drive scheme resolution and the certificates [0] embedding, so a parsed
278
+ // certificate (which does not retain its full DER) is rejected -- pass DER or PEM.
279
+ function _normCertDer(c) {
280
+ if (c == null) throw _err("cms/bad-input", "each signer requires a certificate (cert)");
281
+ if (c instanceof Uint8Array && !Buffer.isBuffer(c)) c = Buffer.from(c); // a Uint8Array -> Buffer (below)
282
+ if (Buffer.isBuffer(c)) return c[0] === 0x30 ? c : _pemToDer(c.toString("latin1")); // DER as-is, else PEM
283
+ if (typeof c === "string") return _pemToDer(c);
284
+ throw _err("cms/bad-input", "a signer certificate must be a DER Buffer or a PEM string");
285
+ }
286
+ function _pemToDer(text) {
287
+ var m = text.match(/-----BEGIN CERTIFICATE-----([\s\S]*?)-----END CERTIFICATE-----/);
288
+ if (!m) throw _err("cms/bad-input", "a signer certificate PEM is not a CERTIFICATE block");
289
+ return Buffer.from(m[1].replace(/[^A-Za-z0-9+/=]/g, ""), "base64");
290
+ }
291
+
292
+ // pki.cms.sign -- documented by the @primitive block in cms-verify.js (the @module pki.cms home).
293
+ function sign(content, signers, opts) {
294
+ opts = opts || {};
295
+ if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw _err("cms/bad-input", "pki.cms.sign options must be an object");
296
+ var contentBuf = _toBuf(content, "content");
297
+ var list = Array.isArray(signers) ? signers : [signers];
298
+ if (!list.length) throw _err("cms/bad-input", "pki.cms.sign requires at least one signer");
299
+ var eContentType = opts.eContentType ? O(opts.eContentType) : OID_DATA;
300
+ // RFC 5652 sec. 5.3: signed attributes MUST be present (carrying a content-type attribute)
301
+ // whenever the encapsulated content type is not id-data -- so signedAttributes:false is only
302
+ // valid for id-data content. Refusing it here keeps cms.sign from emitting a non-conformant
303
+ // SignedData (e.g. a timestamp token, id-ct-TSTInfo, with no signed attributes).
304
+ if (opts.signedAttributes === false && eContentType !== OID_DATA) {
305
+ throw _err("cms/bad-input", "signed attributes are required when eContentType is not id-data (RFC 5652 sec. 5.3)");
306
+ }
307
+ // A supplied signing-time MUST be a valid Date (or false to omit the attribute) -- never a
308
+ // silently-ignored non-Date or an Invalid Date that would encode a garbage Time.
309
+ if (opts.signingTime != null && opts.signingTime !== false && (!(opts.signingTime instanceof Date) || isNaN(opts.signingTime.getTime()))) {
310
+ throw _err("cms/bad-input", "signingTime must be a valid Date, or false to omit it");
311
+ }
312
+
313
+ return Promise.all(list.map(function (s) { return _buildSignerInfo(s, contentBuf, eContentType, opts); })).then(function (built) {
314
+ // digestAlgorithms: the distinct SignerInfo digestAlgorithm AlgorithmIdentifiers, deduped.
315
+ var seen = {}, digestAlgs = [];
316
+ built.forEach(function (x) { var k = x.digestAlgId.toString("hex"); if (!seen[k]) { seen[k] = 1; digestAlgs.push(x.digestAlgId); } });
317
+ // CMSVersion (RFC 5652 sec. 5.1): 3 if any SignerInfo is v3 (ski) or eContentType != id-data;
318
+ // otherwise 1 (v1 emits only X.509 certificates, so the v4/v5 attribute-certificate cases
319
+ // do not arise).
320
+ var v3 = built.some(function (x) { return x.version === 3; }) || eContentType !== OID_DATA;
321
+ var version = v3 ? 3 : 1;
322
+ // EncapsulatedContentInfo: eContentType + [0] EXPLICIT eContent (omitted when detached).
323
+ var encapFields = [b.oid(eContentType)];
324
+ if (!opts.detached) encapFields.push(b.explicit(0, b.octetString(contentBuf)));
325
+ var encap = b.sequence(encapFields);
326
+ // certificates [0] IMPLICIT SET OF (the signer certs), deduped + SET-OF-ordered, when embedded.
327
+ var sdFields = [b.integer(BigInt(version)), b.set(digestAlgs), encap];
328
+ if (opts.certificates !== false) {
329
+ var certDers = _dedupe(built.map(function (x) { return x.certDer; })).sort(Buffer.compare); // X.690 sec. 11.6
330
+ sdFields.push(b.contextConstructed(0, Buffer.concat(certDers))); // [0] IMPLICIT SET OF
331
+ }
332
+ sdFields.push(b.set(built.map(function (x) { return x.si; }))); // signerInfos SET OF
333
+ var signedData = b.sequence(sdFields);
334
+ var contentInfo = b.sequence([b.oid(OID_SIGNED_DATA), b.explicit(0, signedData)]); // ContentInfo
335
+ return opts.pem ? pkix.pemEncode(contentInfo, "CMS", frameworkError.PemError) : contentInfo;
336
+ });
337
+ }
338
+
339
+ // Dedupe certificate DERs (two signers may share a cert -- embed it once).
340
+ function _dedupe(ders) {
341
+ var seen = {}, out = [];
342
+ ders.forEach(function (d) { var k = d.toString("hex"); if (!seen[k]) { seen[k] = 1; out.push(d); } });
343
+ return out;
344
+ }
345
+
346
+ function _toBuf(v, what) {
347
+ if (Buffer.isBuffer(v)) return v;
348
+ if (v instanceof Uint8Array) return Buffer.from(v);
349
+ throw _err("cms/bad-input", what + " must be a Buffer");
350
+ }
351
+
352
+ // Coverage residual -- three defensive branches are unreachable through the shipped path:
353
+ // * `_skiValue`'s `cert.extensions || []` fallback -- x509.parse always surfaces `extensions`
354
+ // as an array (empty when absent), so the `|| []` never fires.
355
+ // * `_assertKeyMatchesScheme`'s `key.algorithm || {}` -- a WebCrypto CryptoKey always carries
356
+ // an `algorithm`, so the `|| {}` fallback never fires.
357
+ // * `_assertKeyMatchesScheme`'s `!ka.hash` guard -- an `imp.hash` is set only for an RSA
358
+ // scheme, which requires `ka.name` to already equal the RSA name (else the earlier name
359
+ // check throws); an RSA CryptoKey always carries a `hash`, so `!ka.hash` never fires.
360
+ // MLDSA_SUITABLE_DIGEST is shared with cms-verify (which requires this module) so the per-
361
+ // parameter-set digest-strength policy has ONE home -- a digest accepted on sign is exactly the
362
+ // set accepted on verify, and neither side can drift from the other (RFC 9882 sec. 3.3).
363
+ module.exports = { sign: sign, MLDSA_SUITABLE_DIGEST: MLDSA_SUITABLE_DIGEST };
package/lib/cms-verify.js CHANGED
@@ -13,13 +13,16 @@
13
13
  * confirms the message-digest attribute equals the digest of the content and verifies the
14
14
  * signature over the DER re-encoding of the SignedAttributes (the on-wire `[0]` tag replaced
15
15
  * by a universal SET OF); otherwise it verifies directly over the content. Attached and
16
- * detached content, single and multiple signers, and RSA / RSASSA-PSS / ECDSA / EdDSA are
17
- * covered. It reports a per-signer verdict; it does NOT chain the signer certificate to a
18
- * trust anchor -- that is the caller's step through `pki.path.validate`.
16
+ * detached content, single and multiple signers, and RSA / RSASSA-PSS / ECDSA / EdDSA and the
17
+ * post-quantum ML-DSA (ML-DSA-44/65/87, RFC 9882) are covered. It reports a per-signer verdict;
18
+ * it does NOT chain the signer certificate to a trust anchor -- that is the caller's step through
19
+ * `pki.path.validate`.
19
20
  * @spec RFC 5652
21
+ * @spec RFC 9882
20
22
  * @card Verify a CMS SignedData signature (S/MIME, timestamps, code signing).
21
23
  */
22
24
 
25
+ var nodeCrypto = require("crypto");
23
26
  var asn1 = require("./asn1-der");
24
27
  var oid = require("./oid");
25
28
  var x509 = require("./schema-x509");
@@ -27,6 +30,8 @@ var cms = require("./schema-cms");
27
30
  var webcrypto = require("./webcrypto");
28
31
  var subtle = webcrypto.webcrypto.subtle;
29
32
  var edwardsPoint = require("./edwards-point");
33
+ var cmsSign = require("./cms-sign");
34
+ var MLDSA_SUITABLE_DIGEST = cmsSign.MLDSA_SUITABLE_DIGEST; // shared sign/verify digest-strength policy (RFC 9882 sec. 3.3)
30
35
  var validator = require("./validator-all");
31
36
  var guard = require("./guard-all");
32
37
  var frameworkError = require("./framework-error");
@@ -39,6 +44,16 @@ var OID_CONTENT_TYPE = oid.byName("contentType");
39
44
 
40
45
  // A digest-algorithm name -> the WebCrypto hash.
41
46
  var DIGEST_HASH = { sha1: "SHA-1", sha256: "SHA-256", sha384: "SHA-384", sha512: "SHA-512" };
47
+ // SHAKE256 (RFC 8419 sec. 2.3, the Ed448 message digest) has no WebCrypto hash, so it is computed
48
+ // with node:crypto; its 512-bit (64-byte) output length is fixed by the profile.
49
+ var SHAKE_OUT = { shake256: 64 };
50
+ // Is `name` a message-digest algorithm this verifier supports (SHA-2 family or SHAKE256)?
51
+ function _supportedDigest(name) { return !!(DIGEST_HASH[name] || SHAKE_OUT[name]); }
52
+ // The digest of `content` under the named algorithm, resolved to a Buffer.
53
+ function _computeDigest(name, content) {
54
+ if (SHAKE_OUT[name]) return Promise.resolve(nodeCrypto.createHash(name, { outputLength: SHAKE_OUT[name] }).update(content).digest());
55
+ return subtle.digest(DIGEST_HASH[name], content).then(function (d) { return Buffer.from(d); });
56
+ }
42
57
  // A signatureAlgorithm name -> its verify scheme. A combined OID (sha256WithRSAEncryption,
43
58
  // ecdsaWithSHA256) carries its own hash; a bare key OID (rsaEncryption, ecPublicKey) takes the
44
59
  // hash from the SignerInfo digestAlgorithm.
@@ -49,8 +64,14 @@ var SIG_SCHEME = {
49
64
  rsaEncryption: { kind: "rsa", params: "null" },
50
65
  rsassaPss: { kind: "rsapss" },
51
66
  ecPublicKey: { kind: "ec", params: "absent" },
52
- Ed25519: { kind: "eddsa", name: "Ed25519", params: "absent" },
53
- Ed448: { kind: "eddsa", name: "Ed448", params: "absent" },
67
+ // One-shot families (EdDSA, ML-DSA): the same OID identifies the key and the signature, so the
68
+ // signer cert SPKI algorithm OID MUST equal the signatureAlgorithm OID -- `sameKeyOid` enables
69
+ // that agreement check (RFC 8410 / RFC 9882; enforced in _verifyAgainstCandidates).
70
+ Ed25519: { kind: "eddsa", name: "Ed25519", params: "absent", sameKeyOid: true },
71
+ Ed448: { kind: "eddsa", name: "Ed448", params: "absent", sameKeyOid: true },
72
+ "id-ml-dsa-44": { kind: "mldsa", name: "ML-DSA-44", params: "absent", sameKeyOid: true },
73
+ "id-ml-dsa-65": { kind: "mldsa", name: "ML-DSA-65", params: "absent", sameKeyOid: true },
74
+ "id-ml-dsa-87": { kind: "mldsa", name: "ML-DSA-87", params: "absent", sameKeyOid: true },
54
75
  sha1WithRSAEncryption: { kind: "rsa", hash: "SHA-1", params: "null" },
55
76
  sha256WithRSAEncryption: { kind: "rsa", hash: "SHA-256", params: "null" },
56
77
  sha384WithRSAEncryption: { kind: "rsa", hash: "SHA-384", params: "null" },
@@ -99,7 +120,7 @@ function _findSignerCerts(sid, parsedCerts) {
99
120
  // through to the next -- so a colliding certificate cannot make a valid signature read invalid.
100
121
  // When no candidate verifies, a plain false is a code-less verdict; the last structural fault's
101
122
  // code is surfaced if one occurred (so the diagnostic is not lost).
102
- function _verifyAgainstCandidates(scheme, sigHash, sigBytes, signedBytes, sid, candidates, pssSalt) {
123
+ function _verifyAgainstCandidates(scheme, sigHash, sigBytes, signedBytes, sid, candidates, pssSalt, expectedKeyOid) {
103
124
  var lastErr = null;
104
125
  function attempt(idx) {
105
126
  if (idx >= candidates.length) {
@@ -107,6 +128,13 @@ function _verifyAgainstCandidates(scheme, sigHash, sigBytes, signedBytes, sid, c
107
128
  : { ok: false, sid: sid, cert: candidates[0].der };
108
129
  }
109
130
  var c = candidates[idx];
131
+ // One-shot family (EdDSA/ML-DSA): the signer cert public-key algorithm OID MUST equal the
132
+ // SignerInfo signatureAlgorithm OID. A candidate whose SPKI disagrees is skipped with a precise
133
+ // verdict, rather than a foreign webcrypto/data throw from importing under the wrong name.
134
+ if (expectedKeyOid && c.cert.subjectPublicKeyInfo.algorithm.oid !== expectedKeyOid) {
135
+ lastErr = _err("cms/unsupported-algorithm", "the signer certificate public-key algorithm does not match the SignerInfo signatureAlgorithm");
136
+ return attempt(idx + 1);
137
+ }
110
138
  return Promise.resolve()
111
139
  .then(function () { return _verifySignature(scheme, sigHash, sigBytes, c.cert.subjectPublicKeyInfo.bytes, signedBytes, _certCurveOid(c.cert), pssSalt); })
112
140
  .then(function (ok) { return ok === true ? { ok: true, sid: sid, cert: c.der } : attempt(idx + 1); },
@@ -213,6 +241,13 @@ function _verifySignature(scheme, hashName, sigBytes, spki, signedBytes, curveOi
213
241
  return subtle.importKey("spki", spki, { name: "ECDSA", namedCurve: ec.curve }, false, ["verify"])
214
242
  .then(function (k) { return subtle.verify({ name: "ECDSA", hash: hashName }, k, raw, signedBytes); });
215
243
  }
244
+ if (scheme.kind === "mldsa") {
245
+ // ML-DSA (RFC 9882): a one-shot post-quantum verify, pure mode, empty context -- no external
246
+ // signature hash and no Edwards-point validation (FIPS 204 sec. 5.3; node structurally
247
+ // validates the SPKI on import). The signature is raw and fixed-length (not ECDSA-DER).
248
+ return subtle.importKey("spki", spki, { name: scheme.name }, false, ["verify"])
249
+ .then(function (k) { return subtle.verify({ name: scheme.name }, k, sigBytes, signedBytes); });
250
+ }
216
251
  // EdDSA -- the WebCrypto name follows the signing key's SPKI OID (Ed25519 / Ed448). node/
217
252
  // OpenSSL imports any Ed25519/Ed448 SPKI without validating the point, and a low-order (e.g.
218
253
  // all-zeroes) key verifies a forged signature -- so reject a non-full-order point first.
@@ -276,15 +311,29 @@ function _verifyOne(si, content, eContentType, parsedCerts) {
276
311
  if (scheme.params && !_algParamsOk(scheme.params, si.signatureAlgorithm.parameters)) return Promise.resolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "the " + si.signatureAlgorithm.name + " signature algorithm parameters must be " + (scheme.params === "null" ? "DER NULL (RFC 4055)" : "absent (RFC 5758/8410)") });
277
312
  // The digestAlgorithm parameters, when present, MUST be a DER NULL (RFC 5754 sec. 2 accepts
278
313
  // absent OR NULL; a present non-NULL is malformed and, being outside the signed preimage,
279
- // must fail closed like the signatureAlgorithm parameters above).
314
+ // must fail closed like the signatureAlgorithm parameters above). EXCEPT for an ML-DSA
315
+ // SignerInfo WITHOUT signed attributes: RFC 9882 sec. 3.3 says the whole digestAlgorithm field
316
+ // has no meaning there and MUST be ignored, so neither its parameters nor its name may reject.
280
317
  var dp = si.digestAlgorithm.parameters;
281
- if (dp !== null && dp !== undefined && !_isDerNull(dp)) return Promise.resolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "the " + si.digestAlgorithm.name + " digest algorithm parameters must be absent or DER NULL (RFC 5754 sec. 2)" });
318
+ var mldsaNoAttrs = scheme.kind === "mldsa" && !si.signedAttrsBytes;
319
+ if (!mldsaNoAttrs && dp !== null && dp !== undefined && !_isDerNull(dp)) return Promise.resolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "the " + si.digestAlgorithm.name + " digest algorithm parameters must be absent or DER NULL (RFC 5754 sec. 2)" });
320
+ // The SHAKE256 digest algorithm identifier carries NO parameters at all -- not even a DER NULL
321
+ // (RFC 8702 sec. 3.1) -- so for an ML-DSA SignerInfo with signed attributes a present shake256
322
+ // parameter fails closed. The SHA-2 ML-DSA digests (id-sha256/384/512) follow RFC 5754, which
323
+ // requires a verifier to accept absent OR NULL (the generic rule above); RFC 9882's omit-on-encode
324
+ // rule binds the signer, not the verifier, so a SHA-2 NULL is not tightened here.
325
+ if (scheme.kind === "mldsa" && si.signedAttrsBytes && si.digestAlgorithm.name === "shake256" && dp !== null && dp !== undefined) return Promise.resolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "the SHAKE256 digestAlgorithm parameters must be absent (RFC 8702 sec. 3.1)" });
282
326
  var sigHash = pss ? pss.hash : (scheme.hash || digestHash);
283
327
  // The signature hash is required for every non-EdDSA scheme (EdDSA hashes internally);
284
328
  // the content digest is required whenever signed attributes are present, for every scheme
285
329
  // (the message-digest attribute is computed under digestAlgorithm). Either gap is a
286
330
  // fail-closed unsupported-algorithm verdict, never a foreign-domain throw from the digest.
287
- if ((scheme.kind !== "eddsa" && !sigHash) || (si.signedAttrsBytes && !digestHash)) return Promise.resolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "unsupported digest algorithm " + JSON.stringify(si.digestAlgorithm.name) });
331
+ if ((scheme.kind !== "eddsa" && scheme.kind !== "mldsa" && !sigHash) || (si.signedAttrsBytes && !_supportedDigest(si.digestAlgorithm.name))) return Promise.resolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "unsupported digest algorithm " + JSON.stringify(si.digestAlgorithm.name) });
332
+ // ML-DSA (RFC 9882 sec. 3.3): with signed attributes present, the message-digest algorithm MUST
333
+ // meet the parameter set's security strength -- a below-strength digest is the weaker link and is
334
+ // rejected fail-closed (the RFC's "verifiers MAY reject", taken by the strict-verifier posture).
335
+ // With signed attributes absent the digestAlgorithm has no meaning (sec. 3.3) and is not checked.
336
+ if (scheme.kind === "mldsa" && si.signedAttrsBytes && !MLDSA_SUITABLE_DIGEST[scheme.name][si.digestAlgorithm.name]) return Promise.resolve({ ok: false, code: "cms/unsupported-algorithm", sid: si.sid, message: "the " + si.digestAlgorithm.name + " message digest is below the security strength of " + scheme.name + " (RFC 9882 sec. 3.3)" });
288
337
  var signers = _findSignerCerts(si.sid, parsedCerts);
289
338
  if (!signers.length) return Promise.resolve({ ok: false, code: "cms/signer-cert-not-found", sid: si.sid, message: "no certificate matches this SignerInfo's signer identifier" });
290
339
  var sigBytes = _toBuf(si.signature, "the SignerInfo signature");
@@ -319,13 +368,13 @@ function _verifyOne(si, content, eContentType, parsedCerts) {
319
368
  try {
320
369
  declared = asn1.read.octetString(mdAttr.values[0]);
321
370
  } catch (e) { throw _err("cms/bad-signed-attrs", "the message-digest attribute value is not an OCTET STRING", e); }
322
- return subtle.digest(digestHash, content).then(function (d) {
323
- if (!Buffer.from(d).equals(declared)) return { mismatch: { code: "cms/message-digest-mismatch", message: "the message-digest attribute does not match the content digest" } };
371
+ return _computeDigest(si.digestAlgorithm.name, content).then(function (d) {
372
+ if (!d.equals(declared)) return { mismatch: { code: "cms/message-digest-mismatch", message: "the message-digest attribute does not match the content digest" } };
324
373
  return reTagged;
325
374
  });
326
375
  }).then(function (signedBytes) {
327
376
  if (signedBytes && signedBytes.mismatch) return { ok: false, code: signedBytes.mismatch.code, sid: si.sid, cert: signers[0].der, message: signedBytes.mismatch.message };
328
- return _verifyAgainstCandidates(scheme, sigHash, sigBytes, signedBytes, si.sid, signers, pss ? pss.saltLength : 0);
377
+ return _verifyAgainstCandidates(scheme, sigHash, sigBytes, signedBytes, si.sid, signers, pss ? pss.saltLength : 0, scheme.sameKeyOid ? oid.byName(si.signatureAlgorithm.name) : null);
329
378
  });
330
379
  }
331
380
 
@@ -335,6 +384,7 @@ function _verifyOne(si, content, eContentType, parsedCerts) {
335
384
  * @since 0.2.14
336
385
  * @status experimental
337
386
  * @spec RFC 5652
387
+ * @spec RFC 9882
338
388
  * @defends cms-signature-forgery (CWE-347)
339
389
  * @related pki.schema.cms.parse, pki.path.validate
340
390
  *
@@ -342,7 +392,8 @@ function _verifyOne(si, content, eContentType, parsedCerts) {
342
392
  * `Buffer`, or a parsed `pki.schema.cms` object. Returns `{ valid, signers }` where each
343
393
  * `signers[i]` is `{ ok, sid, cert }` (`cert` the matched signer certificate DER) or carries
344
394
  * a `code` on a structural failure; `valid` is true when there is at least one signer and
345
- * every signer verified.
395
+ * every signer verified. RSA (PKCS#1 v1.5 and RSASSA-PSS), ECDSA, EdDSA, and the post-quantum
396
+ * ML-DSA (ML-DSA-44/65/87, RFC 9882 -- pure mode, empty context) signatures are recognized.
346
397
  *
347
398
  * @opts content The detached content (a `Buffer`) when the SignedData carries no
348
399
  * encapsulated eContent. Required for a detached signature.
@@ -401,4 +452,40 @@ function _addCert(out, der) {
401
452
  // is always an array (or it throws), so this belt-and-suspenders throw never fires.
402
453
  // * `c && c.bytes ? c.bytes : c` -- schema-cms surfaces every embedded certificate as an
403
454
  // object carrying a `bytes` Buffer, so the raw-value fallback never fires.
404
- module.exports = { verify: verify };
455
+ /**
456
+ * @primitive pki.cms.sign
457
+ * @signature pki.cms.sign(content, signers, opts?) -> Promise<Buffer|string>
458
+ * @since 0.2.15
459
+ * @status experimental
460
+ * @spec RFC 5652
461
+ * @spec RFC 9882
462
+ * @related pki.cms.verify, pki.schema.cms.parse
463
+ *
464
+ * Produce a CMS SignedData (RFC 5652 sec. 5) over `content` (a `Buffer`) -- the structure
465
+ * S/MIME signed mail, RFC 3161 timestamp tokens, and code signing rest on, and exactly what
466
+ * `pki.cms.verify` consumes and OpenSSL `cms -verify` validates. Each `signers[i]` is
467
+ * `{ cert, key, digestAlgorithm?, pss? }`: `cert` the signer certificate (PEM or DER), `key`
468
+ * its private key (a WebCrypto `CryptoKey` or a PKCS#8 DER `Buffer` / PEM string). The
469
+ * signature covers the RFC 5652 sec. 5.4 preimage: with signed attributes (the default) the
470
+ * message-digest attribute is bound to the content digest and the signature is over the
471
+ * canonical DER SET OF SignedAttributes; otherwise over the content directly. RSA (PKCS#1 v1.5
472
+ * and, with `pss`, RSASSA-PSS), ECDSA (P-256/384/521), Ed25519, Ed448, and the post-quantum ML-DSA
473
+ * (ML-DSA-44/65/87, RFC 9882 -- pure mode; SHA-512 message digest by default, SHAKE256 optional)
474
+ * are covered.
475
+ *
476
+ * @opts detached Omit the encapsulated content (a detached signature; the verifier
477
+ * supplies the content). Default false.
478
+ * @opts eContentType The encapsulated content type (an OID name). Default `data`.
479
+ * @opts signedAttributes Include signed attributes (content-type, message-digest, signing-time);
480
+ * false signs the content directly. Default true.
481
+ * @opts signingTime A `Date` for the signing-time attribute, or false to omit it.
482
+ * @opts sid `"issuerAndSerial"` (default) or `"ski"` (subjectKeyIdentifier).
483
+ * @opts certificates Embed the signer certificates in the output. Default true.
484
+ * @opts pem Return a PEM string (`-----BEGIN CMS-----`) instead of a DER Buffer.
485
+ * @example
486
+ * var p7 = await pki.cms.sign(Buffer.from("hello"), { cert: signerCertDer, key: signerKeyPkcs8 });
487
+ * var res = await pki.cms.verify(p7); // res.valid === true
488
+ */
489
+ var sign = cmsSign.sign;
490
+
491
+ module.exports = { verify: verify, sign: sign };
@@ -0,0 +1,161 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ /**
5
+ * @module pki.tsp
6
+ * @nav Signing
7
+ * @title Timestamps
8
+ * @intro Create an RFC 3161 timestamp token. A TimeStampToken IS a CMS SignedData whose
9
+ * encapsulated content is a `TSTInfo` (the timestamped message imprint + trusted time), so
10
+ * `sign(messageImprint, tsa, opts)` builds the `TSTInfo`, attaches the RFC 3161 sec. 2.4.2
11
+ * signing-certificate attribute that binds the token to the TSA certificate, and signs it
12
+ * through `pki.cms.sign`. It is the producing side of `pki.schema.tsp.parseToken`.
13
+ * @spec RFC 3161
14
+ * @card Create an RFC 3161 timestamp token (a CMS SignedData over a TSTInfo).
15
+ */
16
+
17
+ var nodeCrypto = require("crypto");
18
+ var asn1 = require("./asn1-der");
19
+ var oid = require("./oid");
20
+ var cmsSign = require("./cms-sign");
21
+ var frameworkError = require("./framework-error");
22
+
23
+ var TspError = frameworkError.TspError;
24
+ var b = asn1.build;
25
+ function _err(code, message, cause) { return new TspError(code, message, cause); }
26
+ function O(name) { return oid.byName(name); }
27
+
28
+ // Digest names whose imprint / certHash this producer supports (the SHA-2 family).
29
+ var NODE_DIGEST = { sha256: "sha256", sha384: "sha384", sha512: "sha512" };
30
+ // The message-imprint hash MUST be exactly the digest algorithm's output length (RFC 3161 sec.
31
+ // 2.4.1 -- hashedMessage is "the hash of the datum to be time-stamped").
32
+ var HASH_LEN = { sha256: 32, sha384: 48, sha512: 64 };
33
+
34
+ // A hash AlgorithmIdentifier { OID, NULL } -- messageImprint and ESSCertIDv2 hash algorithms
35
+ // carry an explicit NULL parameter (the form RFC 3161 / RFC 5035 producers emit).
36
+ function _hashAlgId(name) {
37
+ if (!NODE_DIGEST[name]) throw _err("tsp/unsupported-algorithm", "unsupported hash algorithm " + JSON.stringify(name));
38
+ return b.sequence([b.oid(O(name)), b.nullValue()]);
39
+ }
40
+ // A policy identifier: an OID name or a dotted-decimal string.
41
+ function _policy(p) {
42
+ if (typeof p !== "string") throw _err("tsp/bad-input", "the timestamp policy must be an OID name or dotted string");
43
+ return /^\d+(\.\d+)+$/.test(p) ? b.oid(p) : b.oid(O(p));
44
+ }
45
+ // The signer certificate DER (DER Buffer / PEM string / Uint8Array) -- for the ESSCertIDv2 hash.
46
+ function _certDer(c) {
47
+ if (c == null) throw _err("tsp/bad-input", "the TSA signer requires a certificate (cert)");
48
+ if (c instanceof Uint8Array && !Buffer.isBuffer(c)) c = Buffer.from(c); // a Uint8Array -> Buffer
49
+ if (Buffer.isBuffer(c)) { if (c[0] === 0x30) return c; c = c.toString("latin1"); } // DER as-is, else decode as PEM
50
+ if (typeof c !== "string") throw _err("tsp/bad-input", "the TSA certificate must be a DER Buffer or a PEM string");
51
+ var m = c.match(/-----BEGIN CERTIFICATE-----([\s\S]*?)-----END CERTIFICATE-----/);
52
+ if (!m) throw _err("tsp/bad-input", "the TSA certificate PEM is not a CERTIFICATE block");
53
+ return Buffer.from(m[1].replace(/[^A-Za-z0-9+/=]/g, ""), "base64");
54
+ }
55
+
56
+ // The RFC 5035 SigningCertificateV2 signed-attribute value binding the token to the TSA cert:
57
+ // SigningCertificateV2 ::= SEQUENCE { certs SEQUENCE OF ESSCertIDv2 }, ESSCertIDv2 ::= SEQUENCE {
58
+ // hashAlgorithm DEFAULT sha256, certHash OCTET STRING, issuerSerial OPTIONAL }. The default
59
+ // sha256 hashAlgorithm is omitted; certHash is the digest of the certificate.
60
+ function _signingCertV2(certDer, hashName) {
61
+ var certHash = nodeCrypto.createHash(NODE_DIGEST[hashName]).update(certDer).digest();
62
+ var essCertId = hashName === "sha256"
63
+ ? b.sequence([b.octetString(certHash)]) // hashAlgorithm DEFAULT sha256 omitted
64
+ : b.sequence([_hashAlgId(hashName), b.octetString(certHash)]);
65
+ return b.sequence([b.sequence([essCertId])]);
66
+ }
67
+
68
+ /**
69
+ * @primitive pki.tsp.sign
70
+ * @signature pki.tsp.sign(messageImprint, tsa, opts) -> Promise<Buffer|string>
71
+ * @since 0.2.15
72
+ * @status experimental
73
+ * @spec RFC 3161
74
+ * @related pki.schema.tsp.parseToken, pki.cms.sign
75
+ *
76
+ * Create an RFC 3161 TimeStampToken over `messageImprint` (`{ hashAlgorithm, hashedMessage }`
77
+ * -- the hash of the data being timestamped, computed by the requester). `tsa` is the
78
+ * timestamp authority's `{ cert, key }` (as `pki.cms.sign` takes them). The token is a CMS
79
+ * SignedData whose content is a `TSTInfo` carrying the imprint, the TSA policy, a serial
80
+ * number, and `genTime`; the RFC 3161 sec. 2.4.2 signing-certificate attribute binding the
81
+ * token to the TSA certificate is attached automatically.
82
+ *
83
+ * @opts policy REQUIRED -- the TSA policy identifier (an OID name or dotted string).
84
+ * @opts serialNumber REQUIRED -- a unique token serial number (a number or BigInt).
85
+ * @opts genTime The trusted time (a `Date`). Default: now.
86
+ * @opts nonce The request nonce to echo (a number or BigInt), for replay protection.
87
+ * @opts accuracy `{ seconds?, millis?, micros? }` -- the genTime +/- accuracy.
88
+ * @opts ordering Whether tokens from this TSA are strictly ordered in time (boolean).
89
+ * @opts certHashAlgorithm The ESSCertIDv2 hash algorithm name. Default `sha256`.
90
+ * @opts sid / pem Passed through to `pki.cms.sign` (signer identifier, PEM output).
91
+ * @example
92
+ * var imprint = { hashAlgorithm: "sha256", hashedMessage: sha256Digest };
93
+ * var token = await pki.tsp.sign(imprint, { cert: signerCertDer, key: signerKeyPkcs8 }, { policy: "1.3.6.1.4.1.1", serialNumber: 1 });
94
+ * (await pki.cms.verify(token)).valid; // true
95
+ */
96
+ function sign(messageImprint, tsa, opts) {
97
+ opts = opts || {};
98
+ if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw _err("tsp/bad-input", "pki.tsp.sign options must be an object");
99
+ var mi = messageImprint || {};
100
+ if (!mi.hashAlgorithm || !NODE_DIGEST[mi.hashAlgorithm]) throw _err("tsp/unsupported-algorithm", "messageImprint.hashAlgorithm must be a supported hash name");
101
+ if (!Buffer.isBuffer(mi.hashedMessage) && !(mi.hashedMessage instanceof Uint8Array)) throw _err("tsp/bad-input", "messageImprint.hashedMessage must be a Buffer");
102
+ if (mi.hashedMessage.length !== HASH_LEN[mi.hashAlgorithm]) throw _err("tsp/bad-input", "messageImprint.hashedMessage length (" + mi.hashedMessage.length + ") does not match the " + mi.hashAlgorithm + " digest length (" + HASH_LEN[mi.hashAlgorithm] + ")");
103
+ if (!opts.policy) throw _err("tsp/bad-input", "a timestamp token requires a policy identifier (opts.policy)");
104
+ if (opts.serialNumber == null) throw _err("tsp/bad-input", "a timestamp token requires a serialNumber (opts.serialNumber)");
105
+
106
+ var certDer = _certDer(tsa && tsa.cert);
107
+ var certHashAlg = opts.certHashAlgorithm || "sha256";
108
+ if (!NODE_DIGEST[certHashAlg]) throw _err("tsp/unsupported-algorithm", "unsupported certHashAlgorithm " + JSON.stringify(certHashAlg));
109
+
110
+ // TSTInfo (RFC 3161 sec. 2.4.2): version(1), policy, messageImprint, serialNumber, genTime,
111
+ // then the OPTIONAL accuracy / ordering / nonce in ascending order.
112
+ var imprint = b.sequence([_hashAlgId(mi.hashAlgorithm), b.octetString(Buffer.from(mi.hashedMessage))]);
113
+ // genTime defaults to now; a supplied value MUST be a valid Date (never a silently-ignored
114
+ // non-Date or an Invalid Date that would encode a garbage GeneralizedTime).
115
+ if (opts.genTime != null && (!(opts.genTime instanceof Date) || isNaN(opts.genTime.getTime()))) {
116
+ throw _err("tsp/bad-input", "genTime must be a valid Date");
117
+ }
118
+ var genTime = opts.genTime instanceof Date ? opts.genTime : new Date();
119
+ var fields = [b.integer(1n), _policy(opts.policy), imprint, b.integer(BigInt(opts.serialNumber)), b.generalizedTime(genTime)];
120
+ if (opts.accuracy) fields.push(_accuracy(opts.accuracy));
121
+ if (opts.ordering === true) fields.push(b.boolean(true));
122
+ if (opts.nonce != null) fields.push(b.integer(BigInt(opts.nonce)));
123
+ var tstInfo = b.sequence(fields);
124
+
125
+ var signCert = { type: "signingCertificateV2", values: [_signingCertV2(certDer, certHashAlg)] };
126
+ var extra = [signCert].concat(opts.additionalSignedAttributes || []);
127
+ var signer = { cert: certDer, key: tsa && tsa.key, digestAlgorithm: opts.digestAlgorithm, pss: opts.pss };
128
+ return cmsSign.sign(tstInfo, signer, {
129
+ eContentType: "tSTInfo",
130
+ additionalSignedAttributes: extra,
131
+ sid: opts.sid,
132
+ pem: opts.pem,
133
+ });
134
+ }
135
+
136
+ // Accuracy ::= SEQUENCE { seconds INTEGER OPTIONAL, millis [0] INTEGER (1..999) OPTIONAL,
137
+ // micros [1] INTEGER (1..999) OPTIONAL } (RFC 3161 sec. 2.4.2). seconds is a non-negative
138
+ // integer; millis and micros MUST each be in 1..999 -- enforced before encoding.
139
+ function _accuracy(a) {
140
+ var f = [];
141
+ if (a.seconds != null) {
142
+ var s = Number(a.seconds);
143
+ if (!Number.isInteger(s) || s < 0 || s > 0x7fffffff) throw _err("tsp/bad-input", "Accuracy seconds must be a non-negative integer");
144
+ f.push(b.integer(BigInt(s)));
145
+ }
146
+ if (a.millis != null) f.push(b.contextPrimitive(0, _subMilliBytes(a.millis, "millis")));
147
+ if (a.micros != null) f.push(b.contextPrimitive(1, _subMilliBytes(a.micros, "micros")));
148
+ return b.sequence(f);
149
+ }
150
+ // The content octets of an Accuracy millis/micros field: an INTEGER in 1..999 (RFC 3161 sec.
151
+ // 2.4.2), without the universal INTEGER tag (the field is IMPLICIT [0]/[1]).
152
+ function _subMilliBytes(n, label) {
153
+ var v = Number(n);
154
+ if (!Number.isInteger(v) || v < 1 || v > 999) throw _err("tsp/bad-input", "Accuracy " + label + " must be an integer in 1..999 (RFC 3161 sec. 2.4.2)");
155
+ return b.integer(BigInt(v)).subarray(2); // strip the universal INTEGER tag+length
156
+ }
157
+
158
+ // Coverage residual -- `_hashAlgId`'s unsupported-hash throw is unreachable through the shipped
159
+ // path: both callers (the messageImprint hash and the ESSCertIDv2 certHashAlgorithm) validate
160
+ // the name against NODE_DIGEST before `_hashAlgId` runs, so the guard is belt-and-suspenders.
161
+ module.exports = { sign: sign };
@@ -54,4 +54,21 @@ function ecdsaSigToRaw(der, coordLen, E, code) {
54
54
  return Buffer.concat([coord(node.children[0], "r"), coord(node.children[1], "s")]);
55
55
  }
56
56
 
57
- module.exports = { ecdsaSigToRaw: ecdsaSigToRaw };
57
+ // rawToEcdsaDer(raw, coordLen) -> the canonical DER ECDSA-Sig-Value SEQUENCE { r, s } for a raw
58
+ // r||s signature (IEEE P1363, the WebCrypto sign() output). The inverse of ecdsaSigToRaw: a
59
+ // signer composes this so the emitted ECDSA signature is canonical DER (minimally-encoded
60
+ // INTEGERs via the build layer), never a hand-built SEQUENCE that could re-introduce a
61
+ // non-minimal encoding. A config-time TypeError guards a mis-sized raw signature at entry.
62
+ // @enforced-by behavioral -- a DER ECDSA-Sig-Value BUILD has no rename-proof code shape distinct
63
+ // from generic sequence([integer,integer]); the round-trip vectors (build -> ecdsaSigToRaw
64
+ // identity) and the cms.sign ECDSA KATs are the guard.
65
+ function rawToEcdsaDer(raw, coordLen) {
66
+ if (!Buffer.isBuffer(raw) || typeof coordLen !== "number" || coordLen <= 0 || raw.length !== coordLen * 2) {
67
+ throw new TypeError("rawToEcdsaDer: raw signature must be a Buffer of exactly 2*coordLen bytes");
68
+ }
69
+ var r = BigInt("0x" + raw.subarray(0, coordLen).toString("hex"));
70
+ var s = BigInt("0x" + raw.subarray(coordLen).toString("hex"));
71
+ return asn1.build.sequence([asn1.build.integer(r), asn1.build.integer(s)]);
72
+ }
73
+
74
+ module.exports = { ecdsaSigToRaw: ecdsaSigToRaw, rawToEcdsaDer: rawToEcdsaDer };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.2.14",
3
+ "version": "0.2.16",
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:7f3139fc-e726-4373-afdf-0e304562ee70",
5
+ "serialNumber": "urn:uuid:171a9606-6760-469e-abba-719538a2f2ab",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-13T21:48:02.712Z",
8
+ "timestamp": "2026-07-14T02:42:50.555Z",
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.2.14",
22
+ "bom-ref": "@blamejs/pki@0.2.16",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.2.14",
25
+ "version": "0.2.16",
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.2.14",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.2.16",
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.2.14",
57
+ "ref": "@blamejs/pki@0.2.16",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]