@blamejs/pki 0.2.18 → 0.2.19
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 +14 -0
- package/README.md +3 -3
- package/lib/cms-verify.js +1 -1
- package/lib/jose.js +1 -1
- package/lib/schema-all.js +1 -1
- package/lib/schema-tsp.js +68 -0
- package/lib/sigstore.js +1 -1
- package/lib/tsp-sign.js +523 -1
- package/lib/webcrypto.js +1 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@ 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.19 — 2026-07-14
|
|
8
|
+
|
|
9
|
+
The RFC 3161 Time-Stamp Protocol surface is complete: pki.tsp.request and pki.tsp.response build and parse the protocol's request and response messages, and pki.tsp.verify verifies a timestamp token end to end -- the CMS signature, the message imprint, the ESSCertID(V2) certificate binding, the critical timeStamping-only extendedKeyUsage, and full validation of the TSA certificate at the token's own genTime.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- pki.tsp.request builds an RFC 3161 TimeStampReq around a message imprint, with the optional nonce, requested TSA policy, certReq, and extensions -- canonical DER, the DEFAULT-FALSE certReq omitted unless true -- and pki.tsp.parseRequest parses one (a new TimeStampReq decoder, also exposed as pki.schema.tsp.parseRequest). pki.tsp.response builds a TimeStampResp -- a granted status wrapping the token pki.tsp.sign produces, or a rejection carrying a PKIStatus, status text, and PKIFailureInfo names -- and pki.tsp.parseResponse parses one; the section 2.4.2 status-to-token coupling (a granted response carries a token, any other status must not) is enforced on build and parse alike. These are the byte payloads an RFC 3161 transport carries, completing the protocol message surface around pki.tsp.sign and pki.schema.tsp.parseToken.
|
|
14
|
+
- pki.tsp.verify(token, data, opts) verifies a timestamp token and returns a verdict carrying the verified genTime, serial number, and TSTInfo fields. It checks the CMS signature over the exact signed bytes, recomputes the message imprint from the supplied data (or compares a precomputed imprint), requires the encapsulated content be a TSTInfo, binds the token to the TSA certificate by recomputing the ESSCertID(V2) certificate hash (RFC 5816) and matching its issuerSerial when present, and enforces RFC 3161 section 2.3 on the TSA certificate: its extendedKeyUsage must be present, critical, and contain exactly id-kp-timeStamping, and -- when the certificate asserts a keyUsage -- that keyUsage must permit signing, so a certificate not issued for timestamping cannot mint a trusted token. When a trust anchor is supplied, the TSA certificate chain -- ordered from the token's embedded certificates, so a TSA under an intermediate CA validates -- receives full certification-path validation at the token's genTime, including optional revocation; when the request carried a nonce, the token must echo it. Every checked field is read from the verified encapsulated content, never a caller-supplied parsed object; a well-formed token failing any check is a fail-closed { valid: false } verdict with a stable reason code.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- CMS signer-certificate lookup now matches the certificate's issuer name in addition to its serial number when a signer is identified by issuerAndSerialNumber (RFC 5652); the issuer comparison was previously inert, so a signer was located by serial number alone. The verification verdict is unchanged -- the signature check remains the authority -- but the correct signer certificate is now selected precisely.
|
|
19
|
+
- Malformed input to several verifiers now fails closed with a typed pki.errors.PkiError instead of a raw TypeError: a signer or issuer distinguished name carrying an embedded control byte (the RFC 5280 section 7.1 name comparison, CVE-2009-2408) and oversized or malformed JSON are rejected with a domain error code across pki.cms.verify, pki.tsp.verify, pki.jose, pki.sigstore, and pki.webcrypto key import.
|
|
20
|
+
|
|
7
21
|
## v0.2.18 — 2026-07-14
|
|
8
22
|
|
|
9
23
|
Composite ML-DSA signatures join CMS SignedData: pki.cms.sign and pki.cms.verify now produce and verify a composite SignerInfo -- a post-quantum ML-DSA paired with a traditional RSA, ECDSA, or EdDSA -- accepted only when BOTH components verify.
|
package/README.md
CHANGED
|
@@ -209,7 +209,7 @@ is callable today; nothing below is a stub.
|
|
|
209
209
|
| `pki.schema.pkcs8` | Parse DER / PEM PKCS#8 private keys per RFC 5208 / 5958 — algorithm, raw key bytes, attributes, optional public key, fail-closed; encrypted keys recognized (not decrypted) — `parse`, `parseEncrypted`, `pemDecode`, `pemEncode` |
|
|
210
210
|
| `pki.schema.cms` | Parse DER / PEM CMS per RFC 5652 / 5083 / 9629 — SignedData (§5, signer infos + raw signed-attribute bytes for external verification), EnvelopedData (§6, all five RecipientInfo kinds incl. RFC 5753 key-agreement and RFC 9629 KEM recipients with ML-KEM validation), EncryptedData (§8), AuthenticatedData (§9, MAC surface + raw `authAttrsBytes`), and AuthEnvelopedData (RFC 5083, with RFC 5084 AES-GCM/CCM parameter validation); §11 attribute placement enforced, countersignatures validated recursively, certificates / CRLs validated against the closed CHOICE sets and kept raw, every result tagged `contentTypeName`, fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
|
211
211
|
| `pki.schema.ocsp` | Parse DER / PEM OCSP requests and responses per RFC 6960 — per-certificate status (good / revoked / unknown), responder identity, raw tbs bytes for external verification, certificates kept raw, fail-closed; non-basic response types recognized (not decoded) — `parseRequest`, `parseResponse`, `pemDecode`, `pemEncode` |
|
|
212
|
-
| `pki.schema.tsp` | Parse DER / PEM RFC 3161 timestamp responses and tokens — the TSTInfo payload (imprint, genTime with sub-second precision, serial, nonce, accuracy), the status-to-token coupling, and the token wrapper composed over CMS with the single-signer rule, fail-closed — `parse`, `parseTstInfo`, `parseToken`, `pemDecode`, `pemEncode` |
|
|
212
|
+
| `pki.schema.tsp` | Parse DER / PEM RFC 3161 timestamp requests, responses, and tokens — the TimeStampReq a client sends (imprint, requested policy, nonce, certReq), the TSTInfo payload (imprint, genTime with sub-second precision, serial, nonce, accuracy), the status-to-token coupling, and the token wrapper composed over CMS with the single-signer rule, fail-closed — `parse`, `parseRequest`, `parseResponse`, `parseTstInfo`, `parseToken`, `pemDecode`, `pemEncode` |
|
|
213
213
|
| `pki.schema.attrcert` | Parse DER / PEM RFC 5755 attribute certificates — the holder and issuer identities (validated GeneralNames), the validity window (real `Date`s), the privilege attributes (role / group / clearance) and extensions, with the raw signed region for a verifier; the obsolete v1 form is recognized and deferred, fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
|
214
214
|
| `pki.schema.crmf` | Parse DER / PEM RFC 4211 certificate request messages (CertReqMessages — the CMP / EST enrollment body) — the requested-certificate template (subject, public key, validity, extensions), proof-of-possession, and registration controls, with the raw `CertRequest` region a POP verifier hashes; names dual-accepted (IMPLICIT and EXPLICIT), fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
|
215
215
|
| `pki.schema.pkcs12` | Parse DER / BER / PEM RFC 7292 PKCS#12 (PFX) stores — key bags via the PKCS#8 parser, shrouded keys (algorithm surfaced, ciphertext opaque), cert / CRL / secret bags raw and byte-exact, encrypted and enveloped safes structurally via CMS, `friendlyName` / `localKeyId` decoded, and the exact MAC byte range (`macedBytes`) plus RFC 9579 PBMAC1 recognition for external verification; BER accepted exactly where §4.1 requires it, fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
|
@@ -222,7 +222,7 @@ is callable today; nothing below is a stub.
|
|
|
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
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, the post-quantum ML-DSA-44/65/87 (RFC 9882) and SLH-DSA (all twelve FIPS 205 sets, RFC 9814), and composite ML-DSA (pairing ML-DSA with a traditional RSA / ECDSA / EdDSA — accepted only when **both** components verify — draft-ietf-lamps-cms-composite-sigs)); it builds the signed attributes (content-type, message-digest, signing-time) as canonical DER, signs the exact §5.4 preimage, and emits a DER `Buffer` or PEM. `verify(input, opts)` parses a SignedData over the strict `pki.schema.cms` codec, locates each SignerInfo's signer certificate by its issuerAndSerialNumber or subjectKeyIdentifier, and checks the signature over the exact §5.4 preimage: when signed attributes are present it confirms the message-digest attribute equals the content digest and verifies over the DER re-encoding of the SignedAttributes (the on-wire `[0]` tag replaced by a universal SET OF), otherwise directly over the content. It returns a per-signer verdict with the matched signer certificate; it does not chain that certificate to a trust anchor — that is the caller's step through `pki.path.validate`. Fail-closed with typed `cms/*` errors — `sign`, `verify` |
|
|
225
|
-
| `pki.tsp` | RFC 3161
|
|
225
|
+
| `pki.tsp` | RFC 3161 Time-Stamp Protocol — `sign(messageImprint, tsa, opts)` produces a TimeStampToken: a CMS SignedData (over `pki.cms.sign`) whose content is a `TSTInfo` carrying the timestamped message imprint, the TSA policy, a serial number, and `genTime` (with optional accuracy / nonce / ordering), plus the RFC 3161 §2.4.2 signing-certificate attribute binding the token to the TSA certificate (SHA-2 imprints, any `pki.cms.sign` TSA key). `request` / `parseRequest` build and parse the TimeStampReq a client sends (imprint, requested policy, nonce, certReq), `response` / `parseResponse` the TimeStampResp a TSA returns — a granted status wrapping a token, or a rejection with PKIStatus and failure info, the §2.4.2 status↔token coupling enforced in both directions. `verify(token, data, opts)` verifies a token fail-closed: the CMS signature over the exact signed bytes, the message imprint recomputed from the data, the TSTInfo content type, the ESSCertID(V2) binding to the TSA certificate, the §2.3 critical timeStamping-only extendedKeyUsage, the request nonce when used, and — with a trust anchor supplied — full certification-path validation of the TSA certificate at the token's `genTime`, returning `{ valid, genTime, serialNumber, tstInfo, … }` — `sign`, `request`, `parseRequest`, `response`, `parseResponse`, `verify` |
|
|
226
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` |
|
|
227
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` |
|
|
228
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` |
|
|
@@ -343,7 +343,7 @@ SCTs), `pki.hpke` (RFC 9180), `pki.shbs` (HSS/LMS stateful hash signatures),
|
|
|
343
343
|
`pki.merkle` (RFC 9162 transparency proofs), `pki.sigstore` (offline npm-provenance
|
|
344
344
|
verification), `pki.webauthn` (WebAuthn / passkey attestation verification),
|
|
345
345
|
`pki.cms` (RFC 5652 SignedData signing + signature verification), `pki.tsp` (RFC 3161
|
|
346
|
-
|
|
346
|
+
timestamping — requests, responses, token creation and verification), and the
|
|
347
347
|
`jose` / `acme` / `est` enrollment surfaces. Each composes
|
|
348
348
|
the shared structure, foundation, and crypto layers directly. Alongside the schema
|
|
349
349
|
engine, the fail-closed **guard family** (`guard-*`) centralizes each CVE-class
|
package/lib/cms-verify.js
CHANGED
|
@@ -120,7 +120,7 @@ function _findSignerCerts(sid, parsedCerts) {
|
|
|
120
120
|
if (sid.subjectKeyIdentifier != null) {
|
|
121
121
|
if (c.ski && c.ski.equals(_toBuf(sid.subjectKeyIdentifier, "sid.subjectKeyIdentifier"))) out.push(c);
|
|
122
122
|
} else if (sid.issuer && sid.serialNumberHex != null) {
|
|
123
|
-
if (c.cert.serialNumberHex === sid.serialNumberHex && guard.name.dnEqual(c.cert.issuer, sid.issuer,
|
|
123
|
+
if (c.cert.serialNumberHex === sid.serialNumberHex && guard.name.dnEqual(c.cert.issuer.rdns, sid.issuer.rdns, _err, "cms/bad-name", "the signer certificate issuer")) out.push(c);
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
return out;
|
package/lib/jose.js
CHANGED
|
@@ -117,7 +117,7 @@ function parseJson(input) {
|
|
|
117
117
|
// The strict bounded reader (byte + depth caps, duplicate-member reject at every
|
|
118
118
|
// depth, __proto__-safe own-property assignment, fatal UTF-8, RFC 8259 grammar)
|
|
119
119
|
// lives once in the shared JSON guard; the frozen jose/* codes are threaded through.
|
|
120
|
-
return guard.json.parse(input,
|
|
120
|
+
return guard.json.parse(input, E, {
|
|
121
121
|
maxBytes: LIMITS.JSON_MAX_BYTES, maxDepth: LIMITS.JSON_MAX_DEPTH,
|
|
122
122
|
badJson: "jose/bad-json", tooDeep: "jose/too-deep", duplicateMember: "jose/duplicate-member",
|
|
123
123
|
tooLarge: "jose/too-large", badInput: "jose/bad-input", label: "the JSON document",
|
package/lib/schema-all.js
CHANGED
|
@@ -301,7 +301,7 @@ module.exports = {
|
|
|
301
301
|
pkcs12: { parse: pkcs12.parse, pemDecode: pkcs12.pemDecode, pemEncode: pkcs12.pemEncode },
|
|
302
302
|
cms: { parse: cms.parse, pemDecode: cms.pemDecode, pemEncode: cms.pemEncode },
|
|
303
303
|
ocsp: { parseRequest: ocsp.parseRequest, parseResponse: ocsp.parseResponse, pemDecode: ocsp.pemDecode, pemEncode: ocsp.pemEncode },
|
|
304
|
-
tsp: { parse: tsp.parse, parseTstInfo: tsp.parseTstInfo, parseToken: tsp.parseToken, pemDecode: tsp.pemDecode, pemEncode: tsp.pemEncode },
|
|
304
|
+
tsp: { parse: tsp.parse, parseResponse: tsp.parseResponse, parseRequest: tsp.parseRequest, parseTstInfo: tsp.parseTstInfo, parseToken: tsp.parseToken, pemDecode: tsp.pemDecode, pemEncode: tsp.pemEncode },
|
|
305
305
|
crmf: { parse: crmf.parse, pemDecode: crmf.pemDecode, pemEncode: crmf.pemEncode },
|
|
306
306
|
cmp: { parse: cmp.parse, pemDecode: cmp.pemDecode, pemEncode: cmp.pemEncode },
|
|
307
307
|
csrattrs: { parse: csrattrs.parse },
|
package/lib/schema-tsp.js
CHANGED
|
@@ -272,6 +272,64 @@ var TIME_STAMP_RESP = schema.seq([
|
|
|
272
272
|
},
|
|
273
273
|
});
|
|
274
274
|
|
|
275
|
+
// ---- TimeStampReq ----------------------------------------------------
|
|
276
|
+
|
|
277
|
+
// TimeStampReq ::= SEQUENCE { version INTEGER { v1(1) }, messageImprint MessageImprint,
|
|
278
|
+
// reqPolicy TSAPolicyId OPTIONAL, nonce INTEGER OPTIONAL, certReq BOOLEAN DEFAULT FALSE,
|
|
279
|
+
// extensions [0] IMPLICIT Extensions OPTIONAL } (RFC 3161 sec. 2.4.1). version + messageImprint
|
|
280
|
+
// are mandatory; the three optionals are consumed by universal tag (OID / INTEGER / BOOLEAN),
|
|
281
|
+
// then the [0] extensions. certReq DEFAULT FALSE follows the TSTInfo.ordering explicit-FALSE rule.
|
|
282
|
+
var TIME_STAMP_REQ = schema.seq([
|
|
283
|
+
schema.field("version", VERSION),
|
|
284
|
+
schema.field("messageImprint", MESSAGE_IMPRINT),
|
|
285
|
+
schema.optional("reqPolicy", schema.oidLeaf(), { whenUniversal: [TAGS.OBJECT_IDENTIFIER] }),
|
|
286
|
+
schema.optional("nonce", schema.integerLeaf(), { whenUniversal: [TAGS.INTEGER] }),
|
|
287
|
+
schema.optional("certReq", schema.boolean(), { whenUniversal: [TAGS.BOOLEAN] }),
|
|
288
|
+
schema.trailing([
|
|
289
|
+
{ tag: 0, name: "extensions", schema: schema.implicitSeqOf(0, EXTENSION, { min: 1, unique: function (it) { return it.value.oid; }, dupCode: "tsp/duplicate-extension", code: "tsp/bad-extensions", what: "extensions" }) },
|
|
290
|
+
], { minTag: 0, maxTag: 0, unexpectedCode: "tsp/bad-request", orderCode: "tsp/bad-request" }),
|
|
291
|
+
], {
|
|
292
|
+
assert: "sequence", code: "tsp/bad-request", what: "TimeStampReq",
|
|
293
|
+
build: function (m, ctx) {
|
|
294
|
+
// certReq BOOLEAN DEFAULT FALSE -- an explicit FALSE must be omitted under DER.
|
|
295
|
+
if (m.fields.certReq.present && m.fields.certReq.value === false) {
|
|
296
|
+
throw NS.E("tsp/bad-request", "certReq is BOOLEAN DEFAULT FALSE -- an explicit FALSE must be omitted (DER)");
|
|
297
|
+
}
|
|
298
|
+
var reqPolicy = m.fields.reqPolicy.present ? m.fields.reqPolicy.value : null;
|
|
299
|
+
return {
|
|
300
|
+
version: m.fields.version.value,
|
|
301
|
+
messageImprint: m.fields.messageImprint.value.result,
|
|
302
|
+
reqPolicy: reqPolicy,
|
|
303
|
+
reqPolicyName: reqPolicy ? (ctx.oid.name(reqPolicy) || null) : null,
|
|
304
|
+
nonce: m.fields.nonce.present ? m.fields.nonce.value : null,
|
|
305
|
+
nonceHex: m.fields.nonce.present ? m.fields.nonce.node.content.toString("hex") : null,
|
|
306
|
+
certReq: m.fields.certReq.present ? m.fields.certReq.value : false,
|
|
307
|
+
extensions: m.fields.extensions.present ? m.fields.extensions.value.items.map(function (it) { return it.value; }) : null,
|
|
308
|
+
};
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* @primitive pki.schema.tsp.parseRequest
|
|
314
|
+
* @signature pki.schema.tsp.parseRequest(input) -> timeStampReq
|
|
315
|
+
* @since 0.2.19
|
|
316
|
+
* @status experimental
|
|
317
|
+
* @spec RFC 3161
|
|
318
|
+
* @related pki.schema.tsp.parse, pki.tsp.request
|
|
319
|
+
*
|
|
320
|
+
* Parse a DER `Buffer` or PEM string into a `TimeStampReq` (RFC 3161 sec. 2.4.1):
|
|
321
|
+
* `{ version, messageImprint, reqPolicy, reqPolicyName, nonce, nonceHex, certReq, extensions }`.
|
|
322
|
+
* `version` MUST be 1; `certReq` is BOOLEAN DEFAULT FALSE (an explicit FALSE is non-DER and
|
|
323
|
+
* rejected `tsp/bad-request`); `nonce` is lossless (BigInt + hex); `messageImprint.hashedMessage`
|
|
324
|
+
* is the raw digest. A malformed structure throws a typed `TspError`.
|
|
325
|
+
*
|
|
326
|
+
* @example
|
|
327
|
+
* var req = pki.schema.tsp.parseRequest(der);
|
|
328
|
+
* req.certReq; // -> boolean
|
|
329
|
+
* req.messageImprint.hashedMessage; // -> Buffer (the raw digest)
|
|
330
|
+
*/
|
|
331
|
+
var parseRequest = pkix.makeParser({ pemLabel: null, PemError: PemError, ErrorClass: TspError, prefix: "tsp", what: "TimeStampReq", topSchema: TIME_STAMP_REQ, ns: NS });
|
|
332
|
+
|
|
275
333
|
/**
|
|
276
334
|
* @primitive pki.schema.tsp.parseTstInfo
|
|
277
335
|
* @signature pki.schema.tsp.parseTstInfo(input) -> tstInfo
|
|
@@ -410,6 +468,13 @@ function parseToken(input) {
|
|
|
410
468
|
* @example
|
|
411
469
|
* var der = pki.schema.tsp.pemDecode(pemText);
|
|
412
470
|
*/
|
|
471
|
+
// Decode a raw GeneralName TLV (the tsa field and the ESS issuerSerial issuer are surfaced raw) into
|
|
472
|
+
// { tagClass, tagNumber, value } -- for a directoryName [4] the value is a decoded Name { rdns },
|
|
473
|
+
// so a verifier canonically compares it (guard.name.dnEqual) rather than by raw bytes. @internal.
|
|
474
|
+
function decodeGeneralName(bytes) {
|
|
475
|
+
return schema.embeddedDer(pkix.generalName(NS, { decodeValue: true, code: "tsp/bad-tsa" }), bytes, NS, { code: "tsp/bad-tsa", what: "GeneralName" });
|
|
476
|
+
}
|
|
477
|
+
|
|
413
478
|
function pemDecode(text, label) { return pkix.pemDecode(text, label || null, PemError); }
|
|
414
479
|
|
|
415
480
|
/**
|
|
@@ -445,6 +510,9 @@ function matches(root) {
|
|
|
445
510
|
|
|
446
511
|
module.exports = {
|
|
447
512
|
parse: parse,
|
|
513
|
+
parseResponse: parse, // TimeStampResp parsing == parse; named for symmetry with parseRequest
|
|
514
|
+
parseRequest: parseRequest,
|
|
515
|
+
decodeGeneralName: decodeGeneralName, // @internal -- the verifier's tsa / issuerSerial DN decode
|
|
448
516
|
parseTstInfo: parseTstInfo,
|
|
449
517
|
parseToken: parseToken,
|
|
450
518
|
pemDecode: pemDecode,
|
package/lib/sigstore.js
CHANGED
|
@@ -47,7 +47,7 @@ var NOTE_SIG = new RegExp("^" + String.fromCharCode(0x2014) + " (\\S+) (\\S+)$")
|
|
|
47
47
|
|
|
48
48
|
// Parse untrusted JSON through the bounded, duplicate-member-rejecting guard.
|
|
49
49
|
function _jsonParse(input, code, label) {
|
|
50
|
-
return guard.json.parse(input,
|
|
50
|
+
return guard.json.parse(input, _err, {
|
|
51
51
|
maxBytes: JSON_MAX, maxDepth: 64,
|
|
52
52
|
badJson: code, tooDeep: code, duplicateMember: code, tooLarge: code, badInput: code, label: label,
|
|
53
53
|
});
|
package/lib/tsp-sign.js
CHANGED
|
@@ -18,6 +18,13 @@ var nodeCrypto = require("crypto");
|
|
|
18
18
|
var asn1 = require("./asn1-der");
|
|
19
19
|
var oid = require("./oid");
|
|
20
20
|
var cmsSign = require("./cms-sign");
|
|
21
|
+
var cmsVerify = require("./cms-verify");
|
|
22
|
+
var pathValidate = require("./path-validate");
|
|
23
|
+
var pkiX509 = require("./schema-x509");
|
|
24
|
+
var smime = require("./schema-smime");
|
|
25
|
+
var schemaTsp = require("./schema-tsp");
|
|
26
|
+
var schema = require("./schema-engine");
|
|
27
|
+
var guard = require("./guard-all");
|
|
21
28
|
var frameworkError = require("./framework-error");
|
|
22
29
|
|
|
23
30
|
var TspError = frameworkError.TspError;
|
|
@@ -158,4 +165,519 @@ function _subMilliBytes(n, label) {
|
|
|
158
165
|
// Coverage residual -- `_hashAlgId`'s unsupported-hash throw is unreachable through the shipped
|
|
159
166
|
// path: both callers (the messageImprint hash and the ESSCertIDv2 certHashAlgorithm) validate
|
|
160
167
|
// the name against NODE_DIGEST before `_hashAlgId` runs, so the guard is belt-and-suspenders.
|
|
161
|
-
|
|
168
|
+
|
|
169
|
+
// PKIFailureInfo name -> NamedBitList bit index (RFC 3161 sec. 2.4.2; the reverse of the
|
|
170
|
+
// schema-tsp FAILURE_BITS decode map). Build maps names -> bits; an unknown name fails closed.
|
|
171
|
+
var FAILINFO_BIT = { badAlg: 0, badRequest: 2, badDataFormat: 5, timeNotAvailable: 14, unacceptedPolicy: 15, unacceptedExtension: 16, addInfoNotAvailable: 17, systemFailure: 25 };
|
|
172
|
+
|
|
173
|
+
// Encode a PKIFailureInfo as a minimal DER NamedBitList BIT STRING from a set of names: set each
|
|
174
|
+
// named bit (bit 0 = MSB of byte 0), strip trailing zero octets, and place unusedBits exactly
|
|
175
|
+
// below the lowest set bit (X.690 sec. 11.2.2).
|
|
176
|
+
function _failInfoBits(names) {
|
|
177
|
+
if (!Array.isArray(names)) throw _err("tsp/bad-input", "failInfo must be an array of PKIFailureInfo names");
|
|
178
|
+
var idxs = names.map(function (n) {
|
|
179
|
+
var i = FAILINFO_BIT[n];
|
|
180
|
+
if (i == null) throw _err("tsp/bad-input", "unknown PKIFailureInfo name " + JSON.stringify(n) + " (RFC 3161 sec. 2.4.2)");
|
|
181
|
+
return i;
|
|
182
|
+
});
|
|
183
|
+
if (!idxs.length) return b.bitString(Buffer.alloc(0), 0); // no bits -> empty BIT STRING
|
|
184
|
+
// buf is sized to the highest set bit, so its last octet always carries that bit (non-zero) --
|
|
185
|
+
// there is never a trailing zero octet to strip; only the trailing zero BITS of the last octet
|
|
186
|
+
// are removed, via the DER unusedBits count (X.690 sec. 11.2.2).
|
|
187
|
+
var buf = Buffer.alloc((Math.max.apply(null, idxs) >> 3) + 1);
|
|
188
|
+
idxs.forEach(function (i) { buf[i >> 3] |= 0x80 >> (i & 7); });
|
|
189
|
+
var unused = 0, last = buf[buf.length - 1];
|
|
190
|
+
while (unused < 7 && ((last >> unused) & 1) === 0) unused++; // unusedBits below the lowest set bit
|
|
191
|
+
return b.bitString(buf, unused);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Coerce a timeStampToken (a CMS ContentInfo the token producer emits) to DER for embedding in a
|
|
195
|
+
// TimeStampResp -- a DER Buffer as-is, a Uint8Array copied, or a PEM string de-armored.
|
|
196
|
+
function _tokenDer(t) {
|
|
197
|
+
if (Buffer.isBuffer(t) && t[0] === 0x30) return t;
|
|
198
|
+
if (t instanceof Uint8Array && !Buffer.isBuffer(t)) { var u = Buffer.from(t); if (u[0] === 0x30) return u; }
|
|
199
|
+
if (typeof t === "string") return schemaTsp.pemDecode(t);
|
|
200
|
+
throw _err("tsp/bad-input", "the timeStampToken must be a DER Buffer or a PEM string");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function _assertImprint(mi) {
|
|
204
|
+
if (!mi.hashAlgorithm || !NODE_DIGEST[mi.hashAlgorithm]) throw _err("tsp/unsupported-algorithm", "messageImprint.hashAlgorithm must be a supported hash name");
|
|
205
|
+
if (!Buffer.isBuffer(mi.hashedMessage) && !(mi.hashedMessage instanceof Uint8Array)) throw _err("tsp/bad-input", "messageImprint.hashedMessage must be a Buffer");
|
|
206
|
+
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] + ")");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* @primitive pki.tsp.request
|
|
211
|
+
* @signature pki.tsp.request(messageImprint, opts) -> Buffer|string
|
|
212
|
+
* @since 0.2.19
|
|
213
|
+
* @status experimental
|
|
214
|
+
* @spec RFC 3161
|
|
215
|
+
* @related pki.tsp.parseRequest, pki.tsp.sign
|
|
216
|
+
*
|
|
217
|
+
* Build an RFC 3161 `TimeStampReq` (sec. 2.4.1) over `messageImprint`
|
|
218
|
+
* (`{ hashAlgorithm, hashedMessage }` -- the same shape `pki.tsp.sign` takes). `version` is 1;
|
|
219
|
+
* `certReq` is BOOLEAN DEFAULT FALSE, so it is emitted only when explicitly `true`. Returns DER
|
|
220
|
+
* (or PEM when `opts.pem`).
|
|
221
|
+
*
|
|
222
|
+
* @opts reqPolicy The requested TSA policy (an OID name or dotted string).
|
|
223
|
+
* @opts nonce A large random nonce (number/BigInt) the client checks the token echoes.
|
|
224
|
+
* @opts certReq Whether the TSA should include its certificate in the token (boolean).
|
|
225
|
+
* @opts extensions An array of encoded Extension DER buffers ([0] IMPLICIT Extensions).
|
|
226
|
+
* @opts pem Return a PEM "TIMESTAMP REQUEST" string instead of DER (boolean).
|
|
227
|
+
* @example
|
|
228
|
+
* var req = pki.tsp.request({ hashAlgorithm: "sha256", hashedMessage: sha256Digest }, { nonce: 0x0102030405060708n, certReq: true });
|
|
229
|
+
*/
|
|
230
|
+
function request(messageImprint, opts) {
|
|
231
|
+
opts = opts || {};
|
|
232
|
+
if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw _err("tsp/bad-input", "pki.tsp.request options must be an object");
|
|
233
|
+
var mi = messageImprint || {};
|
|
234
|
+
_assertImprint(mi);
|
|
235
|
+
var imprint = b.sequence([_hashAlgId(mi.hashAlgorithm), b.octetString(Buffer.from(mi.hashedMessage))]);
|
|
236
|
+
// TimeStampReq: version(1), messageImprint, then OPTIONAL reqPolicy / nonce / certReq /
|
|
237
|
+
// extensions in schema order. certReq DEFAULT FALSE -> only an explicit TRUE is encoded (DER).
|
|
238
|
+
var fields = [b.integer(1n), imprint];
|
|
239
|
+
if (opts.reqPolicy != null) fields.push(_policy(opts.reqPolicy));
|
|
240
|
+
if (opts.nonce != null) fields.push(b.integer(BigInt(opts.nonce)));
|
|
241
|
+
if (opts.certReq != null && typeof opts.certReq !== "boolean") throw _err("tsp/bad-input", "certReq must be a boolean");
|
|
242
|
+
if (opts.certReq === true) fields.push(b.boolean(true));
|
|
243
|
+
if (opts.extensions != null) {
|
|
244
|
+
if (!Array.isArray(opts.extensions) || !opts.extensions.every(function (e) { return Buffer.isBuffer(e) || e instanceof Uint8Array; })) throw _err("tsp/bad-input", "extensions must be an array of encoded Extension DER buffers");
|
|
245
|
+
if (opts.extensions.length) fields.push(b.contextConstructed(0, Buffer.concat(opts.extensions.map(function (e) { return Buffer.from(e); }))));
|
|
246
|
+
}
|
|
247
|
+
var der = b.sequence(fields);
|
|
248
|
+
return opts.pem ? schemaTsp.pemEncode(der, "TIMESTAMP REQUEST") : der;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* @primitive pki.tsp.parseRequest
|
|
253
|
+
* @signature pki.tsp.parseRequest(input) -> timeStampReq
|
|
254
|
+
* @since 0.2.19
|
|
255
|
+
* @status experimental
|
|
256
|
+
* @spec RFC 3161
|
|
257
|
+
* @related pki.tsp.request, pki.schema.tsp.parseRequest
|
|
258
|
+
*
|
|
259
|
+
* Parse a `TimeStampReq` (DER `Buffer` or PEM) -- the `pki.schema.tsp.parseRequest` decoder on the
|
|
260
|
+
* `pki.tsp` namespace. Returns `{ version, messageImprint, reqPolicy, reqPolicyName, nonce,
|
|
261
|
+
* nonceHex, certReq, extensions }`; a malformed structure throws a typed `TspError`.
|
|
262
|
+
*
|
|
263
|
+
* @example
|
|
264
|
+
* var req = pki.tsp.parseRequest(der);
|
|
265
|
+
* req.certReq; // -> boolean
|
|
266
|
+
*/
|
|
267
|
+
function parseRequest(input) { return schemaTsp.parseRequest(input); }
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* @primitive pki.tsp.response
|
|
271
|
+
* @signature pki.tsp.response(token, opts) -> Buffer|string
|
|
272
|
+
* @since 0.2.19
|
|
273
|
+
* @status experimental
|
|
274
|
+
* @spec RFC 3161
|
|
275
|
+
* @related pki.tsp.parseResponse, pki.tsp.sign
|
|
276
|
+
*
|
|
277
|
+
* Build an RFC 3161 `TimeStampResp` (sec. 2.4.2): a `PKIStatusInfo` and, on success, the
|
|
278
|
+
* `timeStampToken`. Pass a `token` (the CMS ContentInfo `pki.tsp.sign` produces) with the default
|
|
279
|
+
* granted status, or build a rejection with `response(null, { status, failInfo, statusString })`.
|
|
280
|
+
* The status-to-token coupling is enforced (granted 0/1 carries a token, any other status must not),
|
|
281
|
+
* mirroring the parse-side gate. Returns DER (or PEM when `opts.pem`).
|
|
282
|
+
*
|
|
283
|
+
* @opts status PKIStatus 0..5 (default 0 granted). granted(0)/grantedWithMods(1) carry a token.
|
|
284
|
+
* @opts failInfo Array of PKIFailureInfo names (only on rejection(2)): e.g. ["badAlg"].
|
|
285
|
+
* @opts statusString Human-readable PKIFreeText (string or array of strings).
|
|
286
|
+
* @opts pem Return a PEM "TIMESTAMP RESPONSE" string instead of DER (boolean).
|
|
287
|
+
* @example
|
|
288
|
+
* var resp = pki.tsp.response(token, {}); // granted
|
|
289
|
+
* var rej = pki.tsp.response(null, { status: 2, failInfo: ["badAlg"] }); // rejection
|
|
290
|
+
*/
|
|
291
|
+
function response(token, opts) {
|
|
292
|
+
opts = opts || {};
|
|
293
|
+
if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw _err("tsp/bad-input", "pki.tsp.response options must be an object");
|
|
294
|
+
var status = opts.status == null ? 0 : Number(opts.status);
|
|
295
|
+
if (!Number.isInteger(status) || status < 0 || status > 5) throw _err("tsp/bad-input", "PKIStatus must be an integer in 0..5 (RFC 3161 sec. 2.4.2)");
|
|
296
|
+
var granted = status === 0 || status === 1;
|
|
297
|
+
if (granted && token == null) throw _err("tsp/missing-token", "a granted TimeStampResp requires a timeStampToken (RFC 3161 sec. 2.4.2)");
|
|
298
|
+
if (!granted && token != null) throw _err("tsp/unexpected-token", "a non-granted TimeStampResp must not carry a timeStampToken (RFC 3161 sec. 2.4.2)");
|
|
299
|
+
var siFields = [b.integer(BigInt(status))];
|
|
300
|
+
if (opts.statusString != null) {
|
|
301
|
+
var texts = Array.isArray(opts.statusString) ? opts.statusString : [opts.statusString];
|
|
302
|
+
// PKIFreeText is SEQUENCE SIZE (1..MAX) OF UTF8String -- an empty statusString would build an
|
|
303
|
+
// empty SEQUENCE the parser (seqOf min:1) rejects, so reject it at the builder instead.
|
|
304
|
+
if (!texts.length) throw _err("tsp/bad-input", "statusString must carry at least one PKIFreeText element (RFC 4210 SEQUENCE SIZE 1..MAX)");
|
|
305
|
+
siFields.push(b.sequence(texts.map(function (t) { return b.utf8(String(t)); })));
|
|
306
|
+
}
|
|
307
|
+
if (opts.failInfo != null) {
|
|
308
|
+
if (status !== 2) throw _err("tsp/unexpected-failinfo", "failInfo is present only when the status is rejection(2) (RFC 3161 sec. 2.4.2)");
|
|
309
|
+
siFields.push(_failInfoBits(opts.failInfo));
|
|
310
|
+
}
|
|
311
|
+
var respFields = [b.sequence(siFields)];
|
|
312
|
+
if (token != null) respFields.push(b.raw(_tokenDer(token)));
|
|
313
|
+
var der = b.sequence(respFields);
|
|
314
|
+
return opts.pem ? schemaTsp.pemEncode(der, "TIMESTAMP RESPONSE") : der;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* @primitive pki.tsp.parseResponse
|
|
319
|
+
* @signature pki.tsp.parseResponse(input) -> timeStampResp
|
|
320
|
+
* @since 0.2.19
|
|
321
|
+
* @status experimental
|
|
322
|
+
* @spec RFC 3161
|
|
323
|
+
* @related pki.tsp.response, pki.schema.tsp.parse
|
|
324
|
+
*
|
|
325
|
+
* Parse a `TimeStampResp` (DER `Buffer` or PEM) -- the shipped `pki.schema.tsp.parse` decoder on
|
|
326
|
+
* the `pki.tsp` namespace. Returns `{ status, statusString, failInfo, timeStampToken }` with the
|
|
327
|
+
* status-to-token coupling enforced; a granted response's token is decoded via `parseToken`.
|
|
328
|
+
*
|
|
329
|
+
* @example
|
|
330
|
+
* var resp = pki.tsp.parseResponse(der);
|
|
331
|
+
* resp.timeStampToken.tstInfo.genTime; // -> Date (on a granted response)
|
|
332
|
+
*/
|
|
333
|
+
function parseResponse(input) { return schemaTsp.parse(input); }
|
|
334
|
+
|
|
335
|
+
// Coverage residual -- the null-guards and decode/parse catches in the verify helpers below are
|
|
336
|
+
// belt-and-suspenders on values already guaranteed upstream, so their failure arms are unreachable
|
|
337
|
+
// through the shipped path: parseToken has already proven the token is a single-signer SignedData
|
|
338
|
+
// carrying a signing-certificate attribute, so `signerInfo`/`signedAttrs`/the signing-cert attr are
|
|
339
|
+
// present; cms.verify returns a parsed, matched signer certificate, so `tsaCertDer` is set and
|
|
340
|
+
// re-parsing it (or decoding its extensions) cannot fail here; decodeAttribute yields at least one
|
|
341
|
+
// ESSCertID (an empty SEQUENCE OF is rejected at decode -> the `!essCert` guard is defensive); and
|
|
342
|
+
// path.validate returns a fail-closed verdict for a bad anchor rather than throwing, so its catch
|
|
343
|
+
// is defensive. Likewise, in the out-of-path chain step, `parsed.certificates` is always an array
|
|
344
|
+
// (empty when absent), the embedded certs the chain walk parses are the X.509 (universal) choices,
|
|
345
|
+
// and guard.name.dnEqual's control-byte reject is defensive on a cert x509.parse already accepted --
|
|
346
|
+
// so the `|| []`, the non-universal filter arm, and that throw are unreachable through the shipped
|
|
347
|
+
// path. In the tsa-hint check, the tsa GeneralName's decode catch is likewise defensive: parseToken
|
|
348
|
+
// eager-decodes the token, so a constructed GeneralName that would fail decodeGeneralName has already
|
|
349
|
+
// failed there, and a primitive one decodes -- the catch only guards decodeGeneralName's standalone
|
|
350
|
+
// use and yields the same safe no-match. A malformed ESS value, a malformed EKU/keyUsage extnValue,
|
|
351
|
+
// and a malformed subjectAltName ARE reachable (a signed-but-broken cert/attribute) and ARE covered;
|
|
352
|
+
// the reachable verdicts (unsupported hash, EKU shape/wrapper, keyUsage, imprint/nonce/policy/binding
|
|
353
|
+
// mismatch, tsa-hint match + mismatch, unknown critical extension, untrusted TSA, embedded-chain
|
|
354
|
+
// ordering) are each driven by a RED conformance vector.
|
|
355
|
+
|
|
356
|
+
// The imprint / ESSCertID hash algorithms a verifier can recompute: the SHA-2 family, plus SHA-1
|
|
357
|
+
// for MATCHING a legacy token's imprint (a verify-side digest, never a signing choice). An unknown
|
|
358
|
+
// or weak (e.g. MD5) algorithm cannot be recomputed, so the check fails closed rather than assume.
|
|
359
|
+
var _VERIFY_DIGEST = { sha256: "sha256", sha384: "sha384", sha512: "sha512", sha1: "sha1" };
|
|
360
|
+
// The digest output length each verify hash MUST produce: a messageImprint or precomputed imprint
|
|
361
|
+
// of any other length is not a real digest and can never correspond to data (RFC 3161 sec. 2.4.1).
|
|
362
|
+
var _VERIFY_DIGEST_LEN = { sha1: 20, sha256: 32, sha384: 48, sha512: 64 };
|
|
363
|
+
// The ESSCertID(V2) certificate binding is a pure identity pin, so its digest MUST be
|
|
364
|
+
// collision-resistant: a SHA-1 certHash would let a chosen-prefix collision substitute the TSA
|
|
365
|
+
// certificate. RFC 5816 moved this binding from the SHA-1 ESSCertID to the SHA-2 ESSCertIDv2 for
|
|
366
|
+
// exactly this reason -- accept only SHA-2 here (the messageImprint stays algorithm-agile, since
|
|
367
|
+
// that hash is the requester's documented RFC 3161 choice, not an identity pin).
|
|
368
|
+
var _BIND_DIGEST = { sha256: "sha256", sha384: "sha384", sha512: "sha512" };
|
|
369
|
+
|
|
370
|
+
// M10 -- recompute the imprint over `data` under the token's messageImprint hash algorithm (read
|
|
371
|
+
// from the verified eContent) and compare to hashedMessage. `data` is raw bytes (hashed) or a
|
|
372
|
+
// precomputed { hashAlgorithm, hashedMessage } (algorithm-exact + byte-exact). Returns true or a
|
|
373
|
+
// tsp/* verdict code; a config-shape error throws (the caller's contract).
|
|
374
|
+
function _imprintMatches(mi, data) {
|
|
375
|
+
var name = mi.hashAlgorithm && mi.hashAlgorithm.name;
|
|
376
|
+
var nodeAlg = name && _VERIFY_DIGEST[name];
|
|
377
|
+
if (!nodeAlg) return "tsp/unsupported-algorithm"; // cannot recompute -> never assume a match
|
|
378
|
+
var wantLen = _VERIFY_DIGEST_LEN[name];
|
|
379
|
+
// The token's messageImprint MUST be a full-length digest for its algorithm. A short/empty
|
|
380
|
+
// hashedMessage is not a hash and cannot correspond to any data, so it fails closed here -- a
|
|
381
|
+
// matching short precomputed imprint must never make it verify (RFC 3161 sec. 2.4.1).
|
|
382
|
+
if (mi.hashedMessage.length !== wantLen) return "tsp/imprint-mismatch";
|
|
383
|
+
var actual;
|
|
384
|
+
if (Buffer.isBuffer(data) || data instanceof Uint8Array) {
|
|
385
|
+
actual = nodeCrypto.createHash(nodeAlg).update(Buffer.from(data)).digest();
|
|
386
|
+
} else if (data && typeof data === "object" && (Buffer.isBuffer(data.hashedMessage) || data.hashedMessage instanceof Uint8Array)) {
|
|
387
|
+
if (data.hashAlgorithm !== name) return "tsp/imprint-mismatch"; // a precomputed imprint under a different algorithm
|
|
388
|
+
// a caller-supplied precomputed imprint of the wrong length is a config error (the sign path
|
|
389
|
+
// rejects it identically) -- throw rather than silently mismatch (three-tier: caller input).
|
|
390
|
+
if (data.hashedMessage.length !== wantLen) throw _err("tsp/bad-input", "precomputed imprint hashedMessage length (" + data.hashedMessage.length + ") does not match the " + name + " digest length (" + wantLen + ")");
|
|
391
|
+
actual = Buffer.from(data.hashedMessage);
|
|
392
|
+
} else {
|
|
393
|
+
throw _err("tsp/bad-input", "pki.tsp.verify data must be a Buffer or a { hashAlgorithm, hashedMessage } imprint");
|
|
394
|
+
}
|
|
395
|
+
return Buffer.compare(actual, Buffer.from(mi.hashedMessage)) === 0 ? true : "tsp/imprint-mismatch";
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// M13 -- the token MUST be bound to the exact signer certificate via the ESS SigningCertificate(V2)
|
|
399
|
+
// signed attribute (RFC 3161 sec. 2.4.2 / RFC 5816): recompute hash(tsaCertDer) under the
|
|
400
|
+
// ESSCertID(V2) hashAlgorithm (v1 implies SHA-1, v2 defaults SHA-256) and compare to certHash.
|
|
401
|
+
function _checkCertBinding(signerInfo, tsaCertDer) {
|
|
402
|
+
var attrs = (signerInfo && signerInfo.signedAttrs) || [];
|
|
403
|
+
// Prefer the RFC 5816 SigningCertificateV2 (SHA-2) binding over the legacy RFC 2634
|
|
404
|
+
// SigningCertificate (SHA-1). A token may carry both for compatibility, and the SHA-1 attribute
|
|
405
|
+
// sorts first in the signed-attribute SET; picking it would fail the SHA-2-only binding pin even
|
|
406
|
+
// though a usable v2 binding is present. Fall back to the legacy attribute only when no v2 exists.
|
|
407
|
+
var sc = null, scV1 = null;
|
|
408
|
+
for (var i = 0; i < attrs.length; i++) {
|
|
409
|
+
if (attrs[i].type === O("signingCertificateV2")) { sc = attrs[i]; break; }
|
|
410
|
+
if (attrs[i].type === O("signingCertificate") && !scV1) scV1 = attrs[i];
|
|
411
|
+
}
|
|
412
|
+
if (!sc) sc = scV1;
|
|
413
|
+
if (!sc) return "tsp/missing-signing-certificate"; // parseToken guarantees presence; belt-and-suspenders
|
|
414
|
+
var decoded;
|
|
415
|
+
try { decoded = smime.decodeAttribute(sc); }
|
|
416
|
+
catch (_e) { return "tsp/bad-signing-certificate"; }
|
|
417
|
+
var essCert = decoded.certs && decoded.certs[0];
|
|
418
|
+
if (!essCert) return "tsp/bad-signing-certificate";
|
|
419
|
+
var nodeAlg = essCert.hashAlgorithm && _BIND_DIGEST[essCert.hashAlgorithm.name];
|
|
420
|
+
if (!nodeAlg) return "tsp/unsupported-algorithm"; // SHA-1 (or an unknown hash) is not a valid identity pin
|
|
421
|
+
var actual = nodeCrypto.createHash(nodeAlg).update(tsaCertDer).digest();
|
|
422
|
+
if (Buffer.compare(actual, Buffer.from(essCert.certHash)) !== 0) return "tsp/cert-binding-mismatch";
|
|
423
|
+
// RFC 5035 sec. 5: if the ESSCertID(V2) carries the OPTIONAL issuerSerial, it MUST identify the
|
|
424
|
+
// signer certificate -- confirm its serialNumber. The certHash already pins the exact cert
|
|
425
|
+
// cryptographically; the serialNumber is that cert's unambiguous identity field.
|
|
426
|
+
if (essCert.issuerSerial) {
|
|
427
|
+
var signerCert;
|
|
428
|
+
try { signerCert = pkiX509.parse(tsaCertDer); }
|
|
429
|
+
catch (_e) { return "tsp/bad-signing-certificate"; }
|
|
430
|
+
if (essCert.issuerSerial.serialNumber !== signerCert.serialNumber) return "tsp/cert-binding-mismatch";
|
|
431
|
+
// the issuerSerial issuer is a single directoryName (RFC 5035 sec. 5) that MUST canonically
|
|
432
|
+
// equal the signer cert's issuer DN -- decode it and compare through the shared name guard.
|
|
433
|
+
var names = essCert.issuerSerial.issuer.names || [];
|
|
434
|
+
var gn = null;
|
|
435
|
+
try { if (names.length === 1) gn = schemaTsp.decodeGeneralName(names[0].bytes); }
|
|
436
|
+
catch (_e) { return "tsp/cert-binding-mismatch"; }
|
|
437
|
+
if (!gn || gn.tagNumber !== 4 || !gn.value || !guard.name.dnEqual(gn.value.rdns, signerCert.issuer.rdns, _err, "tsp/cert-binding-mismatch", "the ESSCertID issuer")) {
|
|
438
|
+
return "tsp/cert-binding-mismatch";
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// M11 -- RFC 3161 sec. 2.3: the TSA certificate MUST contain exactly ONE extendedKeyUsage instance
|
|
445
|
+
// whose sole KeyPurposeID is id-kp-timeStamping, and that extension MUST be CRITICAL. path.validate's
|
|
446
|
+
// requiredEku only checks the purpose is ASSERTED (an absent EKU is unrestricted), so this
|
|
447
|
+
// single-purpose + critical gate is layered on top -- the OCSP-delegate precedent for a format gate
|
|
448
|
+
// above the path checks. Additionally (RFC 5280 sec. 4.2.1.3) a keyUsage extension, if present, MUST
|
|
449
|
+
// permit signing for the cert to sign the token. Returns true or a tsp/* code.
|
|
450
|
+
function _checkTsaCertUsage(tsaCertDer) {
|
|
451
|
+
var cert;
|
|
452
|
+
try { cert = pkiX509.parse(tsaCertDer); }
|
|
453
|
+
catch (_e) { return "tsp/bad-tsa-certificate"; }
|
|
454
|
+
var exts = (cert.extensions || []).filter(function (e) { return e.oid === O("extKeyUsage"); });
|
|
455
|
+
if (exts.length !== 1) return "tsp/bad-eku"; // absent, or a duplicate extension (RFC 5280 -- one instance)
|
|
456
|
+
if (!exts[0].critical) return "tsp/eku-not-critical";
|
|
457
|
+
var purposes, ekuNode;
|
|
458
|
+
try { ekuNode = asn1.decode(exts[0].value); }
|
|
459
|
+
catch (_e) { return "tsp/bad-eku"; }
|
|
460
|
+
// the extnValue MUST be a universal SEQUENCE OF KeyPurposeId -- a non-SEQUENCE constructed wrapper
|
|
461
|
+
// (e.g. a SET) carrying OID children would otherwise slip through the bare children walk.
|
|
462
|
+
if (ekuNode.tagClass !== "universal" || ekuNode.tagNumber !== asn1.TAGS.SEQUENCE || !ekuNode.children) return "tsp/bad-eku";
|
|
463
|
+
try { purposes = ekuNode.children.map(function (c) { return asn1.read.oid(c); }); }
|
|
464
|
+
catch (_e) { return "tsp/bad-eku"; }
|
|
465
|
+
if (purposes.length !== 1 || purposes[0] !== O("timeStamping")) return "tsp/eku-not-exclusive";
|
|
466
|
+
// A keyUsage that forbids signing cannot mint a token (RFC 5280 sec. 4.2.1.3); an absent keyUsage
|
|
467
|
+
// is unrestricted. Require digitalSignature (bit 0) or nonRepudiation/contentCommitment (bit 1) --
|
|
468
|
+
// the signing bits, the TSA analogue of the OCSP-responder keyUsage gate.
|
|
469
|
+
var kuExts = (cert.extensions || []).filter(function (e) { return e.oid === O("keyUsage"); });
|
|
470
|
+
if (kuExts.length) {
|
|
471
|
+
var ku;
|
|
472
|
+
try {
|
|
473
|
+
ku = asn1.read.bitString(asn1.decode(kuExts[0].value));
|
|
474
|
+
// KeyUsage is a NamedBitList: enforce the X.690 sec. 11.2.2 minimal-DER rule (no trailing zero
|
|
475
|
+
// bits) that the shared certExtensionDecoders keyUsage decoder applies, so a non-minimal
|
|
476
|
+
// encoding other paths reject cannot slip a "permits signing" verdict through here.
|
|
477
|
+
schema.assertMinimalNamedBits(ku.unusedBits, ku.bytes, function (m) { throw _err("tsp/bad-key-usage", m); });
|
|
478
|
+
} catch (_e) { return "tsp/bad-key-usage"; }
|
|
479
|
+
var byte0 = ku.bytes.length ? ku.bytes[0] : 0;
|
|
480
|
+
if (!((byte0 >> 7) & 1) && !((byte0 >> 6) & 1)) return "tsp/bad-key-usage"; // no digitalSignature / nonRepudiation
|
|
481
|
+
}
|
|
482
|
+
return true;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// RFC 5816 sec. 2.2.2: if TSTInfo.tsa is present, it is a hint that MUST correspond to one of the
|
|
486
|
+
// signing certificate's subject names -- its subject DN (a directoryName, compared canonically) or a
|
|
487
|
+
// subjectAltName entry (a byte-exact GeneralName match). `tsaBytes` is the raw tsa GeneralName TLV.
|
|
488
|
+
function _tsaMatchesCert(tsaBytes, signerCert) {
|
|
489
|
+
var gn;
|
|
490
|
+
try { gn = schemaTsp.decodeGeneralName(tsaBytes); }
|
|
491
|
+
catch (_e) { return false; }
|
|
492
|
+
if (gn.tagNumber === 4 && gn.value && guard.name.dnEqual(gn.value.rdns, signerCert.subject.rdns, _err, "tsp/tsa-mismatch", "the TSA subject")) return true;
|
|
493
|
+
var sanExt = (signerCert.extensions || []).filter(function (e) { return e.oid === O("subjectAltName"); })[0];
|
|
494
|
+
if (sanExt) {
|
|
495
|
+
var san;
|
|
496
|
+
try { san = asn1.decode(sanExt.value); }
|
|
497
|
+
catch (_e) { return false; }
|
|
498
|
+
// subjectAltName MUST be a universal SEQUENCE OF GeneralName -- a malformed wrapper (e.g. a SET)
|
|
499
|
+
// carrying a byte-equal child must not contribute an identity (mirrors the EKU wrapper gate).
|
|
500
|
+
if (san.tagClass === "universal" && san.tagNumber === asn1.TAGS.SEQUENCE && san.children) {
|
|
501
|
+
for (var i = 0; i < san.children.length; i++) {
|
|
502
|
+
if (san.children[i].bytes.equals(tsaBytes)) return true;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Bound on the candidate certification paths enumerated from a token's certificate pool -- a guard
|
|
510
|
+
// against a hostile embed of many same-subject certificates blowing up the search (CWE-834).
|
|
511
|
+
var MAX_TSA_CHAINS = 32;
|
|
512
|
+
|
|
513
|
+
// Enumerate candidate certification paths for the TSA leaf from a pool of embedded and caller-supplied
|
|
514
|
+
// certificates. path.validate validates a FIXED ordered path (it is not a path builder), so the
|
|
515
|
+
// ordering + backtracking lives here: at every step EVERY unused pool certificate whose subject DN
|
|
516
|
+
// equals the working issuer DN is a candidate issuer -- CA rollover / cross-certification embed
|
|
517
|
+
// several with the same subject, and a same-subject wrong-key certificate must not shadow the real
|
|
518
|
+
// issuer -- and each accumulated prefix is itself a candidate (the anchor may have issued the leaf
|
|
519
|
+
// directly or any intermediate). A certificate is never chained to itself (RFC 5280 identity is
|
|
520
|
+
// issuer + serialNumber, never the serial alone). Each chain is returned anchor-down (highest issuer
|
|
521
|
+
// first, the leaf last) as path.validate takes it; longer chains first so a fully-embedded path is
|
|
522
|
+
// preferred. Bounded by MAX_TSA_CHAINS. The caller (verify) tries each until one validates.
|
|
523
|
+
function _buildTsaChains(leaf, pool) {
|
|
524
|
+
var chains = [];
|
|
525
|
+
function dfs(current, acc, used) {
|
|
526
|
+
if (chains.length >= MAX_TSA_CHAINS) return;
|
|
527
|
+
chains.push(acc.slice()); // acc is leaf..current; the anchor may validate the path at this length
|
|
528
|
+
for (var i = 0; i < pool.length && chains.length < MAX_TSA_CHAINS; i++) {
|
|
529
|
+
if (used[i]) continue;
|
|
530
|
+
// never chain a certificate to itself (issuer + serialNumber identity, not the serial alone).
|
|
531
|
+
if (pool[i].serialNumberHex === current.serialNumberHex && guard.name.dnEqual(pool[i].issuer.rdns, current.issuer.rdns, _err, "tsp/bad-tsa-certificate", "the TSA certificate issuer")) continue;
|
|
532
|
+
if (guard.name.dnEqual(pool[i].subject.rdns, current.issuer.rdns, _err, "tsp/bad-tsa-certificate", "the TSA certificate subject")) {
|
|
533
|
+
used[i] = true; acc.push(pool[i]);
|
|
534
|
+
dfs(pool[i], acc, used);
|
|
535
|
+
acc.pop(); used[i] = false;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
dfs(leaf, [leaf], []);
|
|
540
|
+
return chains.map(function (c) { return c.slice().reverse(); }).sort(function (a, b) { return b.length - a.length; });
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* @primitive pki.tsp.verify
|
|
545
|
+
* @signature pki.tsp.verify(token, data, opts) -> Promise<result>
|
|
546
|
+
* @since 0.2.19
|
|
547
|
+
* @status experimental
|
|
548
|
+
* @spec RFC 3161, RFC 5816
|
|
549
|
+
* @related pki.tsp.sign, pki.cms.verify, pki.path.validate
|
|
550
|
+
*
|
|
551
|
+
* Verify an RFC 3161 TimeStampToken against the data it should cover. `token` is the token DER /
|
|
552
|
+
* PEM (never a parsed object -- every checked field is read from the CMS-verified eContent, so a
|
|
553
|
+
* mutated parsed structure cannot desynchronize the checks from the signed bytes). `data` is the
|
|
554
|
+
* original bytes (hashed under the token's messageImprint algorithm) or a precomputed
|
|
555
|
+
* `{ hashAlgorithm, hashedMessage }`. Returns `{ valid, genTime, accuracy, serialNumber,
|
|
556
|
+
* serialNumberHex, policy, nonce, tsa, tstInfo, signer, code?, reason? }`. `valid` is true only
|
|
557
|
+
* when the CMS signature, the imprint match, the eContentType, the ESSCertID(V2) binding, the
|
|
558
|
+
* RFC 3161 sec. 2.3 critical single-`timeStamping` extendedKeyUsage rule, the requested nonce (when
|
|
559
|
+
* supplied), and -- when a `trustAnchor` is supplied -- the full out-of-path TSA-cert path
|
|
560
|
+
* validation all pass. A conformance / trust failure of a well-formed token is a
|
|
561
|
+
* `{ valid:false, code }` verdict; malformed or config input throws a typed `TspError`.
|
|
562
|
+
*
|
|
563
|
+
* @opts trustAnchor Anchor `{ name, publicKey, algorithm }` -- runs `pki.path.validate` on the
|
|
564
|
+
* TSA certificate chain ordered from the token's embedded certificates
|
|
565
|
+
* (validity at genTime, requiredEku timeStamping, revocation), so a TSA under
|
|
566
|
+
* an intermediate CA validates, not only one directly under the anchor. Omit
|
|
567
|
+
* to verify signature + imprint + binding + EKU only and anchor the cert yourself.
|
|
568
|
+
* @opts nonce Require the token's TSTInfo.nonce to equal this (a number/BigInt).
|
|
569
|
+
* @opts reqPolicy Require the token's policy to equal this (an OID name or dotted string).
|
|
570
|
+
* @opts certs Out-of-band TSA certificates (an array of DER `Buffer`s) added to the signer
|
|
571
|
+
* candidates and the path pool, so a cert-less token (the TSA omits its
|
|
572
|
+
* certificate when `certReq` was false) and an intermediate absent from the
|
|
573
|
+
* token can still verify and chain.
|
|
574
|
+
* @opts revocationChecker Passed through to `pki.path.validate`.
|
|
575
|
+
* @example
|
|
576
|
+
* var imprint = { hashAlgorithm: "sha256", hashedMessage: sha256Digest };
|
|
577
|
+
* var token = await pki.tsp.sign(imprint, { cert: signerCertDer, key: signerKeyPkcs8 }, { policy: "1.3.6.1.4.1.1", serialNumber: 1 });
|
|
578
|
+
* var res = await pki.tsp.verify(token, Buffer.from("hello"), {});
|
|
579
|
+
* res.valid; // boolean; pass opts.trustAnchor to also chain the TSA cert to a root
|
|
580
|
+
* res.genTime; // Date, read from the verified eContent
|
|
581
|
+
*/
|
|
582
|
+
async function verify(token, data, opts) {
|
|
583
|
+
opts = opts || {};
|
|
584
|
+
if (typeof opts !== "object" || Buffer.isBuffer(opts)) throw _err("tsp/bad-input", "pki.tsp.verify options must be an object");
|
|
585
|
+
if (opts.certs != null && (!Array.isArray(opts.certs) || !opts.certs.every(function (c) { return Buffer.isBuffer(c) || c instanceof Uint8Array; }))) {
|
|
586
|
+
throw _err("tsp/bad-input", "pki.tsp.verify opts.certs must be an array of DER certificate Buffers"); // a bad element is a caller error, never silently dropped
|
|
587
|
+
}
|
|
588
|
+
var tokenDer = _tokenDer(token); // DER Buffer / PEM -> DER; a non-token input throws tsp/bad-input
|
|
589
|
+
// M7 -- parseToken enforces the SignedData + id-ct-TSTInfo + single-signer + signing-cert-present
|
|
590
|
+
// structure and decodes the TSTInfo FROM the raw eContent; a structural defect throws.
|
|
591
|
+
var parsed = schemaTsp.parseToken(tokenDer);
|
|
592
|
+
var tst = parsed.tstInfo;
|
|
593
|
+
function fail(code, reason) {
|
|
594
|
+
return { valid: false, code: code, reason: reason || null, genTime: tst.genTime, accuracy: tst.accuracy, serialNumber: tst.serialNumber, serialNumberHex: tst.serialNumberHex, policy: tst.policy, nonce: tst.nonce, tsa: tst.tsa, tstInfo: tst, signer: null };
|
|
595
|
+
}
|
|
596
|
+
// M12 -- the CMS signature over the exact RFC 5652 sec. 5.4 preimage (message-digest bound to the
|
|
597
|
+
// authenticated eContent). cms.verify re-parses the same bytes; a failure is a fail-closed verdict.
|
|
598
|
+
// Caller-supplied out-of-band certificates (opts.certs) join the signer-certificate candidates, so
|
|
599
|
+
// a cert-less token (RFC 3161 -- the TSA omits its certificate when certReq was false) can verify.
|
|
600
|
+
var cmsRes = await cmsVerify.verify(tokenDer, { certs: opts.certs });
|
|
601
|
+
var signer = cmsRes.signers[0];
|
|
602
|
+
if (!cmsRes.valid) return fail("tsp/bad-signature", signer && (signer.code || signer.message));
|
|
603
|
+
var tsaCertDer = signer && signer.cert;
|
|
604
|
+
if (!tsaCertDer) return fail("tsp/bad-signature", "the TSA signer certificate was not found");
|
|
605
|
+
// M10 -- what was timestamped MUST correspond to the data (imprint recompute + match).
|
|
606
|
+
var mi = _imprintMatches(tst.messageImprint, data);
|
|
607
|
+
if (mi !== true) return fail(mi);
|
|
608
|
+
// M14 -- if a request nonce is supplied, the token MUST echo it (BigInt-exact).
|
|
609
|
+
if (opts.nonce != null) {
|
|
610
|
+
var wantNonce = BigInt(opts.nonce);
|
|
611
|
+
if (tst.nonce == null || tst.nonce !== wantNonce) return fail("tsp/nonce-mismatch");
|
|
612
|
+
}
|
|
613
|
+
// M15 -- if the requested policy is supplied, the token's policy MUST equal it.
|
|
614
|
+
if (opts.reqPolicy != null) {
|
|
615
|
+
var wantPolicy = /^\d+(\.\d+)+$/.test(opts.reqPolicy) ? opts.reqPolicy : O(opts.reqPolicy);
|
|
616
|
+
if (tst.policy !== wantPolicy) return fail("tsp/policy-mismatch");
|
|
617
|
+
}
|
|
618
|
+
// A TSTInfo carrying a CRITICAL extension the verifier cannot process is unusable: RFC 3161 sec.
|
|
619
|
+
// 2.4.2 defers extension handling to RFC 5280 sec. 4.2, and defines no TSTInfo extension, so any
|
|
620
|
+
// critical one is unrecognized (the unknown-critical-extension gate the cert path also applies).
|
|
621
|
+
if (tst.extensions && tst.extensions.some(function (e) { return e.critical; })) {
|
|
622
|
+
return fail("tsp/unknown-critical-extension");
|
|
623
|
+
}
|
|
624
|
+
// M13 -- the ESSCertID(V2) binding: the verifying cert MUST be the one the signer identified.
|
|
625
|
+
var bind = _checkCertBinding(parsed.signerInfo, tsaCertDer);
|
|
626
|
+
if (bind !== true) return fail(bind);
|
|
627
|
+
// M11 -- the RFC 3161 sec. 2.3 critical single-timeStamping EKU gate + the keyUsage-permits-signing check.
|
|
628
|
+
var usage = _checkTsaCertUsage(tsaCertDer);
|
|
629
|
+
if (usage !== true) return fail(usage);
|
|
630
|
+
// RFC 5816 sec. 2.2.2 -- the tsa hint, when present, MUST name the signer certificate.
|
|
631
|
+
if (tst.tsa) {
|
|
632
|
+
var sc;
|
|
633
|
+
try { sc = pkiX509.parse(tsaCertDer); }
|
|
634
|
+
catch (_e) { return fail("tsp/bad-tsa-certificate"); }
|
|
635
|
+
if (!_tsaMatchesCert(tst.tsa.bytes, sc)) return fail("tsp/tsa-mismatch");
|
|
636
|
+
}
|
|
637
|
+
// M18 -- full out-of-path TSA-cert validation (issuer-sig, validity at genTime, unknown-critical,
|
|
638
|
+
// key-param inheritance, requiredEku, optional revocation), only when a trustAnchor is supplied.
|
|
639
|
+
// The path is ordered from the token's embedded certificates (leaf + any intermediates), so a TSA
|
|
640
|
+
// issued under an intermediate CA -- not just directly under the anchor -- validates.
|
|
641
|
+
if (opts.trustAnchor) {
|
|
642
|
+
var pathRes = null;
|
|
643
|
+
// tst.genTime floors to millisecond precision. When genTime carries sub-millisecond digits the true
|
|
644
|
+
// instant lies in (floor, floor+1ms], so fail closed on BOTH validity boundaries: require each
|
|
645
|
+
// candidate path to validate at the floor AND the ceil. A token minted just after the certificate
|
|
646
|
+
// expired is rejected at the ceil (floor <= notAfter but ceil > notAfter); one minted just before
|
|
647
|
+
// notBefore is rejected at the floor (ceil >= notBefore but floor < notBefore). Certificate
|
|
648
|
+
// validity is seconds-precise (RFC 5280 sec. 4.1.2.5), so this never over-rejects a valid token.
|
|
649
|
+
var floorT = tst.genTime;
|
|
650
|
+
var ceilT = (tst.genTimeFraction != null && /[1-9]/.test(tst.genTimeFraction.slice(3))) ? new Date(floorT.getTime() + 1) : floorT;
|
|
651
|
+
try {
|
|
652
|
+
// parseToken surfaces each embedded cert as { bytes, tagClass, ... }; take the X.509 certs
|
|
653
|
+
// (universal SEQUENCE), skipping any attribute-certificate choices, and parse their raw bytes.
|
|
654
|
+
// Caller out-of-band certificates (opts.certs) join the pool so an intermediate absent from the
|
|
655
|
+
// token can still complete the path.
|
|
656
|
+
var pool = (parsed.certificates || []).filter(function (c) { return c.tagClass === "universal"; }).map(function (c) { return pkiX509.parse(c.bytes); });
|
|
657
|
+
(opts.certs || []).forEach(function (c) { pool.push(pkiX509.parse(c)); });
|
|
658
|
+
// path.validate validates a FIXED path, so backtracking over same-subject issuer candidates
|
|
659
|
+
// happens here -- accept the TSA certificate if ANY enumerated chain validates to the anchor at
|
|
660
|
+
// both window endpoints.
|
|
661
|
+
var chains = _buildTsaChains(pkiX509.parse(tsaCertDer), pool);
|
|
662
|
+
for (var ci = 0; ci < chains.length && !(pathRes && pathRes.valid); ci++) {
|
|
663
|
+
pathRes = await pathValidate.validate(chains[ci], {
|
|
664
|
+
time: floorT, trustAnchor: opts.trustAnchor, requiredEku: ["timeStamping"], revocationChecker: opts.revocationChecker,
|
|
665
|
+
});
|
|
666
|
+
if (pathRes.valid && ceilT !== floorT) {
|
|
667
|
+
pathRes = await pathValidate.validate(chains[ci], {
|
|
668
|
+
time: ceilT, trustAnchor: opts.trustAnchor, requiredEku: ["timeStamping"], revocationChecker: opts.revocationChecker,
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
} catch (e) { return fail("tsp/untrusted-tsa", (e && e.message) || String(e)); }
|
|
673
|
+
if (!pathRes || !pathRes.valid) return fail("tsp/untrusted-tsa", "the TSA certificate did not validate to the trust anchor at genTime");
|
|
674
|
+
}
|
|
675
|
+
return {
|
|
676
|
+
valid: true, genTime: tst.genTime, accuracy: tst.accuracy,
|
|
677
|
+
serialNumber: tst.serialNumber, serialNumberHex: tst.serialNumberHex,
|
|
678
|
+
policy: tst.policy, policyName: tst.policyName, nonce: tst.nonce, tsa: tst.tsa,
|
|
679
|
+
tstInfo: tst, signer: { cert: tsaCertDer, sid: signer.sid },
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
module.exports = { sign: sign, request: request, parseRequest: parseRequest, response: response, parseResponse: parseResponse, verify: verify };
|
package/lib/webcrypto.js
CHANGED
|
@@ -627,7 +627,7 @@ SubtleCrypto.prototype.unwrapKey = async function unwrapKey(format, wrappedKey,
|
|
|
627
627
|
// notice. The shared JSON guard parses strictly (bounded, fatal UTF-8, and a
|
|
628
628
|
// smuggled duplicate member rejected rather than resolved last-wins), and its
|
|
629
629
|
// failure surfaces as the module's typed webcrypto/data (W3C: DataError).
|
|
630
|
-
keyData = guard.json.parse(bytes,
|
|
630
|
+
keyData = guard.json.parse(bytes, _wcErr, {
|
|
631
631
|
maxBytes: constants.LIMITS.JSON_MAX_BYTES, maxDepth: constants.LIMITS.JSON_MAX_DEPTH,
|
|
632
632
|
badJson: "webcrypto/data", tooDeep: "webcrypto/data", duplicateMember: "webcrypto/data",
|
|
633
633
|
tooLarge: "webcrypto/data", badInput: "webcrypto/data", label: "the unwrapped JWK",
|
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:d6e0649c-f4db-4241-8784-6f3c9d3d663f",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-14T16:14:40.970Z",
|
|
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.19",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.2.
|
|
25
|
+
"version": "0.2.19",
|
|
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.19",
|
|
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.19",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|