@blamejs/pki 0.2.14 → 0.2.15

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,15 @@ 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.15 — 2026-07-13
8
+
9
+ 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.
10
+
11
+ ### Added
12
+
13
+ - 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.
14
+ - 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.
15
+
7
16
  ## v0.2.14 — 2026-07-13
8
17
 
9
18
  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); 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,324 @@
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
+
48
+ var OID_DATA = O("data");
49
+ var OID_SIGNED_DATA = O("signedData");
50
+ var OID_SKI = O("subjectKeyIdentifier");
51
+
52
+ // An AlgorithmIdentifier { OID } (absent parameters) or { OID, NULL }.
53
+ function _algId(name, shape) { return shape === "null" ? b.sequence([b.oid(O(name)), b.nullValue()]) : b.sequence([b.oid(O(name))]); }
54
+ // The RSASSA-PSS AlgorithmIdentifier with the params SEQUENCE cms.verify's _resolvePss accepts:
55
+ // an explicit SHA-2 hashAlgorithm, MGF1 keyed to the same hash, the hash-length saltLength, and
56
+ // the default trailerField (omitted). RFC 4055.
57
+ function _pssAlgId(digestName) {
58
+ var hashAlg = b.sequence([b.oid(O(digestName)), b.nullValue()]);
59
+ var mgf = b.sequence([b.oid(O("mgf1")), hashAlg]);
60
+ var params = b.sequence([b.explicit(0, hashAlg), b.explicit(1, mgf), b.explicit(2, b.integer(BigInt(PSS_SALT[HASH[digestName]])))]);
61
+ return b.sequence([b.oid(O("rsassaPss")), params]);
62
+ }
63
+
64
+ // An id-RSASSA-PSS SubjectPublicKeyInfo MAY pin the permitted hash in its RSASSA-PSS-params
65
+ // (RFC 4055 sec. 1.2 / sec. 3.1): a key generated for SHA-384 cannot sign under SHA-256. Read the
66
+ // pinned digest name from the SPKI algorithm parameters so signing honors the key's restriction
67
+ // instead of hard-coding SHA-256. Absent params (an unrestricted key) or an unrecognized hash
68
+ // returns null -- the caller then falls back to its opts.digestAlgorithm or the SHA-256 default.
69
+ function _pssHashFromSpki(cert) {
70
+ var params = cert.subjectPublicKeyInfo.algorithm.parameters;
71
+ if (params == null) return null; // an unrestricted id-RSASSA-PSS key
72
+ // params is the raw parameters TLV the strict codec already validated when it parsed the cert,
73
+ // so re-decoding a single valid TLV cannot throw. Read the pinned hash structurally, defaulting
74
+ // to null (caller falls back) whenever the shape is not the expected RSASSA-PSS-params.
75
+ var node = asn1.decode(params);
76
+ if (node.tagClass !== "universal" || node.tagNumber !== asn1.TAGS.SEQUENCE || !node.children) return null;
77
+ var hashField = node.children.filter(function (c) { return c.tagClass === "context" && c.tagNumber === 0; })[0];
78
+ if (!hashField || !hashField.children || !hashField.children[0] || !hashField.children[0].children) return null;
79
+ var oidNode = hashField.children[0].children[0]; // [0] EXPLICIT AlgorithmIdentifier { hash OID, ... }
80
+ if (!oidNode || oidNode.tagClass !== "universal" || oidNode.tagNumber !== asn1.TAGS.OBJECT_IDENTIFIER) return null;
81
+ var pinnedOid = asn1.read.oid(oidNode);
82
+ var name = HASH_NAME_BY_OID[pinnedOid];
83
+ // A recognized SHA-2 pin resolves to its name; an unsupported pinned hash (e.g. SHA-3) fails
84
+ // closed -- we cannot honor the key's restriction, so we never silently sign under a different
85
+ // digest the key forbids.
86
+ if (!name) throw _err("cms/unsupported-algorithm", "the id-RSASSA-PSS signer key pins an unsupported hash algorithm (" + pinnedOid + ")");
87
+ return name;
88
+ }
89
+
90
+ // The message digest of the content under the digest algorithm (SHA-2 or SHAKE256).
91
+ function _digest(digestName, content) {
92
+ var h = SHAKE_OUT[digestName]
93
+ ? nodeCrypto.createHash(NODE_DIGEST[digestName], { outputLength: SHAKE_OUT[digestName] })
94
+ : nodeCrypto.createHash(NODE_DIGEST[digestName]);
95
+ return h.update(content).digest();
96
+ }
97
+
98
+ // Resolve the sign scheme from the signer certificate's public-key algorithm + per-signer opts:
99
+ // the digest, the digestAlgorithm and signatureAlgorithm AlgorithmIdentifiers (with the exact
100
+ // parameter shape cms.verify requires), and the WebCrypto import + sign algorithms.
101
+ function _scheme(cert, so) {
102
+ var alg = cert.subjectPublicKeyInfo.algorithm;
103
+ var keyOid = alg.oid;
104
+ if (keyOid === O("rsaEncryption") || keyOid === O("rsassaPss")) {
105
+ var isPssKey = keyOid === O("rsassaPss");
106
+ // An id-RSASSA-PSS key MAY pin its permitted hash in the SPKI params (RFC 4055 sec. 1.2): honor
107
+ // that over the SHA-256 default so Node/OpenSSL does not reject the otherwise-valid signer with
108
+ // ERR_OSSL_DIGEST_NOT_ALLOWED. An explicit digestAlgorithm that contradicts the pin is rejected
109
+ // fail-closed rather than silently signed under a digest the key forbids.
110
+ var pinned = isPssKey ? _pssHashFromSpki(cert) : null;
111
+ 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");
112
+ var d = so.digestAlgorithm || pinned || "sha256";
113
+ if (!HASH[d]) throw _err("cms/unsupported-algorithm", "unsupported RSA digest algorithm " + JSON.stringify(d));
114
+ // A general rsaEncryption key signs PKCS#1 v1.5 by default, or RSASSA-PSS when opts.pss is set.
115
+ 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 };
116
+ 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 };
117
+ }
118
+ if (keyOid === O("ecPublicKey")) {
119
+ var curveOid;
120
+ try { curveOid = asn1.read.oid(asn1.decode(alg.parameters)); }
121
+ catch (e) { throw _err("cms/unsupported-algorithm", "the signer EC key parameters are not a named-curve OID", e); }
122
+ var ec = EC_BY_CURVE_OID[curveOid];
123
+ if (!ec) throw _err("cms/unsupported-algorithm", "the signer key is on an unsupported EC curve");
124
+ var de = so.digestAlgorithm || "sha256";
125
+ if (!HASH[de]) throw _err("cms/unsupported-algorithm", "unsupported ECDSA digest algorithm " + JSON.stringify(de));
126
+ 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 };
127
+ }
128
+ if (keyOid === O("Ed25519") || keyOid === O("Ed448")) {
129
+ var name = keyOid === O("Ed25519") ? "Ed25519" : "Ed448";
130
+ var dd = so.digestAlgorithm || (name === "Ed25519" ? "sha512" : "shake256");
131
+ if (!NODE_DIGEST[dd]) throw _err("cms/unsupported-algorithm", "unsupported " + name + " digest algorithm " + JSON.stringify(dd));
132
+ return { digest: dd, digestAlgId: _algId(dd, "absent"), sigAlgId: _algId(name, "absent"), imp: { name: name }, sign: { name: name }, ecdsaDer: false };
133
+ }
134
+ throw _err("cms/unsupported-algorithm", "unsupported signer key algorithm " + keyOid);
135
+ }
136
+
137
+ // The raw issuer Name TLV from a parsed certificate (byte-identical to the cert, so the sid the
138
+ // verifier canonically compares matches exactly). The issuer is the Name after the optional
139
+ // version [0] and the serial + signature AlgorithmIdentifier in the tbsCertificate.
140
+ function _issuerBytes(cert) {
141
+ var tbs = asn1.decode(cert.tbsBytes);
142
+ var hasVersion = tbs.children[0].tagClass === "context" && tbs.children[0].tagNumber === 0;
143
+ return tbs.children[hasVersion ? 3 : 2].bytes;
144
+ }
145
+ // The cert's subjectKeyIdentifier extension value (the raw key id), or throws.
146
+ function _skiValue(cert) {
147
+ var ext = (cert.extensions || []).filter(function (e) { return e.oid === OID_SKI; })[0];
148
+ if (!ext) throw _err("cms/no-ski", "a subjectKeyIdentifier signer identifier requires the signer certificate to carry an SKI extension");
149
+ try { return asn1.read.octetString(asn1.decode(ext.value)); }
150
+ catch (e) { throw _err("cms/no-ski", "the signer certificate's subjectKeyIdentifier extension value is not an OCTET STRING", e); }
151
+ }
152
+
153
+ // A caller-supplied CryptoKey carries its OWN algorithm (name, and for RSA the baked-in hash,
154
+ // for ECDSA the curve). A PKCS#8 key is imported under the resolved scheme, so it always agrees;
155
+ // but a pre-imported CryptoKey whose algorithm disagrees with the certificate's scheme would
156
+ // silently sign under a different hash than the digestAlgorithm advertises -- an inconsistent,
157
+ // non-verifiable CMS. Reject the mismatch fail-closed.
158
+ function _assertKeyMatchesScheme(key, imp) {
159
+ var ka = key.algorithm || {};
160
+ 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 + ")");
161
+ 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 + ")");
162
+ 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 + ")");
163
+ }
164
+ // Import the signer private key: a CryptoKey passed through, or a PKCS#8 DER Buffer / PEM string.
165
+ function _importKey(key, imp) {
166
+ if (key && typeof key === "object" && !Buffer.isBuffer(key) && !(key instanceof Uint8Array) && key.type === "private") {
167
+ _assertKeyMatchesScheme(key, imp);
168
+ return Promise.resolve(key);
169
+ }
170
+ var der;
171
+ if (Buffer.isBuffer(key)) der = key;
172
+ else if (key instanceof Uint8Array) der = Buffer.from(key);
173
+ 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); } }
174
+ else throw _err("cms/bad-input", "a signer key must be a CryptoKey, a PKCS#8 DER Buffer, or a PKCS#8 PEM string");
175
+ return subtle.importKey("pkcs8", der, imp, false, ["sign"]);
176
+ }
177
+
178
+ // Build one SignerInfo (RFC 5652 sec. 5.3) and sign it. Resolves the SignerInfo (as a build
179
+ // node) plus its digestAlgorithm AlgorithmIdentifier and the signer certificate DER (for the
180
+ // SignedData digestAlgorithms + certificates sets).
181
+ function _buildSignerInfo(signer, content, eContentType, opts) {
182
+ var so = signer || {};
183
+ var certDer = _normCertDer(so.cert);
184
+ var cert = x509.parse(certDer);
185
+ var scheme = _scheme(cert, so);
186
+ var useSki = opts.sid === "ski";
187
+ var sid = useSki
188
+ ? b.contextPrimitive(0, _skiValue(cert)) // [0] IMPLICIT SubjectKeyIdentifier
189
+ : b.sequence([b.raw(_issuerBytes(cert)), b.integer(cert.serialNumber)]); // IssuerAndSerialNumber
190
+ var version = useSki ? 3 : 1;
191
+
192
+ return _importKey(so.key, scheme.imp).then(function (priv) {
193
+ return Promise.resolve().then(function () {
194
+ if (opts.signedAttributes === false) return content; // sign the content directly (no signed attributes)
195
+ // Signed attributes (RFC 5652 sec. 5.3): content-type == eContentType, message-digest ==
196
+ // digest(content), and (by default) signing-time. build.set canonical-DER SET-OF-sorts them.
197
+ // Each attribute type appears AT MOST ONCE across the whole set (RFC 5652 sec. 5.3);
198
+ // `seenTypes` catches a caller-supplied attribute that duplicates a built-in or another.
199
+ var seenTypes = {};
200
+ function _pushAttr(typeOid, values) {
201
+ if (seenTypes[typeOid]) throw _err("cms/bad-input", "signedAttrs must not repeat an attribute type (RFC 5652 sec. 5.3): " + typeOid);
202
+ seenTypes[typeOid] = 1;
203
+ attrs.push(b.sequence([b.oid(typeOid), b.set(values)]));
204
+ }
205
+ var attrs = [];
206
+ _pushAttr(O("contentType"), [b.oid(eContentType)]);
207
+ _pushAttr(O("messageDigest"), [b.octetString(_digest(scheme.digest, content))]);
208
+ if (opts.signingTime !== false) _pushAttr(O("signingTime"), [_timeValue(opts.signingTime)]);
209
+ // Caller-supplied signed attributes (e.g. an RFC 3161 signing-certificate attribute): each
210
+ // { type: <OID name or dotted string>, values: [<DER value Buffer>] }. build.set sorts them in.
211
+ (opts.additionalSignedAttributes || []).forEach(function (a) {
212
+ var vals = (a.values || []).map(function (v) { return _toBuf(v, "a signed attribute value"); });
213
+ 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))");
214
+ _pushAttr(/^\d+(\.\d+)+$/.test(a.type) ? a.type : O(a.type), vals);
215
+ });
216
+ var setOf = b.set(attrs); // SET OF (tag 0x31) -- the exact bytes the signature covers (sec. 5.4)
217
+ var wire = Buffer.from(setOf); wire[0] = 0xA0; // the on-wire [0] IMPLICIT tag
218
+ return { setOf: setOf, wire: wire };
219
+ }).then(function (toSign) {
220
+ var signedBytes = toSign.setOf ? toSign.setOf : toSign; // SET-OF form for signing (sec. 5.4)
221
+ return subtle.sign(scheme.sign, priv, signedBytes).then(function (sigRaw) {
222
+ var sig = Buffer.from(sigRaw);
223
+ if (scheme.ecdsaDer) sig = validator.sig.rawToEcdsaDer(sig, scheme.coordLen);
224
+ var fields = [b.integer(BigInt(version)), sid, scheme.digestAlgId];
225
+ if (toSign.wire) fields.push(toSign.wire); // [0] IMPLICIT signedAttrs
226
+ fields.push(scheme.sigAlgId, b.octetString(sig));
227
+ return { si: b.sequence(fields), digestAlgId: scheme.digestAlgId, version: version, certDer: certDer };
228
+ });
229
+ });
230
+ });
231
+ }
232
+
233
+ // A signing-time Time value: UTCTime before 2050, GeneralizedTime from 2050 (RFC 5652 sec. 11.3 /
234
+ // RFC 5280 sec. 4.1.2.5). A caller Date overrides; false omits the attribute (handled above).
235
+ function _timeValue(when) {
236
+ var d = (when instanceof Date) ? when : new Date();
237
+ return d.getUTCFullYear() < 2050 ? b.utcTime(d) : b.generalizedTime(d);
238
+ }
239
+
240
+ // Normalize a signer certificate input to its raw DER (DER Buffer / PEM string / Uint8Array).
241
+ // The same bytes drive scheme resolution and the certificates [0] embedding, so a parsed
242
+ // certificate (which does not retain its full DER) is rejected -- pass DER or PEM.
243
+ function _normCertDer(c) {
244
+ if (c == null) throw _err("cms/bad-input", "each signer requires a certificate (cert)");
245
+ if (c instanceof Uint8Array && !Buffer.isBuffer(c)) c = Buffer.from(c); // a Uint8Array -> Buffer (below)
246
+ if (Buffer.isBuffer(c)) return c[0] === 0x30 ? c : _pemToDer(c.toString("latin1")); // DER as-is, else PEM
247
+ if (typeof c === "string") return _pemToDer(c);
248
+ throw _err("cms/bad-input", "a signer certificate must be a DER Buffer or a PEM string");
249
+ }
250
+ function _pemToDer(text) {
251
+ var m = text.match(/-----BEGIN CERTIFICATE-----([\s\S]*?)-----END CERTIFICATE-----/);
252
+ if (!m) throw _err("cms/bad-input", "a signer certificate PEM is not a CERTIFICATE block");
253
+ return Buffer.from(m[1].replace(/[^A-Za-z0-9+/=]/g, ""), "base64");
254
+ }
255
+
256
+ // pki.cms.sign -- documented by the @primitive block in cms-verify.js (the @module pki.cms home).
257
+ function sign(content, signers, opts) {
258
+ opts = opts || {};
259
+ if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw _err("cms/bad-input", "pki.cms.sign options must be an object");
260
+ var contentBuf = _toBuf(content, "content");
261
+ var list = Array.isArray(signers) ? signers : [signers];
262
+ if (!list.length) throw _err("cms/bad-input", "pki.cms.sign requires at least one signer");
263
+ var eContentType = opts.eContentType ? O(opts.eContentType) : OID_DATA;
264
+ // RFC 5652 sec. 5.3: signed attributes MUST be present (carrying a content-type attribute)
265
+ // whenever the encapsulated content type is not id-data -- so signedAttributes:false is only
266
+ // valid for id-data content. Refusing it here keeps cms.sign from emitting a non-conformant
267
+ // SignedData (e.g. a timestamp token, id-ct-TSTInfo, with no signed attributes).
268
+ if (opts.signedAttributes === false && eContentType !== OID_DATA) {
269
+ throw _err("cms/bad-input", "signed attributes are required when eContentType is not id-data (RFC 5652 sec. 5.3)");
270
+ }
271
+ // A supplied signing-time MUST be a valid Date (or false to omit the attribute) -- never a
272
+ // silently-ignored non-Date or an Invalid Date that would encode a garbage Time.
273
+ if (opts.signingTime != null && opts.signingTime !== false && (!(opts.signingTime instanceof Date) || isNaN(opts.signingTime.getTime()))) {
274
+ throw _err("cms/bad-input", "signingTime must be a valid Date, or false to omit it");
275
+ }
276
+
277
+ return Promise.all(list.map(function (s) { return _buildSignerInfo(s, contentBuf, eContentType, opts); })).then(function (built) {
278
+ // digestAlgorithms: the distinct SignerInfo digestAlgorithm AlgorithmIdentifiers, deduped.
279
+ var seen = {}, digestAlgs = [];
280
+ built.forEach(function (x) { var k = x.digestAlgId.toString("hex"); if (!seen[k]) { seen[k] = 1; digestAlgs.push(x.digestAlgId); } });
281
+ // CMSVersion (RFC 5652 sec. 5.1): 3 if any SignerInfo is v3 (ski) or eContentType != id-data;
282
+ // otherwise 1 (v1 emits only X.509 certificates, so the v4/v5 attribute-certificate cases
283
+ // do not arise).
284
+ var v3 = built.some(function (x) { return x.version === 3; }) || eContentType !== OID_DATA;
285
+ var version = v3 ? 3 : 1;
286
+ // EncapsulatedContentInfo: eContentType + [0] EXPLICIT eContent (omitted when detached).
287
+ var encapFields = [b.oid(eContentType)];
288
+ if (!opts.detached) encapFields.push(b.explicit(0, b.octetString(contentBuf)));
289
+ var encap = b.sequence(encapFields);
290
+ // certificates [0] IMPLICIT SET OF (the signer certs), deduped + SET-OF-ordered, when embedded.
291
+ var sdFields = [b.integer(BigInt(version)), b.set(digestAlgs), encap];
292
+ if (opts.certificates !== false) {
293
+ var certDers = _dedupe(built.map(function (x) { return x.certDer; })).sort(Buffer.compare); // X.690 sec. 11.6
294
+ sdFields.push(b.contextConstructed(0, Buffer.concat(certDers))); // [0] IMPLICIT SET OF
295
+ }
296
+ sdFields.push(b.set(built.map(function (x) { return x.si; }))); // signerInfos SET OF
297
+ var signedData = b.sequence(sdFields);
298
+ var contentInfo = b.sequence([b.oid(OID_SIGNED_DATA), b.explicit(0, signedData)]); // ContentInfo
299
+ return opts.pem ? pkix.pemEncode(contentInfo, "CMS", frameworkError.PemError) : contentInfo;
300
+ });
301
+ }
302
+
303
+ // Dedupe certificate DERs (two signers may share a cert -- embed it once).
304
+ function _dedupe(ders) {
305
+ var seen = {}, out = [];
306
+ ders.forEach(function (d) { var k = d.toString("hex"); if (!seen[k]) { seen[k] = 1; out.push(d); } });
307
+ return out;
308
+ }
309
+
310
+ function _toBuf(v, what) {
311
+ if (Buffer.isBuffer(v)) return v;
312
+ if (v instanceof Uint8Array) return Buffer.from(v);
313
+ throw _err("cms/bad-input", what + " must be a Buffer");
314
+ }
315
+
316
+ // Coverage residual -- three defensive branches are unreachable through the shipped path:
317
+ // * `_skiValue`'s `cert.extensions || []` fallback -- x509.parse always surfaces `extensions`
318
+ // as an array (empty when absent), so the `|| []` never fires.
319
+ // * `_assertKeyMatchesScheme`'s `key.algorithm || {}` -- a WebCrypto CryptoKey always carries
320
+ // an `algorithm`, so the `|| {}` fallback never fires.
321
+ // * `_assertKeyMatchesScheme`'s `!ka.hash` guard -- an `imp.hash` is set only for an RSA
322
+ // scheme, which requires `ka.name` to already equal the RSA name (else the earlier name
323
+ // check throws); an RSA CryptoKey always carries a `hash`, so `!ka.hash` never fires.
324
+ module.exports = { sign: sign };
package/lib/cms-verify.js CHANGED
@@ -20,6 +20,7 @@
20
20
  * @card Verify a CMS SignedData signature (S/MIME, timestamps, code signing).
21
21
  */
22
22
 
23
+ var nodeCrypto = require("crypto");
23
24
  var asn1 = require("./asn1-der");
24
25
  var oid = require("./oid");
25
26
  var x509 = require("./schema-x509");
@@ -27,6 +28,7 @@ var cms = require("./schema-cms");
27
28
  var webcrypto = require("./webcrypto");
28
29
  var subtle = webcrypto.webcrypto.subtle;
29
30
  var edwardsPoint = require("./edwards-point");
31
+ var cmsSign = require("./cms-sign");
30
32
  var validator = require("./validator-all");
31
33
  var guard = require("./guard-all");
32
34
  var frameworkError = require("./framework-error");
@@ -39,6 +41,16 @@ var OID_CONTENT_TYPE = oid.byName("contentType");
39
41
 
40
42
  // A digest-algorithm name -> the WebCrypto hash.
41
43
  var DIGEST_HASH = { sha1: "SHA-1", sha256: "SHA-256", sha384: "SHA-384", sha512: "SHA-512" };
44
+ // SHAKE256 (RFC 8419 sec. 2.3, the Ed448 message digest) has no WebCrypto hash, so it is computed
45
+ // with node:crypto; its 512-bit (64-byte) output length is fixed by the profile.
46
+ var SHAKE_OUT = { shake256: 64 };
47
+ // Is `name` a message-digest algorithm this verifier supports (SHA-2 family or SHAKE256)?
48
+ function _supportedDigest(name) { return !!(DIGEST_HASH[name] || SHAKE_OUT[name]); }
49
+ // The digest of `content` under the named algorithm, resolved to a Buffer.
50
+ function _computeDigest(name, content) {
51
+ if (SHAKE_OUT[name]) return Promise.resolve(nodeCrypto.createHash(name, { outputLength: SHAKE_OUT[name] }).update(content).digest());
52
+ return subtle.digest(DIGEST_HASH[name], content).then(function (d) { return Buffer.from(d); });
53
+ }
42
54
  // A signatureAlgorithm name -> its verify scheme. A combined OID (sha256WithRSAEncryption,
43
55
  // ecdsaWithSHA256) carries its own hash; a bare key OID (rsaEncryption, ecPublicKey) takes the
44
56
  // hash from the SignerInfo digestAlgorithm.
@@ -284,7 +296,7 @@ function _verifyOne(si, content, eContentType, parsedCerts) {
284
296
  // the content digest is required whenever signed attributes are present, for every scheme
285
297
  // (the message-digest attribute is computed under digestAlgorithm). Either gap is a
286
298
  // 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) });
299
+ if ((scheme.kind !== "eddsa" && !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) });
288
300
  var signers = _findSignerCerts(si.sid, parsedCerts);
289
301
  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
302
  var sigBytes = _toBuf(si.signature, "the SignerInfo signature");
@@ -319,8 +331,8 @@ function _verifyOne(si, content, eContentType, parsedCerts) {
319
331
  try {
320
332
  declared = asn1.read.octetString(mdAttr.values[0]);
321
333
  } 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" } };
334
+ return _computeDigest(si.digestAlgorithm.name, content).then(function (d) {
335
+ if (!d.equals(declared)) return { mismatch: { code: "cms/message-digest-mismatch", message: "the message-digest attribute does not match the content digest" } };
324
336
  return reTagged;
325
337
  });
326
338
  }).then(function (signedBytes) {
@@ -401,4 +413,37 @@ function _addCert(out, der) {
401
413
  // is always an array (or it throws), so this belt-and-suspenders throw never fires.
402
414
  // * `c && c.bytes ? c.bytes : c` -- schema-cms surfaces every embedded certificate as an
403
415
  // object carrying a `bytes` Buffer, so the raw-value fallback never fires.
404
- module.exports = { verify: verify };
416
+ /**
417
+ * @primitive pki.cms.sign
418
+ * @signature pki.cms.sign(content, signers, opts?) -> Promise<Buffer|string>
419
+ * @since 0.2.15
420
+ * @status experimental
421
+ * @spec RFC 5652
422
+ * @related pki.cms.verify, pki.schema.cms.parse
423
+ *
424
+ * Produce a CMS SignedData (RFC 5652 sec. 5) over `content` (a `Buffer`) -- the structure
425
+ * S/MIME signed mail, RFC 3161 timestamp tokens, and code signing rest on, and exactly what
426
+ * `pki.cms.verify` consumes and OpenSSL `cms -verify` validates. Each `signers[i]` is
427
+ * `{ cert, key, digestAlgorithm?, pss? }`: `cert` the signer certificate (PEM or DER), `key`
428
+ * its private key (a WebCrypto `CryptoKey` or a PKCS#8 DER `Buffer` / PEM string). The
429
+ * signature covers the RFC 5652 sec. 5.4 preimage: with signed attributes (the default) the
430
+ * message-digest attribute is bound to the content digest and the signature is over the
431
+ * canonical DER SET OF SignedAttributes; otherwise over the content directly. RSA (PKCS#1 v1.5
432
+ * and, with `pss`, RSASSA-PSS), ECDSA (P-256/384/521), Ed25519, and Ed448 are covered.
433
+ *
434
+ * @opts detached Omit the encapsulated content (a detached signature; the verifier
435
+ * supplies the content). Default false.
436
+ * @opts eContentType The encapsulated content type (an OID name). Default `data`.
437
+ * @opts signedAttributes Include signed attributes (content-type, message-digest, signing-time);
438
+ * false signs the content directly. Default true.
439
+ * @opts signingTime A `Date` for the signing-time attribute, or false to omit it.
440
+ * @opts sid `"issuerAndSerial"` (default) or `"ski"` (subjectKeyIdentifier).
441
+ * @opts certificates Embed the signer certificates in the output. Default true.
442
+ * @opts pem Return a PEM string (`-----BEGIN CMS-----`) instead of a DER Buffer.
443
+ * @example
444
+ * var p7 = await pki.cms.sign(Buffer.from("hello"), { cert: signerCertDer, key: signerKeyPkcs8 });
445
+ * var res = await pki.cms.verify(p7); // res.valid === true
446
+ */
447
+ var sign = cmsSign.sign;
448
+
449
+ 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.15",
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:edfce9fb-f69d-4264-b69d-27177c001bc9",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-13T21:48:02.712Z",
8
+ "timestamp": "2026-07-14T00:56:05.894Z",
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.15",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.2.14",
25
+ "version": "0.2.15",
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.15",
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.15",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]