@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 +9 -0
- package/README.md +13 -8
- package/bin/pki.js +28 -4
- package/index.js +2 -0
- package/lib/cms-sign.js +324 -0
- package/lib/cms-verify.js +49 -4
- package/lib/tsp-sign.js +161 -0
- package/lib/validator-sig.js +18 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
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.
|
|
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
|
|
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,
|
|
254
|
-
API can't. `lint` exits non-zero when any `error`/`fatal` finding is present; `verify`
|
|
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),
|
|
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,
|
|
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
|
-
|
|
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
|
package/lib/cms-sign.js
ADDED
|
@@ -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 && !
|
|
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
|
|
323
|
-
if (!
|
|
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
|
-
|
|
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 };
|
package/lib/tsp-sign.js
ADDED
|
@@ -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 };
|
package/lib/validator-sig.js
CHANGED
|
@@ -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
|
-
|
|
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
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:edfce9fb-f69d-4264-b69d-27177c001bc9",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
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.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.2.15",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.2.
|
|
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.
|
|
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.
|
|
57
|
+
"ref": "@blamejs/pki@0.2.15",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|