@blamejs/pki 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,24 @@ All notable changes to `@blamejs/pki` are documented here. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
5
5
  project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## v0.1.13 — 2026-07-05
8
+
9
+ An RFC 3161 timestamp parser joins the pki.schema family.
10
+
11
+ ### Added
12
+
13
+ - pki.schema.tsp — an RFC 3161 timestamp parser. pki.schema.tsp.parse turns a DER Buffer or PEM into a TimeStampResp ({ status, statusString, failInfo, timeStampToken }) with the granted-carries-token / rejected-carries-none coupling enforced (tsp/missing-token, tsp/unexpected-token) and the PKIFailureInfo named bits decoded. pki.schema.tsp.parseToken parses a TimeStampToken by composing pki.schema.cms.parse and asserting the id-ct-TSTInfo content type (tsp/wrong-econtent-type), attached content (tsp/detached-token), and the single (TSA) signer (tsp/multi-signer), returning the decoded TSTInfo plus the signer material. pki.schema.tsp.parseTstInfo decodes a bare TSTInfo. The TSTInfo mandatory version-1, the GeneralizedTime-only genTime, the accuracy 1..999 range, the ordering DEFAULT FALSE omission, and the PKIStatus 0..5 range are all enforced fail-closed. pki.schema.parse detect-and-routes a TimeStampResp.
14
+ - The codec and schema engine gain three composable primitives that TSP required: pki.asn1.read.integerImplicit / pki.schema.engine.implicitInteger(tag) (a context-tagged IMPLICIT INTEGER, for the Accuracy millis / micros fields); pki.schema.engine.implicitSeqOf(tag, item) (an order-preserving IMPLICIT SEQUENCE OF, the sibling of implicitSetOf without the SET ordering rule, for the extensions field); and RFC 3161 / X.690 §11.7 fractional-seconds GeneralizedTime support in pki.asn1.read.time (a '.'-separated, trailing-zero-free, Z-terminated fraction, surfaced at millisecond precision).
15
+ - The OID registry gains the id-kp extended-key-purpose family (including id-kp-timeStamping) and the id-aa S/MIME authenticated-attribute family (signingCertificate / signingCertificateV2 / timeStampToken), so a parsed TSA certificate's key purpose and a signer's ESS binding attribute resolve by name.
16
+
17
+ ## v0.1.12 — 2026-07-05
18
+
19
+ SLH-DSA object identifiers corrected and completed to all twelve FIPS 205 parameter sets.
20
+
21
+ ### Fixed
22
+
23
+ - SLH-DSA OID resolution — id-slh-dsa-shake-128s and id-slh-dsa-shake-256s were mapped to the arcs of id-slh-dsa-sha2-256s (.24) and id-slh-dsa-shake-128f (.27), so pki.oid.name / pki.oid.byName resolved them incorrectly. All twelve Pure SLH-DSA parameter sets (sha2-128s/128f/192s/192f/256s/256f and shake-128s/128f/192s/192f/256s/256f) are now registered at their correct arcs .20 through .31 per RFC 9909 §3; the previously-absent nine sets now resolve as well. WebCrypto SLH-DSA sign/verify was unaffected — it selects by algorithm name, not through the OID registry.
24
+
7
25
  ## v0.1.11 — 2026-07-05
8
26
 
9
27
  An OCSP request and response parser joins the pki.schema family.
package/README.md CHANGED
@@ -197,9 +197,10 @@ is callable today; nothing below is a stub.
197
197
  | `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` |
198
198
  | `pki.schema.cms` | Parse DER / PEM CMS SignedData per RFC 5652 — encapsulated content, signer infos, raw signed-attribute bytes for external verification, certificates / CRLs kept raw, fail-closed; non-SignedData content types recognized (not decoded) — `parse`, `pemDecode`, `pemEncode` |
199
199
  | `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` |
200
+ | `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` |
200
201
  | `pki.schema.engine` | The declarative ASN.1 structure-schema engine every format parser composes — `walk` plus the schema combinators |
201
202
  | `pki.C` / `pki.constants` | Version-stable constants — functional scale helpers (`C.TIME.*`, `C.BYTES.*`), codec `LIMITS`, `version` |
202
- | `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError` / `Asn1Error` / `OidError` / `PemError` / `CertificateError` / `CrlError` / `CsrError` / `Pkcs8Error` / `CmsError` / `OcspError`, each carrying a stable `code` in `domain/reason` form |
203
+ | `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError` / `Asn1Error` / `OidError` / `PemError` / `CertificateError` / `CrlError` / `CsrError` / `Pkcs8Error` / `CmsError` / `OcspError` / `TspError`, each carrying a stable `code` in `domain/reason` form |
203
204
  | `pki` CLI | `pki version`, `pki oid <dotted\|name>`, `pki parse <cert>` |
204
205
 
205
206
  ### CLI
@@ -233,26 +234,31 @@ reintroduce the bug class it prevents. Adding a format is a schema declaration
233
234
  plus a documentation comment block, not new parse logic.
234
235
 
235
236
  ```
236
- Detect + route pki.schema.parse ── detects the format, routes to a sibling
237
-
238
- Format parsers x509 crl csr pkcs8 cms ocsp (siblings)
239
- (one schema each) └──────┴─────┴──────┴──────┴─────┘
240
- │ every sibling COMPOSES
241
- Shared structure pki.schema.engine PKIX sub-schemas
242
- walk + combinators · AlgorithmIdentifier · Name ·
243
- (positional reads, Extension · version reader ·
244
- tag ordering, SET-OF the one coerce → decode → walk
245
- uniqueness, arity, parse-entry (PEM cap + DER
246
- fail-closed errors) wrapping shared, never copied)
247
- built on
248
- Foundation pki.asn1 pki.oid pki.errors pki.C
249
- DER codec · OID registry · PkiError · constants
250
- (strict, (two-way, taxonomy (LIMITS,
251
- bounded) PQC-seeded) (domain/reason) scale)
252
-
253
- Crypto pki.webcrypto ──▶ node:crypto
254
- W3C SubtleCrypto over the platform: the classical set +
255
- post-quantum ML-DSA / SLH-DSA signatures + ML-KEM keygen
237
+ ┌─ Detect + route ────────────────────────────────────────────────────────┐
238
+ pki.schema.parse — inspect the DER root, route to the matching sibling │
239
+ └─────────────────────────────────────────────────────────────────────────┘
240
+
241
+ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ Format
242
+ │ x509 │ │ crl │ csr │ │pkcs8 │ │ cms │ │ ocsp │ │ tsp │ parsers
243
+ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘ (siblings)
244
+ │ every sibling composes
245
+ ┌─ Shared structure ──────────────────────────────────────────────────────┐
246
+ │ pki.schema.engine walk + combinators (positional reads, tag order, │
247
+ │ SET-OF uniqueness, arity, typed errors) · the PKIX sub-schemas — │
248
+ AlgorithmIdentifier · Name · Extension · a bounded version reader — │
249
+ and the one coerce → decode → walk parse-entry (PEM cap + DER wrap).
250
+ └─────────────────────────────────────────────────────────────────────────┘
251
+ │ built on ↓
252
+ ┌─ Foundation ────────────────────────────────────────────────────────────┐
253
+ │ pki.asn1 — strict, bounded DER codec · pki.oid — two-way, PQC-seeded │
254
+ OID registry · pki.errors the PkiError domain/reason taxonomy · │
255
+ │ pki.C version-stable LIMITS + scale constants. │
256
+ └─────────────────────────────────────────────────────────────────────────┘
257
+
258
+ ┌─ Crypto ────────────────────────────────────────────────────────────────┐
259
+ │ pki.webcrypto ──▶ node:crypto — a W3C SubtleCrypto engine: the │
260
+ │ classical set + post-quantum ML-DSA / SLH-DSA + ML-KEM key generation. │
261
+ └─────────────────────────────────────────────────────────────────────────┘
256
262
  ```
257
263
 
258
264
  **Foundation.** The strict, bounded DER codec (`pki.asn1`), the two-way OID
@@ -266,7 +272,7 @@ bounded version reader, and the single coerce → decode → walk parse-entry th
266
272
  every format's `parse` is bound to. Because input coercion, the PEM size cap, and
267
273
  the DER-decode wrapping live here once, a format cannot diverge on a guard.
268
274
 
269
- **Format parsers.** `x509`, `crl`, `csr`, `pkcs8`, `cms`, and `ocsp` are siblings:
275
+ **Format parsers.** `x509`, `crl`, `csr`, `pkcs8`, `cms`, `ocsp`, and `tsp` are siblings:
270
276
  each is a schema declaration composed from the shared pieces, emitting its own
271
277
  typed `domain/reason` error codes. `pki.schema.parse` inspects a decoded root and
272
278
  detect-and-routes to the matching sibling; the detectors are mutually exclusive by
package/lib/asn1-der.js CHANGED
@@ -318,6 +318,18 @@ function readEnumerated(node) {
318
318
  return _readIntegerLikeContent(node, "ENUMERATED", "readEnumerated");
319
319
  }
320
320
 
321
+ // Read a [tag] IMPLICIT INTEGER — a context-class PRIMITIVE node whose content is an
322
+ // integer body (the IMPLICIT tag replaces the universal INTEGER tag). Same minimal
323
+ // two's-complement content rules as a universal INTEGER. The integer counterpart of
324
+ // readBitStringImplicit, for the RFC 3161 Accuracy millis [0] / micros [1] fields.
325
+ function readIntegerImplicit(node, tag) {
326
+ if (node.tagClass !== "context" || node.tagNumber !== tag) {
327
+ throw new Asn1Error("asn1/unexpected-tag", "readIntegerImplicit: expected context tag [" + tag +
328
+ "], got " + node.tagClass + "/" + node.tagNumber);
329
+ }
330
+ return _readIntegerLikeContent(node, "INTEGER", "readIntegerImplicit");
331
+ }
332
+
321
333
  // The BIT STRING content decoder (the leading unused-bit octet + body, with the
322
334
  // DER zero-pad rule). Shared by the universal reader and the IMPLICIT
323
335
  // context-tagged reader — the caller asserts the tag first.
@@ -533,8 +545,17 @@ function _decodeUtf32be(buf) {
533
545
 
534
546
  var UTC_RE = /^(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})Z$/;
535
547
  var GEN_RE = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})Z$/;
536
-
537
- function readTime(node) {
548
+ // The X.690 §11.7 DER fractional-seconds GeneralizedTime profile: seconds present,
549
+ // a `.` decimal separator (never `,`), a non-empty fraction, Z-terminated. The
550
+ // trailing-zeros rule (§11.7.3) is enforced separately. Used by RFC 3161 genTime.
551
+ var GEN_FRAC_RE = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\.(\d+)Z$/;
552
+
553
+ // readTime(node, opts?) — opts.allowFractional enables the RFC 3161 / X.690 §11.7
554
+ // fractional-seconds GeneralizedTime profile. Fractional is OFF by default because
555
+ // most profiles (RFC 5280 certificate/CRL Validity, §4.1.2.5.2) require the integer-
556
+ // second YYYYMMDDHHMMSSZ form and MUST NOT include a fraction; only callers that
557
+ // permit sub-second precision (TSP genTime) pass allowFractional.
558
+ function readTime(node, opts) {
538
559
  if (node.tagClass !== "universal") throw new Asn1Error("asn1/expected-time", "readTime: not a universal time type");
539
560
  _expectPrimitive(node, "readTime");
540
561
  var s = node.content.toString("latin1");
@@ -547,7 +568,19 @@ function readTime(node) {
547
568
  year += (year < 50) ? 2000 : 1900;
548
569
  } else if (node.tagNumber === TAGS.GENERALIZED_TIME) {
549
570
  m = GEN_RE.exec(s);
550
- if (!m) throw new Asn1Error("asn1/bad-generalizedtime", "GeneralizedTime must be YYYYMMDDHHMMSSZ, got " + JSON.stringify(s));
571
+ if (!m) {
572
+ if (!(opts && opts.allowFractional)) throw new Asn1Error("asn1/bad-generalizedtime", "GeneralizedTime must be YYYYMMDDHHMMSSZ, got " + JSON.stringify(s));
573
+ m = GEN_FRAC_RE.exec(s);
574
+ if (!m) throw new Asn1Error("asn1/bad-generalizedtime", "GeneralizedTime must be YYYYMMDDHHMMSS[.fraction]Z, got " + JSON.stringify(s));
575
+ // X.690 §11.7.3 — the fractional part MUST NOT end in a zero (and is non-empty
576
+ // by the regex). ".5Z" is valid; ".50Z" and ".500Z" are non-canonical. X.690
577
+ // §11.7 places NO cap on the fraction length (RFC 3161 §2.4.2 gives the 5-digit
578
+ // example 19990609001326.34352Z), so a fraction of any length is accepted; the
579
+ // returned Date is millisecond-precision, and a caller needing the exact fraction
580
+ // reads it from node.content (the raw bytes, surfaced losslessly like the OID /
581
+ // serial paths).
582
+ if (m[7].charAt(m[7].length - 1) === "0") throw new Asn1Error("asn1/bad-generalizedtime", "GeneralizedTime fraction must not have trailing zeros, got " + JSON.stringify(s));
583
+ }
551
584
  year = parseInt(m[1], 10);
552
585
  } else {
553
586
  throw new Asn1Error("asn1/expected-time", "readTime: tag " + node.tagNumber + " is not a time type");
@@ -562,9 +595,12 @@ function readTime(node) {
562
595
  // GeneralizedTime year below 100 (0099 -> 1999). setUTCFullYear(year, ...)
563
596
  // takes the year literally and uses that year's own calendar, so years
564
597
  // below 100 and proleptic leap years are handled correctly.
598
+ // A fractional GeneralizedTime carries sub-second precision; surface it on the
599
+ // Date at millisecond resolution (the first three fraction digits, zero-padded).
600
+ var ms = (node.tagNumber === TAGS.GENERALIZED_TIME && m[7]) ? parseInt((m[7] + "000").slice(0, 3), 10) : 0;
565
601
  var d = new Date(0);
566
602
  d.setUTCFullYear(year, month - 1, day);
567
- d.setUTCHours(hour, min, sec, 0);
603
+ d.setUTCHours(hour, min, sec, ms);
568
604
  if (isNaN(d.getTime())) throw new Asn1Error("asn1/bad-time", "unparseable time " + JSON.stringify(s));
569
605
  // Date/setUTC* silently roll over out-of-range fields (Feb 30 -> Mar 2,
570
606
  // month 13 -> next Jan, hour 25 -> +1 day). Reject unless every component
@@ -818,6 +854,7 @@ module.exports = {
818
854
  read: {
819
855
  boolean: readBoolean,
820
856
  integer: readInteger,
857
+ integerImplicit: readIntegerImplicit,
821
858
  enumerated: readEnumerated,
822
859
  bitString: readBitString,
823
860
  bitStringImplicit: readBitStringImplicit,
@@ -145,6 +145,10 @@ var CmsError = defineClass("CmsError", { withCause: true });
145
145
  // (OCSPRequest / OCSPResponse — RFC 6960 §4). Carries the leaf fault as `.cause`.
146
146
  var OcspError = defineClass("OcspError", { withCause: true });
147
147
 
148
+ // TspError — a byte sequence that is not a well-formed RFC 3161 timestamp token,
149
+ // TSTInfo, or TimeStampResp. Carries the underlying leaf fault as `.cause`.
150
+ var TspError = defineClass("TspError", { withCause: true });
151
+
148
152
  module.exports = {
149
153
  PkiError: PkiError,
150
154
  defineClass: defineClass,
@@ -159,4 +163,5 @@ module.exports = {
159
163
  Pkcs8Error: Pkcs8Error,
160
164
  CmsError: CmsError,
161
165
  OcspError: OcspError,
166
+ TspError: TspError,
162
167
  };
package/lib/oid.js CHANGED
@@ -79,6 +79,11 @@ var FAMILIES = {
79
79
  ocspBasic: 1, ocspNonce: 2, ocspCrl: 3, ocspResponse: 4, ocspNoCheck: 5,
80
80
  ocspArchiveCutoff: 6, ocspServiceLocator: 7, ocspPrefSigAlgs: 8, ocspExtendedRevoke: 9 } },
81
81
 
82
+ // PKIX extended key purposes (id-kp, RFC 5280 §4.2.1.12). timeStamping is required
83
+ // — critical and sole — on an RFC 3161 TSA signing certificate (§2.3).
84
+ pkixKp: { base: [1, 3, 6, 1, 5, 5, 7, 3], of: {
85
+ serverAuth: 1, clientAuth: 2, codeSigning: 3, emailProtection: 4, timeStamping: 8, ocspSigning: 9 } },
86
+
82
87
  // PKCS#1 RSA public-key + RSASSA signature algorithms.
83
88
  rsa: { base: [1, 2, 840, 113549, 1, 1], of: {
84
89
  rsaEncryption: 1, rsassaPss: 10, sha256WithRSAEncryption: 11,
@@ -98,6 +103,12 @@ var FAMILIES = {
98
103
  // S/MIME content types on the PKCS#9 smime arc (RFC 5652, RFC 3161): id-ct.
99
104
  smimeCt: { base: [1, 2, 840, 113549, 1, 9, 16, 1], of: { authData: 2, tSTInfo: 4 } },
100
105
 
106
+ // S/MIME authenticated attributes (id-aa, RFC 2634 / RFC 5035 / RFC 5816). The ESS
107
+ // signing-certificate attributes bind a CMS / TSP SignerInfo to its signing cert;
108
+ // signingCertificateV2 (ESSCertIDv2) carries a non-SHA-1 cert hash (RFC 5816 §2.2.1).
109
+ smimeAa: { base: [1, 2, 840, 113549, 1, 9, 16, 2], of: {
110
+ signingCertificate: 12, timeStampToken: 14, signingCertificateV2: 47 } },
111
+
101
112
  // ANSI X9.62 EC public key, named curve, and ECDSA signatures.
102
113
  ansiX962: { base: [1, 2, 840, 10045], of: {
103
114
  ecPublicKey: [2, 1], prime256v1: [3, 1, 7],
@@ -118,10 +129,17 @@ var FAMILIES = {
118
129
  sha256: 1, sha384: 2, sha512: 3, "sha3-256": 8, "sha3-512": 10, shake256: 12 } },
119
130
 
120
131
  // NIST signature algorithms — FIPS 204 ML-DSA + FIPS 205 SLH-DSA share the
121
- // signature arc 2.16.840.1.101.3.4.3.
132
+ // signature arc 2.16.840.1.101.3.4.3. The 12 Pure SLH-DSA parameter sets are
133
+ // assigned sequentially (RFC 9909 §3): the SHA-2 sets .20-.25, then the SHAKE
134
+ // sets .26-.31.
122
135
  nistSig: { base: [2, 16, 840, 1, 101, 3, 4, 3], of: {
123
136
  "id-ml-dsa-44": 17, "id-ml-dsa-65": 18, "id-ml-dsa-87": 19,
124
- "id-slh-dsa-sha2-128s": 20, "id-slh-dsa-shake-128s": 24, "id-slh-dsa-shake-256s": 27 } },
137
+ "id-slh-dsa-sha2-128s": 20, "id-slh-dsa-sha2-128f": 21,
138
+ "id-slh-dsa-sha2-192s": 22, "id-slh-dsa-sha2-192f": 23,
139
+ "id-slh-dsa-sha2-256s": 24, "id-slh-dsa-sha2-256f": 25,
140
+ "id-slh-dsa-shake-128s": 26, "id-slh-dsa-shake-128f": 27,
141
+ "id-slh-dsa-shake-192s": 28, "id-slh-dsa-shake-192f": 29,
142
+ "id-slh-dsa-shake-256s": 30, "id-slh-dsa-shake-256f": 31 } },
125
143
 
126
144
  // NIST KEM — FIPS 203 ML-KEM (arc 2.16.840.1.101.3.4.4).
127
145
  nistKem: { base: [2, 16, 840, 1, 101, 3, 4, 4], of: {
package/lib/schema-all.js CHANGED
@@ -38,6 +38,7 @@ var csr = require("./schema-csr");
38
38
  var pkcs8 = require("./schema-pkcs8");
39
39
  var cms = require("./schema-cms");
40
40
  var ocsp = require("./schema-ocsp");
41
+ var tsp = require("./schema-tsp");
41
42
  var frameworkError = require("./framework-error");
42
43
 
43
44
  var SchemaError = frameworkError.SchemaError;
@@ -66,12 +67,27 @@ var FORMATS = [
66
67
  detect: cms.matches,
67
68
  parse: function (input) { return cms.parse(input); },
68
69
  },
70
+ {
71
+ // TimeStampResp ::= SEQUENCE { status PKIStatusInfo, timeStampToken OPTIONAL } —
72
+ // a SEQUENCE of 1-2 whose first child is a PKIStatusInfo SEQUENCE whose own first
73
+ // child is an INTEGER status. This detector is a strict REFINEMENT of the
74
+ // ocsp-request one (both are SEQUENCE-of-1-2 with a SEQUENCE first child), so it
75
+ // MUST be checked first: a tokenless TimeStampResp (a bare PKIStatusInfo) would
76
+ // otherwise be shadowed by ocsp-request, whereas an OCSPRequest's tbsRequest never
77
+ // leads with a universal INTEGER and so never matches here (RFC 3161 §2.4.2). A
78
+ // bare timestamp token is a CMS ContentInfo and routes to cms; a bare TSTInfo is
79
+ // an internal payload (reached via pki.schema.tsp.parseTstInfo).
80
+ name: "tsp",
81
+ module: tsp,
82
+ detect: tsp.matches,
83
+ parse: function (input) { return tsp.parse(input); },
84
+ },
69
85
  {
70
86
  // OCSPRequest ::= SEQUENCE { tbsRequest SEQUENCE, optionalSignature [0] EXPLICIT
71
87
  // OPTIONAL } — a SEQUENCE of 1-2 whose first child is the tbsRequest SEQUENCE.
72
88
  // Leads with a SEQUENCE like the signed-envelope trio, but is excluded by arity
73
- // (the trio is EXACTLY 3 children; an OCSPRequest is 1-2), so it detects
74
- // unambiguously regardless of registry order (RFC 6960 §4.1.1).
89
+ // (the trio is EXACTLY 3 children; an OCSPRequest is 1-2). Checked AFTER tsp,
90
+ // whose detector is a strict refinement (RFC 6960 §4.1.1).
75
91
  name: "ocsp-request",
76
92
  module: ocsp,
77
93
  detect: ocsp.matchesRequest,
@@ -141,7 +157,7 @@ var FORMATS = [
141
157
  * The names of every registered format, in detection order.
142
158
  *
143
159
  * @example
144
- * pki.schema.all(); // → ["cms", "ocsp-request", "ocsp-response", "pkcs8", "csr", "crl", "x509"]
160
+ * pki.schema.all(); // → ["cms", "tsp", "ocsp-request", "ocsp-response", "pkcs8", "csr", "crl", "x509"]
145
161
  */
146
162
  function all() { return FORMATS.map(function (f) { return f.name; }); }
147
163
 
@@ -185,6 +201,7 @@ module.exports = {
185
201
  pkcs8: { parse: pkcs8.parse, parseEncrypted: pkcs8.parseEncrypted, pemDecode: pkcs8.pemDecode, pemEncode: pkcs8.pemEncode },
186
202
  cms: { parse: cms.parse, pemDecode: cms.pemDecode, pemEncode: cms.pemEncode },
187
203
  ocsp: { parseRequest: ocsp.parseRequest, parseResponse: ocsp.parseResponse, pemDecode: ocsp.pemDecode },
204
+ tsp: { parse: tsp.parse, parseTstInfo: tsp.parseTstInfo, parseToken: tsp.parseToken, pemDecode: tsp.pemDecode },
188
205
  all: all,
189
206
  parse: parse,
190
207
  };
@@ -109,6 +109,11 @@ function implicitOctetString(tag) { return { kind: "leaf", read: function (n) {
109
109
  // unknown [2] arms (RFC 6960 §4.2.1). Yields null; rejects a constructed or
110
110
  // non-empty [tag] node fail-closed.
111
111
  function implicitNull(tag) { return { kind: "leaf", read: function (n) { return asn1.read.nullImplicit(n, tag); } }; }
112
+ // A [tag] IMPLICIT INTEGER leaf (context-class primitive) — the integer sibling of
113
+ // implicitBitString, for the RFC 3161 Accuracy millis [0] / micros [1] fields
114
+ // (context-tagged primitive INTEGERs). Yields a BigInt; a constructed or wrong-tag
115
+ // node fails asn1/* at the codec.
116
+ function implicitInteger(tag) { return { kind: "leaf", read: function (n) { return asn1.read.integerImplicit(n, tag); } }; }
112
117
  function any() { return { kind: "any" }; }
113
118
  function decode(fn) { return { kind: "decode", fn: fn }; }
114
119
 
@@ -196,6 +201,15 @@ function implicitSetOf(tag, item, opts) {
196
201
  return { kind: "repeat", item: item, assert: "implicit", implicitTag: tag, derSetOrder: true, code: opts.code, what: opts.what,
197
202
  min: opts.min, unique: opts.unique, dupCode: opts.dupCode, build: opts.build };
198
203
  }
204
+ // [tag] IMPLICIT SEQUENCE OF item — the context tag REPLACES the universal SEQUENCE
205
+ // tag, so the node is a context-class constructed [tag] whose direct children are the
206
+ // items. Order-preserving: the SEQUENCE-OF sibling of implicitSetOf WITHOUT the SET
207
+ // ascending-DER-order rule (RFC 3161 extensions [1] IMPLICIT Extensions).
208
+ function implicitSeqOf(tag, item, opts) {
209
+ opts = opts || {};
210
+ return { kind: "repeat", item: item, assert: "implicit", implicitTag: tag, code: opts.code, what: opts.what,
211
+ min: opts.min, unique: opts.unique, dupCode: opts.dupCode, build: opts.build };
212
+ }
199
213
 
200
214
  // ---- the walk engine -------------------------------------------------
201
215
 
@@ -377,11 +391,11 @@ function _consumeTrailing(fld, kids, start, fields, ctx) {
377
391
  module.exports = {
378
392
  // structural
379
393
  seq: seq, field: field, optional: optional, explicit: explicit, trailing: trailing,
380
- seqOf: seqOf, setOf: setOf, setOfUnique: setOfUnique, implicitSetOf: implicitSetOf, choice: choice,
394
+ seqOf: seqOf, setOf: setOf, setOfUnique: setOfUnique, implicitSetOf: implicitSetOf, implicitSeqOf: implicitSeqOf, choice: choice,
381
395
  // leaves
382
396
  oidLeaf: oidLeaf, integerLeaf: integerLeaf, boolean: boolean, octetString: octetString,
383
397
  bitString: bitString, implicitBitString: implicitBitString, implicitOctetString: implicitOctetString,
384
- implicitNull: implicitNull,
398
+ implicitNull: implicitNull, implicitInteger: implicitInteger,
385
399
  any: any, decode: decode, time: time,
386
400
  // engine
387
401
  walk: walk,
@@ -105,59 +105,10 @@ function certificateBytes() {
105
105
  });
106
106
  }
107
107
 
108
- // requestorName [1] EXPLICIT GeneralName — GeneralName ::= CHOICE (RFC 5280
109
- // §4.2.1.6). We keep the value RAW (no pkix.generalName factory yet), but validate
110
- // the chosen alternative's tag AND form/content so a malformed GeneralName fails
111
- // closed rather than passing the signed-request requestorName guard: the constructed
112
- // alternatives (otherName [0], x400Address [3], directoryName [4], ediPartyName [5])
113
- // must be constructed; rfc822Name [1] / dNSName [2] / uniformResourceIdentifier [6]
114
- // are primitive IA5String (7-bit); iPAddress [7] is a 4- or 16-octet OCTET STRING;
115
- // registeredID [8] is a primitive OBJECT IDENTIFIER.
116
- var GN_CONSTRUCTED = { 0: 1, 3: 1, 4: 1, 5: 1 };
117
- var GN_IA5 = { 1: 1, 2: 1, 6: 1 };
118
- function _validateGeneralName(n, ctx) {
119
- if (n.tagClass !== "context" || n.tagNumber < 0 || n.tagNumber > 8) {
120
- throw ctx.E("ocsp/bad-requestor-name", "requestorName must be a GeneralName (context tag [0]..[8]) (RFC 6960 §4.1.1)");
121
- }
122
- var t = n.tagNumber;
123
- var constructed = !!n.children;
124
- if (GN_CONSTRUCTED[t]) {
125
- if (!constructed || n.children.length < 1) throw ctx.E("ocsp/bad-requestor-name", "GeneralName [" + t + "] must be a non-empty constructed value (RFC 5280 §4.2.1.6)");
126
- if (t === 0) {
127
- // otherName ::= SEQUENCE { type-id OBJECT IDENTIFIER, value [0] EXPLICIT ANY } —
128
- // both fields mandatory: exactly two children, a type-id OID and a [0] EXPLICIT
129
- // value wrapper carrying exactly one inner value.
130
- if (n.children.length !== 2) throw ctx.E("ocsp/bad-requestor-name", "GeneralName otherName [0] must be a SEQUENCE { type-id, value [0] } (RFC 5280 §4.2.1.6)");
131
- try { asn1.read.oid(n.children[0]); }
132
- catch (e) { throw ctx.E("ocsp/bad-requestor-name", "GeneralName otherName [0] must lead with a type-id OBJECT IDENTIFIER", e); }
133
- var v = n.children[1];
134
- if (!(v.tagClass === "context" && v.tagNumber === 0 && v.children && v.children.length === 1)) {
135
- throw ctx.E("ocsp/bad-requestor-name", "GeneralName otherName [0] value must be a [0] EXPLICIT wrapper carrying exactly one value");
136
- }
137
- } else if (t === 4) {
138
- // directoryName [4] EXPLICIT Name — validate the wrapped RDNSequence.
139
- if (n.children.length !== 1) throw ctx.E("ocsp/bad-requestor-name", "GeneralName directoryName [4] must wrap exactly one Name (RFC 5280 §4.2.1.6)");
140
- schema.walk(NAME, n.children[0], ctx);
141
- }
142
- // x400Address [3] / ediPartyName [5]: validated to non-empty constructed form
143
- // (their full ORAddress / EDIPartyName bodies are above the requestorName altitude).
144
- } else {
145
- if (constructed) throw ctx.E("ocsp/bad-requestor-name", "GeneralName [" + t + "] must be primitive (RFC 5280 §4.2.1.6)");
146
- if (GN_IA5[t]) {
147
- if (n.content.length === 0) throw ctx.E("ocsp/bad-requestor-name", "GeneralName [" + t + "] must be a non-empty IA5String");
148
- for (var i = 0; i < n.content.length; i++) {
149
- if (n.content[i] > 0x7f) throw ctx.E("ocsp/bad-requestor-name", "GeneralName [" + t + "] must be an IA5String (7-bit)");
150
- }
151
- } else if (t === 7) {
152
- if (n.content.length !== 4 && n.content.length !== 16) throw ctx.E("ocsp/bad-requestor-name", "GeneralName iPAddress [7] must be a 4- or 16-octet address");
153
- } else if (t === 8) {
154
- try { asn1.decodeOidContent(n.content); }
155
- catch (e) { throw ctx.E("ocsp/bad-requestor-name", "GeneralName registeredID [8] must be a valid OBJECT IDENTIFIER", e); }
156
- }
157
- }
158
- return { bytes: n.bytes, tagClass: n.tagClass, tagNumber: n.tagNumber };
159
- }
160
- var GENERAL_NAME_RAW = schema.decode(_validateGeneralName);
108
+ // requestorName [1] EXPLICIT GeneralName (RFC 6960 §4.1.1) validated + surfaced raw
109
+ // via the shared pkix.generalName primitive (RFC 5280 §4.2.1.6), so a malformed
110
+ // GeneralName fails closed and the OCSP + TSP parsers cannot drift on this grammar.
111
+ var GENERAL_NAME_RAW = pkix.generalName(NS, { code: "ocsp/bad-requestor-name" });
161
112
 
162
113
  // An OCSP signature BIT STRING is a byte string handed to a cryptographic verifier,
163
114
  // so it MUST be octet-aligned (0 unused bits); a non-octet-aligned signature is
@@ -208,6 +208,62 @@ function name(ns) {
208
208
  });
209
209
  }
210
210
 
211
+ // GeneralName ::= CHOICE (RFC 5280 §4.2.1.6). A validate-and-surface-raw leaf for a
212
+ // caller that keeps the value RAW but needs it to be a well-formed GeneralName: it
213
+ // checks the chosen alternative's tag, form (constructed vs primitive per X.690
214
+ // §10.2), and content — otherName [0] is a SEQUENCE { type-id OID, value [0] EXPLICIT
215
+ // }; x400Address [3] / ediPartyName [5] are non-empty constructed; directoryName [4]
216
+ // EXPLICIT wraps a valid Name; rfc822Name [1] / dNSName [2] / uniformResourceIdentifier
217
+ // [6] are primitive non-empty IA5String (7-bit); iPAddress [7] is a 4- or 16-octet
218
+ // OCTET STRING; registeredID [8] is a primitive OBJECT IDENTIFIER — then surfaces the
219
+ // value RAW ({ bytes, tagClass, tagNumber }). `opts.code` is the caller's error code
220
+ // (e.g. ocsp/bad-requestor-name, tsp/bad-tsa). Shared so the two parsers cannot drift.
221
+ var GN_CONSTRUCTED = { 0: 1, 3: 1, 4: 1, 5: 1 };
222
+ var GN_IA5 = { 1: 1, 2: 1, 6: 1 };
223
+ function generalName(ns, opts) {
224
+ opts = opts || {};
225
+ var code = opts.code || (ns.prefix + "/bad-general-name");
226
+ var NAME = name(ns);
227
+ return schema.decode(function (n, ctx) {
228
+ if (n.tagClass !== "context" || n.tagNumber < 0 || n.tagNumber > 8) {
229
+ throw ctx.E(code, "value must be a GeneralName (context tag [0]..[8]) (RFC 5280 §4.2.1.6)");
230
+ }
231
+ var t = n.tagNumber;
232
+ var constructed = !!n.children;
233
+ if (GN_CONSTRUCTED[t]) {
234
+ if (!constructed || n.children.length < 1) throw ctx.E(code, "GeneralName [" + t + "] must be a non-empty constructed value (RFC 5280 §4.2.1.6)");
235
+ if (t === 0) {
236
+ // otherName ::= SEQUENCE { type-id OBJECT IDENTIFIER, value [0] EXPLICIT ANY }.
237
+ if (n.children.length !== 2) throw ctx.E(code, "GeneralName otherName [0] must be a SEQUENCE { type-id, value [0] }");
238
+ try { asn1.read.oid(n.children[0]); }
239
+ catch (e) { throw ctx.E(code, "GeneralName otherName [0] must lead with a type-id OBJECT IDENTIFIER", e); }
240
+ var v = n.children[1];
241
+ if (!(v.tagClass === "context" && v.tagNumber === 0 && v.children && v.children.length === 1)) {
242
+ throw ctx.E(code, "GeneralName otherName [0] value must be a [0] EXPLICIT wrapper carrying exactly one value");
243
+ }
244
+ } else if (t === 4) {
245
+ // directoryName [4] EXPLICIT Name — validate the wrapped RDNSequence.
246
+ if (n.children.length !== 1) throw ctx.E(code, "GeneralName directoryName [4] must wrap exactly one Name");
247
+ schema.walk(NAME, n.children[0], ctx);
248
+ }
249
+ } else {
250
+ if (constructed) throw ctx.E(code, "GeneralName [" + t + "] must be primitive (X.690 §10.2)");
251
+ if (GN_IA5[t]) {
252
+ if (n.content.length === 0) throw ctx.E(code, "GeneralName [" + t + "] must be a non-empty IA5String");
253
+ for (var i = 0; i < n.content.length; i++) {
254
+ if (n.content[i] > 0x7f) throw ctx.E(code, "GeneralName [" + t + "] must be an IA5String (7-bit)");
255
+ }
256
+ } else if (t === 7) {
257
+ if (n.content.length !== 4 && n.content.length !== 16) throw ctx.E(code, "GeneralName iPAddress [7] must be a 4- or 16-octet address");
258
+ } else if (t === 8) {
259
+ try { asn1.decodeOidContent(n.content); }
260
+ catch (e) { throw ctx.E(code, "GeneralName registeredID [8] must be a valid OBJECT IDENTIFIER", e); }
261
+ }
262
+ }
263
+ return { bytes: n.bytes, tagClass: n.tagClass, tagNumber: n.tagNumber };
264
+ });
265
+ }
266
+
211
267
  // Extension ::= SEQUENCE { extnID OID, critical BOOLEAN DEFAULT FALSE,
212
268
  // extnValue OCTET STRING }. `critical` is a universal BOOLEAN present-by-count
213
269
  // (not context-tagged), so the per-extension decode handles the 2-vs-3-child
@@ -362,6 +418,7 @@ module.exports = {
362
418
  attributeTypeAndValue: attributeTypeAndValue,
363
419
  relativeDistinguishedName: relativeDistinguishedName,
364
420
  name: name,
421
+ generalName: generalName,
365
422
  attribute: attribute,
366
423
  extension: extension,
367
424
  extensions: extensions,
@@ -0,0 +1,411 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ /**
5
+ * @module pki.schema.tsp
6
+ * @nav Schema
7
+ * @title TSP
8
+ * @order 170
9
+ * @slug tsp
10
+ *
11
+ * @intro
12
+ * RFC 3161 Time-Stamp Protocol handling. A `TimeStampResp` is what a client
13
+ * receives from a TSA — a `PKIStatusInfo` plus, on success, a `TimeStampToken`;
14
+ * `parse` decodes it and enforces the status-to-token coupling (a granted
15
+ * response carries a token, a rejection does not). A `TimeStampToken` is itself a
16
+ * CMS SignedData whose encapsulated content is a `TSTInfo`, so `parseToken`
17
+ * composes the CMS parser, asserts the `id-ct-TSTInfo` content type and the
18
+ * single-signer rule, and decodes the inner `TSTInfo`. `parseTstInfo` decodes a
19
+ * bare `TSTInfo` payload directly.
20
+ *
21
+ * The parser surfaces everything a verifier needs and interprets nothing it
22
+ * cannot: `messageImprint.hashAlgorithm` and the raw `hashedMessage`, the
23
+ * `genTime` (with sub-second precision), the `serialNumber` and `nonce` (lossless,
24
+ * as BigInt + hex), and the `policy`. The imprint-to-request and nonce-to-request
25
+ * round-trips, the ESS signing-certificate binding, the timestamping EKU, and the
26
+ * signature are verification-layer concerns above parse altitude. DER-only,
27
+ * fail-closed.
28
+ *
29
+ * @card
30
+ * Parse DER / PEM RFC 3161 timestamp responses and tokens — per-response status,
31
+ * the TSTInfo payload (imprint, genTime, serial, nonce, accuracy), raw verifier
32
+ * inputs, single-signer token composition over CMS, fail-closed.
33
+ */
34
+
35
+ var asn1 = require("./asn1-der");
36
+ var schema = require("./schema-engine");
37
+ var pkix = require("./schema-pkix");
38
+ var oid = require("./oid");
39
+ var cms = require("./schema-cms");
40
+ var frameworkError = require("./framework-error");
41
+
42
+ var TspError = frameworkError.TspError;
43
+ var PemError = frameworkError.PemError;
44
+
45
+ var NS = pkix.makeNS("tsp", TspError, oid);
46
+
47
+ var ALGORITHM_IDENTIFIER = pkix.algorithmIdentifier(NS);
48
+ var EXTENSION = pkix.extension(NS);
49
+
50
+ // TSTInfo.version is INTEGER { v1(1) } — the only legal value is 1.
51
+ var VERSION = pkix.versionReader(NS, { "1": 1 });
52
+
53
+ // id-ct-TSTInfo is the eContentType that identifies a timestamp token (RFC 3161
54
+ // §2.4.2); resolved from the registry, never a dotted literal.
55
+ var OID_TST_INFO = oid.byName("tSTInfo");
56
+
57
+ var TAGS = asn1.TAGS;
58
+
59
+ // PKIFailureInfo ::= BIT STRING NamedBitList (RFC 3161 §2.4.2). bit 0 is the MSB of
60
+ // the first content octet; a set bit outside the named set is surfaced by index.
61
+ var FAILURE_BITS = {
62
+ 0: "badAlg", 2: "badRequest", 5: "badDataFormat", 14: "timeNotAvailable",
63
+ 15: "unacceptedPolicy", 16: "unacceptedExtension", 17: "addInfoNotAvailable", 25: "systemFailure",
64
+ };
65
+
66
+ // ---- shared leaves ---------------------------------------------------
67
+
68
+ // GeneralizedTime-only leaf. RFC 3161 §2.4.2 requires genTime to be GeneralizedTime,
69
+ // never UTCTime; assert the tag before the (fractional-capable) codec read.
70
+ var GEN_TIME = schema.decode(function (n, ctx) {
71
+ if (n.tagClass !== "universal" || n.tagNumber !== TAGS.GENERALIZED_TIME) {
72
+ throw ctx.E("tsp/bad-gentime", "genTime must be a GeneralizedTime (RFC 3161 §2.4.2)");
73
+ }
74
+ // RFC 3161 genTime may carry sub-second precision (X.690 §11.7 fractional profile).
75
+ return asn1.read.time(n, { allowFractional: true });
76
+ });
77
+
78
+ // tsa [0] EXPLICIT GeneralName (RFC 3161 §2.4.2) — validated + surfaced raw via the
79
+ // shared pkix.generalName primitive (RFC 5280 §4.2.1.6), which checks the chosen
80
+ // alternative's form and content so a malformed GeneralName fails closed.
81
+ var GENERAL_NAME_RAW = pkix.generalName(NS, { code: "tsp/bad-tsa" });
82
+
83
+ // A PKIFreeText element MUST be a UTF8String (RFC 3161 §2.4.2).
84
+ var UTF8_TEXT = schema.decode(function (n, ctx) {
85
+ if (n.tagClass !== "universal" || n.tagNumber !== TAGS.UTF8_STRING) {
86
+ throw ctx.E("tsp/bad-status-info", "PKIFreeText elements must be UTF8String");
87
+ }
88
+ return asn1.read.string(n);
89
+ });
90
+
91
+ // ---- MessageImprint --------------------------------------------------
92
+
93
+ // MessageImprint ::= SEQUENCE { hashAlgorithm AlgorithmIdentifier, hashedMessage
94
+ // OCTET STRING } (RFC 3161 §2.4.1). hashedMessage is a digest — surfaced RAW.
95
+ var MESSAGE_IMPRINT = schema.seq([
96
+ schema.field("hashAlgorithm", ALGORITHM_IDENTIFIER),
97
+ schema.field("hashedMessage", schema.octetString()),
98
+ ], {
99
+ assert: "sequence", arity: { exact: 2 }, code: "tsp/bad-message-imprint", what: "MessageImprint",
100
+ build: function (m) {
101
+ return { hashAlgorithm: m.fields.hashAlgorithm.value.result, hashedMessage: m.fields.hashedMessage.value };
102
+ },
103
+ });
104
+
105
+ // ---- Accuracy --------------------------------------------------------
106
+
107
+ // Accuracy ::= SEQUENCE { seconds INTEGER OPTIONAL, millis [0] IMPLICIT INTEGER
108
+ // (1..999) OPTIONAL, micros [1] IMPLICIT INTEGER (1..999) OPTIONAL } (RFC 3161
109
+ // §2.4.2). Every sub-field is optional; a missing one defaults to 0.
110
+ var ACCURACY = schema.seq([
111
+ schema.optional("seconds", schema.integerLeaf(), { whenUniversal: [TAGS.INTEGER] }),
112
+ schema.trailing([
113
+ { tag: 0, name: "millis", schema: schema.implicitInteger(0) },
114
+ { tag: 1, name: "micros", schema: schema.implicitInteger(1) },
115
+ ], { minTag: 0, maxTag: 1, unexpectedCode: "tsp/bad-accuracy", orderCode: "tsp/bad-accuracy" }),
116
+ ], {
117
+ assert: "sequence", code: "tsp/bad-accuracy", what: "Accuracy",
118
+ build: function (m) {
119
+ function sub(f) {
120
+ if (!f.present) return 0;
121
+ var v = f.value;
122
+ // millis / micros are constrained to 1..999 (RFC 3161 §2.4.2); 0 is also
123
+ // excluded (and non-canonical for a positive count).
124
+ if (v < 1n || v > 999n) throw NS.E("tsp/bad-accuracy", "Accuracy millis/micros must be in 1..999");
125
+ return Number(v);
126
+ }
127
+ return {
128
+ // seconds is an unbounded INTEGER — keep it lossless as a BigInt (millis /
129
+ // micros are constrained to 1..999, so Number is exact for those).
130
+ seconds: m.fields.seconds.present ? m.fields.seconds.value : 0n,
131
+ millis: sub(m.fields.millis),
132
+ micros: sub(m.fields.micros),
133
+ };
134
+ },
135
+ });
136
+
137
+ // ---- TSTInfo ---------------------------------------------------------
138
+
139
+ // TSTInfo ::= SEQUENCE { version, policy, messageImprint, serialNumber, genTime,
140
+ // accuracy OPTIONAL, ordering DEFAULT FALSE, nonce OPTIONAL, tsa [0] EXPLICIT
141
+ // GeneralName OPTIONAL, extensions [1] IMPLICIT Extensions OPTIONAL } (RFC 3161
142
+ // §2.4.2). The five mandatory fields come first; the optionals are consumed by tag.
143
+ var TST_INFO = schema.seq([
144
+ schema.field("version", VERSION),
145
+ schema.field("policy", schema.oidLeaf()),
146
+ schema.field("messageImprint", MESSAGE_IMPRINT),
147
+ schema.field("serialNumber", schema.integerLeaf()),
148
+ schema.field("genTime", GEN_TIME),
149
+ schema.optional("accuracy", ACCURACY, { whenUniversal: [TAGS.SEQUENCE] }),
150
+ schema.optional("ordering", schema.boolean(), { whenUniversal: [TAGS.BOOLEAN] }),
151
+ schema.optional("nonce", schema.integerLeaf(), { whenUniversal: [TAGS.INTEGER] }),
152
+ schema.trailing([
153
+ { tag: 0, name: "tsa", schema: GENERAL_NAME_RAW, explicit: true, emptyCode: "tsp/bad-tsa" },
154
+ { tag: 1, name: "extensions", schema: schema.implicitSeqOf(1, EXTENSION, { min: 1, unique: function (it) { return it.value.oid; }, dupCode: "tsp/duplicate-extension", code: "tsp/bad-extensions", what: "extensions" }) },
155
+ ], { minTag: 0, maxTag: 1, unexpectedCode: "tsp/bad-tst-info", orderCode: "tsp/bad-tst-info" }),
156
+ ], {
157
+ assert: "sequence", code: "tsp/bad-tst-info", what: "TSTInfo",
158
+ build: function (m, ctx) {
159
+ // ordering BOOLEAN DEFAULT FALSE — an explicit FALSE must be omitted (DER).
160
+ if (m.fields.ordering.present && m.fields.ordering.value === false) {
161
+ throw NS.E("tsp/bad-ordering", "ordering is BOOLEAN DEFAULT FALSE — an explicit FALSE must be omitted");
162
+ }
163
+ var policy = m.fields.policy.value;
164
+ var tsa = m.fields.tsa;
165
+ return {
166
+ version: m.fields.version.value,
167
+ policy: policy,
168
+ policyName: ctx.oid.name(policy) || null,
169
+ messageImprint: m.fields.messageImprint.value.result,
170
+ serialNumber: m.fields.serialNumber.value,
171
+ serialNumberHex: m.fields.serialNumber.node.content.toString("hex"),
172
+ genTime: m.fields.genTime.value,
173
+ // genTime is surfaced as a millisecond-precision Date; genTimeFraction is the
174
+ // exact fractional-seconds digits (lossless for sub-millisecond precision), or
175
+ // null when genTime carries no fraction.
176
+ genTimeFraction: (function () { var g = /\.(\d+)Z$/.exec(m.fields.genTime.node.content.toString("latin1")); return g ? g[1] : null; })(),
177
+ accuracy: m.fields.accuracy.present ? m.fields.accuracy.value.result : null,
178
+ ordering: m.fields.ordering.present ? m.fields.ordering.value : false,
179
+ nonce: m.fields.nonce.present ? m.fields.nonce.value : null,
180
+ nonceHex: m.fields.nonce.present ? m.fields.nonce.node.content.toString("hex") : null,
181
+ tsa: tsa.present ? tsa.value : null,
182
+ extensions: m.fields.extensions.present ? m.fields.extensions.value.items.map(function (it) { return it.value; }) : null,
183
+ };
184
+ },
185
+ });
186
+
187
+ // ---- PKIStatusInfo / TimeStampResp -----------------------------------
188
+
189
+ // PKIStatusInfo ::= SEQUENCE { status PKIStatus (INTEGER), statusString PKIFreeText
190
+ // OPTIONAL, failInfo PKIFailureInfo OPTIONAL } (RFC 3161 §2.4.2, from RFC 4210).
191
+ var PKI_STATUS_INFO = schema.seq([
192
+ schema.field("status", schema.integerLeaf()),
193
+ schema.optional("statusString", schema.seqOf(UTF8_TEXT, { assert: "sequence", min: 1, code: "tsp/bad-status-info", what: "statusString" }), { whenUniversal: [TAGS.SEQUENCE] }),
194
+ schema.optional("failInfo", schema.bitString(), { whenUniversal: [TAGS.BIT_STRING] }),
195
+ ], {
196
+ assert: "sequence", code: "tsp/bad-status-info", what: "PKIStatusInfo",
197
+ build: function (m) {
198
+ var status = m.fields.status.value;
199
+ // PKIStatus ::= INTEGER { granted(0) .. revocationNotification(5) }.
200
+ if (status < 0n || status > 5n) throw NS.E("tsp/bad-status", "PKIStatus " + status + " is outside 0..5 (RFC 3161 §2.4.2)");
201
+ var failInfo = null;
202
+ if (m.fields.failInfo.present) {
203
+ var bs = m.fields.failInfo.value;
204
+ _assertMinimalNamedBits(bs.unusedBits, bs.bytes);
205
+ failInfo = { unusedBits: bs.unusedBits, bytes: bs.bytes, bits: _namedBits(bs.bytes) };
206
+ }
207
+ return {
208
+ status: Number(status),
209
+ statusString: m.fields.statusString.present ? m.fields.statusString.value.items.map(function (it) { return it.value; }) : null,
210
+ failInfo: failInfo,
211
+ };
212
+ },
213
+ });
214
+
215
+ // X.690 §11.2.2 — a BIT STRING typed with a NamedBitList (PKIFailureInfo) MUST have
216
+ // all trailing 0 bits removed under DER: no trailing all-zero content octet, and the
217
+ // declared unusedBits must sit exactly below the lowest set bit of the last octet (so
218
+ // the encoding is minimal). The empty value encodes as 0 content octets, 0 unusedBits.
219
+ function _assertMinimalNamedBits(unusedBits, bytes) {
220
+ if (bytes.length === 0) {
221
+ if (unusedBits !== 0) throw NS.E("tsp/bad-failinfo", "an empty PKIFailureInfo must encode with 0 unused bits (X.690 §11.2.2)");
222
+ return;
223
+ }
224
+ var last = bytes[bytes.length - 1];
225
+ if (last === 0) throw NS.E("tsp/bad-failinfo", "PKIFailureInfo NamedBitList must not have a trailing all-zero octet (X.690 §11.2.2)");
226
+ if (((last >> unusedBits) & 1) !== 1) throw NS.E("tsp/bad-failinfo", "PKIFailureInfo NamedBitList must have all trailing zero bits removed (X.690 §11.2.2)");
227
+ }
228
+
229
+ // Decode the set NamedBitList bits to their RFC 3161 names (bit 0 = MSB of byte 0).
230
+ // A set bit outside the defined set is an unsupported PKIFailureInfo value — a client
231
+ // MUST error on a failInfo it does not understand (RFC 3161 §2.4.2), so reject it
232
+ // rather than surfacing an opaque "bitN".
233
+ function _namedBits(bytes) {
234
+ var out = [];
235
+ for (var i = 0; i < bytes.length * 8; i++) {
236
+ if ((bytes[i >> 3] >> (7 - (i & 7))) & 1) {
237
+ var nm = FAILURE_BITS[i];
238
+ if (!nm) throw NS.E("tsp/bad-failinfo", "unsupported PKIFailureInfo bit " + i + " (RFC 3161 §2.4.2)");
239
+ out.push(nm);
240
+ }
241
+ }
242
+ return out;
243
+ }
244
+
245
+ // TimeStampResp ::= SEQUENCE { status PKIStatusInfo, timeStampToken TimeStampToken
246
+ // OPTIONAL } (RFC 3161 §2.4.2). There is NO version field. The token is a CMS
247
+ // ContentInfo, decoded via cms.parse; the status-to-token coupling is load-bearing.
248
+ var TIME_STAMP_RESP = schema.seq([
249
+ schema.field("status", PKI_STATUS_INFO),
250
+ schema.optional("timeStampToken", schema.any(), { whenUniversal: [TAGS.SEQUENCE] }),
251
+ ], {
252
+ assert: "sequence", arity: { min: 1 }, code: "tsp/bad-response", what: "TimeStampResp",
253
+ build: function (m) {
254
+ var status = m.fields.status.value.result;
255
+ var present = m.fields.timeStampToken.present;
256
+ // RFC 3161 §2.4.2 — granted / grantedWithMods carry a token; any other status
257
+ // (rejection / waiting / revocation*) MUST NOT.
258
+ var granted = status.status === 0 || status.status === 1;
259
+ if (granted && !present) throw NS.E("tsp/missing-token", "a granted TimeStampResp must carry a timeStampToken (RFC 3161 §2.4.2)");
260
+ if (!granted && present) throw NS.E("tsp/unexpected-token", "a non-granted TimeStampResp must not carry a timeStampToken (RFC 3161 §2.4.2)");
261
+ // failInfo is the reason a request was rejected, so it is present ONLY when the
262
+ // status is rejection(2) — not on granted(0/1) nor on waiting / revocation* (3/4/5).
263
+ if (status.status !== 2 && status.failInfo) throw NS.E("tsp/unexpected-failinfo", "failInfo is present only when the status is rejection(2) (RFC 3161 §2.4.2)");
264
+ // A granted response's timeStampToken MUST be a well-formed TimeStampToken —
265
+ // decode it (composing the CMS parser) rather than surfacing arbitrary SEQUENCE
266
+ // bytes as a token; a malformed token fails the response parse.
267
+ return {
268
+ status: status.status,
269
+ statusString: status.statusString,
270
+ failInfo: status.failInfo,
271
+ timeStampToken: present ? parseToken(m.fields.timeStampToken.value.bytes) : null,
272
+ };
273
+ },
274
+ });
275
+
276
+ /**
277
+ * @primitive pki.schema.tsp.parseTstInfo
278
+ * @signature pki.schema.tsp.parseTstInfo(input) -> tstInfo
279
+ * @since 0.1.13
280
+ * @status experimental
281
+ * @spec RFC 3161
282
+ * @related pki.schema.tsp.parseToken, pki.schema.tsp.parse
283
+ *
284
+ * Parse a bare `TSTInfo` payload (a DER `Buffer`) — the structure a timestamp token
285
+ * encapsulates — into `{ version, policy, messageImprint, serialNumber, genTime,
286
+ * accuracy, ordering, nonce, tsa, extensions }`. `messageImprint.hashedMessage` is
287
+ * the raw digest; `serialNumber` / `nonce` are lossless (BigInt + hex); `genTime` is
288
+ * a `Date` with sub-second precision. A malformed structure throws a typed
289
+ * `TspError` (`tsp/*`); a leaf-level codec fault surfaces as `asn1/*`.
290
+ *
291
+ * @example
292
+ * var tst = pki.schema.tsp.parseTstInfo(der);
293
+ * tst.genTime; // -> Date
294
+ * tst.messageImprint.hashedMessage; // -> Buffer (the raw digest)
295
+ */
296
+ var parseTstInfo = pkix.makeParser({ pemLabel: null, PemError: PemError, ErrorClass: TspError, prefix: "tsp", what: "TSTInfo", topSchema: TST_INFO, ns: NS });
297
+
298
+ /**
299
+ * @primitive pki.schema.tsp.parse
300
+ * @signature pki.schema.tsp.parse(input) -> timeStampResp
301
+ * @since 0.1.13
302
+ * @status experimental
303
+ * @spec RFC 3161
304
+ * @related pki.schema.tsp.parseToken, pki.schema.parse
305
+ *
306
+ * Parse a DER `Buffer` or a PEM string into a `TimeStampResp`: `{ status,
307
+ * statusString, failInfo, timeStampToken }`. The status-to-token coupling is
308
+ * enforced — a granted response (status 0/1) carries `timeStampToken` (surfaced raw
309
+ * for `parseToken`), any other status does not. `failInfo` decodes the
310
+ * `PKIFailureInfo` named bits. A malformed structure throws a typed `TspError`.
311
+ *
312
+ * @example
313
+ * var res = pki.schema.tsp.parse(der);
314
+ * res.status; // -> 0 (granted)
315
+ * res.timeStampToken.tstInfo.genTime; // -> Date (a granted token is decoded)
316
+ */
317
+ var parse = pkix.makeParser({ pemLabel: null, PemError: PemError, ErrorClass: TspError, prefix: "tsp", what: "TimeStampResp", topSchema: TIME_STAMP_RESP, ns: NS });
318
+
319
+ /**
320
+ * @primitive pki.schema.tsp.parseToken
321
+ * @signature pki.schema.tsp.parseToken(input) -> tstInfo
322
+ * @since 0.1.13
323
+ * @status experimental
324
+ * @spec RFC 3161, RFC 5652
325
+ * @related pki.schema.tsp.parse, pki.schema.cms.parse
326
+ *
327
+ * Parse a `TimeStampToken` (a DER `Buffer` or PEM) — a CMS SignedData whose
328
+ * encapsulated content is a `TSTInfo`. Composes `pki.schema.cms.parse`, asserts the
329
+ * `id-ct-TSTInfo` content type (`tsp/wrong-econtent-type`), that the content is
330
+ * attached (`tsp/detached-token`), and the single-signer rule (`tsp/multi-signer`,
331
+ * RFC 3161 §2.4.2), then decodes the inner `TSTInfo`. Returns `{ tstInfo, eContent,
332
+ * signerInfo, certificates }` — the decoded payload, the raw eContent bytes a verifier
333
+ * hashes for the CMS message-digest, and the CMS signer material.
334
+ *
335
+ * @example
336
+ * var token = pki.schema.tsp.parseToken(tokenDer);
337
+ * token.tstInfo.genTime; // -> Date
338
+ * token.signerInfo.sid; // -> the TSA signer identifier
339
+ */
340
+ function parseToken(input) {
341
+ // De-armor with TSP's label-agnostic PEM rules first (RFC 3161 has no standard
342
+ // label), THEN hand DER to the CMS parser — otherwise cms.parse would reject any
343
+ // PEM block not labeled "CMS" before the TSP checks run.
344
+ var der = pkix.coerceToDer(input, { pemLabel: null, PemError: PemError, ErrorClass: TspError, prefix: "tsp" });
345
+ // Wrap the CMS decode so tsp.parse / parseToken keep their typed-TspError contract —
346
+ // a malformed token surfaces tsp/bad-token, not a bare cms/* error.
347
+ var signed;
348
+ try { signed = cms.parse(der); }
349
+ catch (e) {
350
+ if (e instanceof TspError) throw e;
351
+ throw new TspError("tsp/bad-token", "the timeStampToken did not decode as a CMS SignedData: " + ((e && e.message) || String(e)), e);
352
+ }
353
+ var encap = signed.encapContentInfo;
354
+ if (encap.eContentType !== OID_TST_INFO) {
355
+ throw new TspError("tsp/wrong-econtent-type", "a TimeStampToken must encapsulate id-ct-TSTInfo, got " + encap.eContentType);
356
+ }
357
+ if (encap.eContent === null) throw new TspError("tsp/detached-token", "a TimeStampToken must carry attached eContent (RFC 3161 §2.4.2)");
358
+ if (signed.signerInfos.length !== 1) {
359
+ throw new TspError("tsp/multi-signer", "a TimeStampToken must contain exactly one (TSA) signerInfo (RFC 3161 §2.4.2)");
360
+ }
361
+ var tstInfo;
362
+ try { tstInfo = schema.walk(TST_INFO, asn1.decode(encap.eContent), NS); }
363
+ catch (e) {
364
+ if (e instanceof TspError) throw e;
365
+ throw new TspError("tsp/bad-der", "the encapsulated TSTInfo did not decode: " + ((e && e.message) || String(e)), e);
366
+ }
367
+ // Surface the RAW eContent (the exact DER a verifier hashes for the CMS
368
+ // message-digest signed attribute) alongside the decoded TSTInfo — a re-serialized
369
+ // tstInfo may not byte-match, so the raw bytes are the verification feed.
370
+ return { tstInfo: tstInfo.result, eContent: encap.eContent, signerInfo: signed.signerInfos[0], certificates: signed.certificates };
371
+ }
372
+
373
+ /**
374
+ * @primitive pki.schema.tsp.pemDecode
375
+ * @signature pki.schema.tsp.pemDecode(text, label?) -> Buffer
376
+ * @since 0.1.13
377
+ * @status experimental
378
+ * @spec RFC 7468, RFC 3161
379
+ * @related pki.schema.tsp.parse
380
+ *
381
+ * Extract the DER bytes from a PEM block (RFC 3161 defines no standard label, so the
382
+ * first block is taken unless `label` is given). Throws `PemError` on a missing
383
+ * envelope or a non-base64 body.
384
+ *
385
+ * @example
386
+ * var der = pki.schema.tsp.pemDecode(pemText);
387
+ */
388
+ function pemDecode(text, label) { return pkix.pemDecode(text, label || null, PemError); }
389
+
390
+ // A TimeStampResp root is a SEQUENCE of 1-2 whose first child is a PKIStatusInfo (a
391
+ // SEQUENCE whose own first child is an INTEGER status). Disjoint from the OID-first
392
+ // CMS ContentInfo, the INTEGER-first PKCS#8 key, and the exactly-3 signed-envelope
393
+ // trio, so it detects unambiguously regardless of registry order.
394
+ function matches(root) {
395
+ if (!root || root.tagClass !== "universal" || root.tagNumber !== TAGS.SEQUENCE || !root.children) return false;
396
+ var k = root.children;
397
+ if (k.length < 1 || k.length > 2) return false;
398
+ var si = k[0];
399
+ if (!(si.tagClass === "universal" && si.tagNumber === TAGS.SEQUENCE && si.children && si.children.length >= 1)) return false;
400
+ if (!(si.children[0].tagClass === "universal" && si.children[0].tagNumber === TAGS.INTEGER)) return false;
401
+ if (k.length === 2 && !(k[1].tagClass === "universal" && k[1].tagNumber === TAGS.SEQUENCE)) return false;
402
+ return true;
403
+ }
404
+
405
+ module.exports = {
406
+ parse: parse,
407
+ parseTstInfo: parseTstInfo,
408
+ parseToken: parseToken,
409
+ pemDecode: pemDecode,
410
+ matches: matches,
411
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
package/sbom.cdx.json CHANGED
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:f4519864-8754-4475-8ee0-b2556afddae1",
5
+ "serialNumber": "urn:uuid:f3d799b0-bde8-4f05-8577-e064f7a420f1",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-05T20:10:05.842Z",
8
+ "timestamp": "2026-07-06T00:04:32.672Z",
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.1.11",
22
+ "bom-ref": "@blamejs/pki@0.1.13",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.1.11",
25
+ "version": "0.1.13",
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.1.11",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.1.13",
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.1.11",
57
+ "ref": "@blamejs/pki@0.1.13",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]