@blamejs/pki 0.1.12 → 0.1.14

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.
@@ -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.12",
3
+ "version": "0.1.14",
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:88d4b070-1605-4f2d-84b3-a7851ac00842",
5
+ "serialNumber": "urn:uuid:7865e3f3-f43e-4cdb-818f-8a8fe2cf2d7a",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-05T20:47:24.516Z",
8
+ "timestamp": "2026-07-06T02:35:05.577Z",
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.12",
22
+ "bom-ref": "@blamejs/pki@0.1.14",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.1.12",
25
+ "version": "0.1.14",
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.12",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.1.14",
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.12",
57
+ "ref": "@blamejs/pki@0.1.14",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]