@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.
- package/CHANGELOG.md +20 -0
- package/README.md +32 -22
- package/lib/asn1-der.js +41 -4
- package/lib/framework-error.js +12 -0
- package/lib/oid.js +36 -5
- package/lib/schema-all.js +43 -3
- package/lib/schema-attrcert.js +383 -0
- package/lib/schema-engine.js +16 -2
- package/lib/schema-ocsp.js +4 -53
- package/lib/schema-pkix.js +78 -0
- package/lib/schema-tsp.js +411 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
|
@@ -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
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:7865e3f3-f43e-4cdb-818f-8a8fe2cf2d7a",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
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.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.1.14",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.1.
|
|
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.
|
|
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.
|
|
57
|
+
"ref": "@blamejs/pki@0.1.14",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|