@blamejs/blamejs-shop 0.0.127 → 0.0.129
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 +4 -0
- package/README.md +4 -0
- package/lib/storefront.js +235 -6
- package/lib/vendor/MANIFEST.json +3 -3
- package/lib/vendor/blamejs/CHANGELOG.md +14 -0
- package/lib/vendor/blamejs/README.md +7 -1
- package/lib/vendor/blamejs/SECURITY.md +3 -0
- package/lib/vendor/blamejs/api-snapshot.json +408 -2
- package/lib/vendor/blamejs/index.js +6 -0
- package/lib/vendor/blamejs/lib/cose.js +212 -4
- package/lib/vendor/blamejs/lib/cwt.js +244 -0
- package/lib/vendor/blamejs/lib/eat.js +240 -0
- package/lib/vendor/blamejs/lib/mdoc.js +305 -0
- package/lib/vendor/blamejs/lib/scitt.js +243 -0
- package/lib/vendor/blamejs/lib/tsa.js +688 -0
- package/lib/vendor/blamejs/lib/vc.js +328 -0
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.12.34.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.12.35.json +22 -0
- package/lib/vendor/blamejs/release-notes/v0.12.36.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.12.37.json +27 -0
- package/lib/vendor/blamejs/release-notes/v0.12.38.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.12.39.json +18 -0
- package/lib/vendor/blamejs/release-notes/v0.12.40.json +18 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +27 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cose.test.js +84 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/cwt.test.js +137 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/eat.test.js +111 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mdoc.test.js +230 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/scitt.test.js +158 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/tsa.test.js +373 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/vc.test.js +188 -0
- package/package.json +1 -1
|
@@ -0,0 +1,688 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.tsa
|
|
4
|
+
* @nav Crypto
|
|
5
|
+
* @title Timestamping (RFC 3161)
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* An RFC 3161 Time-Stamp Protocol client — the requester / verifier
|
|
9
|
+
* side, not a TSA. A timestamp authority binds a hash of your data to
|
|
10
|
+
* a trusted time, producing a timestamp token that proves the data
|
|
11
|
+
* existed at that instant: timestamp a release artifact, an audit-log
|
|
12
|
+
* checkpoint, a <code>b.scitt</code> signed statement, a contract.
|
|
13
|
+
*
|
|
14
|
+
* <code>b.tsa.buildRequest(data, opts)</code> produces the DER
|
|
15
|
+
* TimeStampReq (a message imprint = the hash of your data, plus an
|
|
16
|
+
* optional nonce and a request for the TSA's certificate);
|
|
17
|
+
* <code>b.tsa.parseResponse(der)</code> reads the TimeStampResp,
|
|
18
|
+
* surfacing the PKIStatus (and any failure-info bits) and the token;
|
|
19
|
+
* <code>b.tsa.verifyToken(token, opts)</code> verifies the token
|
|
20
|
+
* against your data and returns the asserted time. The transport (an
|
|
21
|
+
* HTTP POST of <code>application/timestamp-query</code> to the TSA's
|
|
22
|
+
* URL) is the operator's to make — the framework builds the request
|
|
23
|
+
* and verifies the response.
|
|
24
|
+
*
|
|
25
|
+
* <strong>Verification</strong> (RFC 3161 §2.4.2 / §2.3) is the
|
|
26
|
+
* security-bearing part and is done in full: the token is a CMS
|
|
27
|
+
* SignedData (<code>b.cms</code>) whose eContentType must be
|
|
28
|
+
* <code>id-ct-TSTInfo</code>; the message imprint inside the TSTInfo
|
|
29
|
+
* must equal the hash of your data (constant-time compare); a sent
|
|
30
|
+
* nonce must round-trip; the signer's certificate must carry the
|
|
31
|
+
* <code>id-kp-timeStamping</code> extended key usage, marked critical
|
|
32
|
+
* and as the <em>only</em> EKU; and the CMS signature over the signed
|
|
33
|
+
* attributes must verify (the <code>messageDigest</code> attribute is
|
|
34
|
+
* checked against the recomputed eContent digest first). An optional
|
|
35
|
+
* trust-anchor set verifies the certificate chain and validity at the
|
|
36
|
+
* asserted time.
|
|
37
|
+
*
|
|
38
|
+
* <strong>Algorithms.</strong> Timestamp tokens are third-party
|
|
39
|
+
* artifacts: public TSAs sign with classical RSA (PKCS#1 v1.5 or PSS)
|
|
40
|
+
* or ECDSA over SHA-2, so verification accepts those — the same
|
|
41
|
+
* consume-what-exists stance as <code>b.cose</code> verification. This
|
|
42
|
+
* is not a signing default (the framework is not the TSA); it is
|
|
43
|
+
* verification of externally-produced tokens. The message-imprint
|
|
44
|
+
* hash you request defaults to SHA-512 and may be any of SHA-256 /
|
|
45
|
+
* 384 / 512 or SHA3-256 / 512 the TSA supports.
|
|
46
|
+
*
|
|
47
|
+
* @card
|
|
48
|
+
* RFC 3161 timestamp client — build a TimeStampReq, parse the
|
|
49
|
+
* response, and verify a timestamp token in full (imprint match,
|
|
50
|
+
* nonce, id-kp-timeStamping EKU critical+sole, CMS signature, optional
|
|
51
|
+
* chain). Composes b.cms + the in-tree ASN.1 DER codec.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
var nodeCrypto = require("node:crypto");
|
|
55
|
+
var bCrypto = require("./crypto");
|
|
56
|
+
var asn1 = require("./asn1-der");
|
|
57
|
+
var cms = require("./cms-codec");
|
|
58
|
+
var validateOpts = require("./validate-opts");
|
|
59
|
+
var { defineClass } = require("./framework-error");
|
|
60
|
+
|
|
61
|
+
var TsaError = defineClass("TsaError", { alwaysPermanent: true });
|
|
62
|
+
|
|
63
|
+
// id-ct-TSTInfo (RFC 3161 §2.4.2) — the eContentType of a timestamp token.
|
|
64
|
+
var OID_TST_INFO = "1.2.840.113549.1.9.16.1.4";
|
|
65
|
+
// id-kp-timeStamping (RFC 3161 §2.3) — the required, critical, sole EKU.
|
|
66
|
+
var OID_KP_TIMESTAMPING = "1.3.6.1.5.5.7.3.8";
|
|
67
|
+
var OID_EXT_EKU = "2.5.29.37"; // certificate extendedKeyUsage extension
|
|
68
|
+
var OID_MESSAGE_DIGEST = "1.2.840.113549.1.9.4"; // RFC 5652 messageDigest signed attribute
|
|
69
|
+
var OID_CONTENT_TYPE = "1.2.840.113549.1.9.3"; // RFC 5652 contentType signed attribute
|
|
70
|
+
|
|
71
|
+
// Message-imprint hash algorithms: name to { oid, nodeHash }.
|
|
72
|
+
var IMPRINT_HASHES = {
|
|
73
|
+
"SHA-256": { oid: "2.16.840.1.101.3.4.2.1", nodeHash: "sha256" },
|
|
74
|
+
"SHA-384": { oid: "2.16.840.1.101.3.4.2.2", nodeHash: "sha384" },
|
|
75
|
+
"SHA-512": { oid: "2.16.840.1.101.3.4.2.3", nodeHash: "sha512" },
|
|
76
|
+
"SHA3-256": { oid: "2.16.840.1.101.3.4.2.8", nodeHash: "sha3-256" },
|
|
77
|
+
"SHA3-512": { oid: "2.16.840.1.101.3.4.2.10", nodeHash: "sha3-512" },
|
|
78
|
+
};
|
|
79
|
+
var OID_TO_IMPRINT_HASH = {};
|
|
80
|
+
Object.keys(IMPRINT_HASHES).forEach(function (n) { OID_TO_IMPRINT_HASH[IMPRINT_HASHES[n].oid] = n; });
|
|
81
|
+
|
|
82
|
+
// Signer signature algorithms a real TSA emits to node verify parameters.
|
|
83
|
+
// scheme "rsa" = PKCS#1 v1.5, "pss" = RSASSA-PSS, "ecdsa" = ECDSA(DER).
|
|
84
|
+
var SIG_ALGS = {
|
|
85
|
+
// Bare rsaEncryption — RFC 8933 / common CMS producers (incl. OpenSSL
|
|
86
|
+
// ts) put this in SignerInfo.signatureAlgorithm and take the hash from
|
|
87
|
+
// the separate digestAlgorithm field.
|
|
88
|
+
"1.2.840.113549.1.1.1": { hash: null, scheme: "rsa" }, // rsaEncryption (hash from digestAlg)
|
|
89
|
+
"1.2.840.113549.1.1.11": { hash: "sha256", scheme: "rsa" }, // sha256WithRSAEncryption
|
|
90
|
+
"1.2.840.113549.1.1.12": { hash: "sha384", scheme: "rsa" }, // sha384WithRSAEncryption
|
|
91
|
+
"1.2.840.113549.1.1.13": { hash: "sha512", scheme: "rsa" }, // sha512WithRSAEncryption
|
|
92
|
+
"1.2.840.113549.1.1.10": { hash: null, scheme: "pss" }, // RSASSA-PSS (hash from signerInfo digestAlg)
|
|
93
|
+
// Bare id-ecPublicKey appears in some producers' SignerInfo too.
|
|
94
|
+
"1.2.840.10045.2.1": { hash: null, scheme: "ecdsa" }, // id-ecPublicKey (hash from digestAlg)
|
|
95
|
+
"1.2.840.10045.4.3.2": { hash: "sha256", scheme: "ecdsa" }, // ecdsa-with-SHA256
|
|
96
|
+
"1.2.840.10045.4.3.3": { hash: "sha384", scheme: "ecdsa" }, // ecdsa-with-SHA384
|
|
97
|
+
"1.2.840.10045.4.3.4": { hash: "sha512", scheme: "ecdsa" }, // ecdsa-with-SHA512
|
|
98
|
+
};
|
|
99
|
+
var DIGEST_OID_TO_NODE = {
|
|
100
|
+
"2.16.840.1.101.3.4.2.1": "sha256",
|
|
101
|
+
"2.16.840.1.101.3.4.2.2": "sha384",
|
|
102
|
+
"2.16.840.1.101.3.4.2.3": "sha512",
|
|
103
|
+
"2.16.840.1.101.3.4.2.8": "sha3-256",
|
|
104
|
+
"2.16.840.1.101.3.4.2.10": "sha3-512",
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
function _bytes(x, what) {
|
|
108
|
+
if (Buffer.isBuffer(x)) return x;
|
|
109
|
+
if (x instanceof Uint8Array) return Buffer.from(x);
|
|
110
|
+
if (typeof x === "string") return Buffer.from(x, "utf8");
|
|
111
|
+
throw new TsaError("tsa/bad-bytes", "tsa: " + what + " must be Buffer / Uint8Array / string");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function _normHex(h) {
|
|
115
|
+
return String(h).replace(/[^0-9a-fA-F]/g, "").replace(/^0+(?=.)/, "").toUpperCase();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Resolve the message imprint: hash the data, or use a pre-computed hash.
|
|
119
|
+
function _imprint(data, opts, fnName) {
|
|
120
|
+
var hashName = opts.hashAlg || "SHA-512";
|
|
121
|
+
var h = IMPRINT_HASHES[hashName];
|
|
122
|
+
if (!h) {
|
|
123
|
+
throw new TsaError("tsa/bad-hash-alg",
|
|
124
|
+
fnName + ": hashAlg must be one of " + Object.keys(IMPRINT_HASHES).join(" / "));
|
|
125
|
+
}
|
|
126
|
+
var digest;
|
|
127
|
+
if (opts.hashed) {
|
|
128
|
+
digest = _bytes(data, "hash");
|
|
129
|
+
var expectLen = nodeCrypto.createHash(h.nodeHash).update(Buffer.alloc(0)).digest().length;
|
|
130
|
+
if (digest.length !== expectLen) {
|
|
131
|
+
throw new TsaError("tsa/bad-hash-length",
|
|
132
|
+
fnName + ": pre-hashed input is " + digest.length + " bytes, expected " +
|
|
133
|
+
expectLen + " for " + hashName);
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
digest = nodeCrypto.createHash(h.nodeHash).update(_bytes(data, "data")).digest();
|
|
137
|
+
}
|
|
138
|
+
return { hashName: hashName, hashOid: h.oid, digest: digest };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @primitive b.tsa.buildRequest
|
|
143
|
+
* @signature b.tsa.buildRequest(data, opts?)
|
|
144
|
+
* @since 0.12.38
|
|
145
|
+
* @status experimental
|
|
146
|
+
* @compliance soc2
|
|
147
|
+
* @related b.tsa.parseResponse, b.tsa.verifyToken
|
|
148
|
+
*
|
|
149
|
+
* Build a DER-encoded RFC 3161 TimeStampReq for <code>data</code>. POST
|
|
150
|
+
* the returned bytes to the TSA as <code>application/timestamp-query</code>;
|
|
151
|
+
* keep the returned <code>nonce</code> to pass to
|
|
152
|
+
* <code>verifyToken</code>. By default a random 64-bit nonce is included
|
|
153
|
+
* and the TSA is asked to return its certificate.
|
|
154
|
+
*
|
|
155
|
+
* @opts
|
|
156
|
+
* {
|
|
157
|
+
* hashAlg: string, // "SHA-512" (default) | "SHA-256" | "SHA-384" | "SHA3-256" | "SHA3-512"
|
|
158
|
+
* hashed: boolean, // true means `data` is already the digest (must match hashAlg length)
|
|
159
|
+
* reqPolicy: string, // request a specific TSA policy OID (dotted)
|
|
160
|
+
* nonce: Buffer, // explicit nonce bytes, or false to omit (default: random 8 bytes)
|
|
161
|
+
* certReq: boolean, // ask the TSA to include its cert (default true)
|
|
162
|
+
* }
|
|
163
|
+
*
|
|
164
|
+
* @example
|
|
165
|
+
* var req = b.tsa.buildRequest(releaseTarball, { hashAlg: "SHA-512" });
|
|
166
|
+
* // POST req.der to the TSA; keep req.nonce for verifyToken
|
|
167
|
+
* // → { der, nonce, hashAlg, messageImprint }
|
|
168
|
+
*/
|
|
169
|
+
function buildRequest(data, opts) {
|
|
170
|
+
opts = opts || {};
|
|
171
|
+
validateOpts.requireObject(opts, "tsa.buildRequest", TsaError);
|
|
172
|
+
validateOpts(opts, ["hashAlg", "hashed", "reqPolicy", "nonce", "certReq"], "tsa.buildRequest");
|
|
173
|
+
var imp = _imprint(data, opts, "tsa.buildRequest");
|
|
174
|
+
|
|
175
|
+
var algId = asn1.writeSequence([asn1.writeOid(imp.hashOid), asn1.writeNull()]);
|
|
176
|
+
var messageImprint = asn1.writeSequence([algId, asn1.writeOctetString(imp.digest)]);
|
|
177
|
+
|
|
178
|
+
var children = [asn1.writeInteger(Buffer.from([1])), messageImprint]; // version 1
|
|
179
|
+
if (typeof opts.reqPolicy === "string") children.push(asn1.writeOid(opts.reqPolicy));
|
|
180
|
+
|
|
181
|
+
var nonce = null;
|
|
182
|
+
if (opts.nonce !== false) {
|
|
183
|
+
nonce = Buffer.isBuffer(opts.nonce) ? opts.nonce : nodeCrypto.randomBytes(8); // allow:raw-byte-literal — RFC 3161 nonce: 64-bit random
|
|
184
|
+
children.push(asn1.writeInteger(nonce));
|
|
185
|
+
}
|
|
186
|
+
// certReq DEFAULT FALSE — only encode when true (the default we want).
|
|
187
|
+
var certReq = opts.certReq !== false;
|
|
188
|
+
if (certReq) children.push(asn1.writeBoolean(true));
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
der: asn1.writeSequence(children),
|
|
192
|
+
nonce: nonce,
|
|
193
|
+
hashAlg: imp.hashName,
|
|
194
|
+
messageImprint: imp.digest,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* @primitive b.tsa.parseResponse
|
|
200
|
+
* @signature b.tsa.parseResponse(der)
|
|
201
|
+
* @since 0.12.38
|
|
202
|
+
* @status experimental
|
|
203
|
+
* @compliance soc2
|
|
204
|
+
* @related b.tsa.buildRequest, b.tsa.verifyToken
|
|
205
|
+
*
|
|
206
|
+
* Parse a DER RFC 3161 TimeStampResp. Returns the PKIStatus and, when
|
|
207
|
+
* the request was granted, the timestamp token (the DER ContentInfo to
|
|
208
|
+
* pass to <code>verifyToken</code>). A non-granted status surfaces the
|
|
209
|
+
* status integer, any free-text, and the decoded failure-info flags
|
|
210
|
+
* rather than throwing — the caller decides how to react.
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* var resp = b.tsa.parseResponse(httpBodyBytes);
|
|
214
|
+
* if (resp.granted) { var out = b.tsa.verifyToken(resp.token, { data: tarball, nonce: req.nonce }); }
|
|
215
|
+
* // → { granted: true, status: 0, token, statusString: null, failInfo: [] }
|
|
216
|
+
*/
|
|
217
|
+
function parseResponse(der) {
|
|
218
|
+
var buf = _bytes(der, "der");
|
|
219
|
+
var root;
|
|
220
|
+
try { root = asn1.readNode(buf, 0); } catch (e) {
|
|
221
|
+
throw new TsaError("tsa/malformed", "tsa.parseResponse: not DER: " + ((e && e.message) || e));
|
|
222
|
+
}
|
|
223
|
+
if (root.tag !== asn1.TAG.SEQUENCE || root.tagClass !== asn1.TAG_CLASS.UNIVERSAL) {
|
|
224
|
+
throw new TsaError("tsa/malformed", "tsa.parseResponse: TimeStampResp must be a SEQUENCE");
|
|
225
|
+
}
|
|
226
|
+
var children = asn1.readSequence(root.value);
|
|
227
|
+
if (children.length < 1 || children[0].tag !== asn1.TAG.SEQUENCE) {
|
|
228
|
+
throw new TsaError("tsa/malformed", "tsa.parseResponse: missing PKIStatusInfo");
|
|
229
|
+
}
|
|
230
|
+
var statusInfo = asn1.readSequence(children[0].value);
|
|
231
|
+
if (statusInfo.length < 1 || statusInfo[0].tag !== asn1.TAG.INTEGER) {
|
|
232
|
+
throw new TsaError("tsa/malformed", "tsa.parseResponse: PKIStatusInfo missing status INTEGER");
|
|
233
|
+
}
|
|
234
|
+
var status = asn1.readUnsignedInt(statusInfo[0]);
|
|
235
|
+
if (typeof status !== "number") {
|
|
236
|
+
throw new TsaError("tsa/malformed", "tsa.parseResponse: status integer out of range");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
var statusString = null;
|
|
240
|
+
var failInfo = [];
|
|
241
|
+
for (var i = 1; i < statusInfo.length; i += 1) {
|
|
242
|
+
var n = statusInfo[i];
|
|
243
|
+
if (n.tag === asn1.TAG.SEQUENCE && statusString === null) {
|
|
244
|
+
var texts = asn1.readSequence(n.value).map(function (t) { return t.value.toString("utf8"); });
|
|
245
|
+
statusString = texts.join("; ");
|
|
246
|
+
} else if (n.tag === asn1.TAG.BIT_STRING) {
|
|
247
|
+
failInfo = _decodeFailInfo(n);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// granted(0) / grantedWithMods(1) carry a token; 2..5 are failures.
|
|
252
|
+
var granted = status === 0 || status === 1;
|
|
253
|
+
var token = null;
|
|
254
|
+
if (granted) {
|
|
255
|
+
var tokNode = children[1];
|
|
256
|
+
if (!tokNode) {
|
|
257
|
+
throw new TsaError("tsa/no-token",
|
|
258
|
+
"tsa.parseResponse: status granted but no timeStampToken present");
|
|
259
|
+
}
|
|
260
|
+
token = tokNode.raw;
|
|
261
|
+
}
|
|
262
|
+
return { granted: granted, status: status, statusString: statusString, failInfo: failInfo, token: token };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// PKIFailureInfo bit names (RFC 3161 §2.4.2 / RFC 2510).
|
|
266
|
+
var FAIL_INFO_BITS = { // allow:raw-byte-literal — RFC 3161 PKIFailureInfo bit positions
|
|
267
|
+
0: "badAlg", 2: "badRequest", 5: "badDataFormat", 14: "timeNotAvailable",
|
|
268
|
+
15: "unacceptedPolicy", 16: "unacceptedExtension", 17: "addInfoNotAvailable", 25: "systemFailure",
|
|
269
|
+
};
|
|
270
|
+
function _decodeFailInfo(bitStringNode) {
|
|
271
|
+
var out = [];
|
|
272
|
+
var v = bitStringNode.value;
|
|
273
|
+
if (v.length <= 1) return out; // first byte = unused-bit count
|
|
274
|
+
var bits = v.slice(1);
|
|
275
|
+
for (var byteIdx = 0; byteIdx < bits.length; byteIdx += 1) {
|
|
276
|
+
for (var b = 0; b < 8; b += 1) { // allow:raw-byte-literal — 8 bits per byte
|
|
277
|
+
if (bits[byteIdx] & (0x80 >> b)) {
|
|
278
|
+
var pos = byteIdx * 8 + b; // allow:raw-byte-literal — 8 bits per byte
|
|
279
|
+
out.push(FAIL_INFO_BITS[pos] || ("bit" + pos));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Parse GeneralizedTime "YYYYMMDDHHMMSS[.fff]Z" to Date (UTC).
|
|
287
|
+
function _parseGeneralizedTime(node) {
|
|
288
|
+
var s = node.value.toString("ascii");
|
|
289
|
+
var m = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\.\d+)?Z$/.exec(s);
|
|
290
|
+
if (!m) {
|
|
291
|
+
throw new TsaError("tsa/bad-gentime", "tsa: genTime is not a 'Z'-terminated GeneralizedTime: " + s);
|
|
292
|
+
}
|
|
293
|
+
// Fractional seconds (rare in timestamps) → milliseconds from the
|
|
294
|
+
// first three fractional digits, zero-padded; no float arithmetic.
|
|
295
|
+
var frac = m[7] ? m[7].slice(1) : "";
|
|
296
|
+
var ms = frac ? parseInt((frac + "000").slice(0, 3), 10) : 0;
|
|
297
|
+
var t = Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +m[6], ms);
|
|
298
|
+
return new Date(t);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// TSTInfo (RFC 3161 §2.4.2) to fields. messageImprint stays raw so
|
|
302
|
+
// verifyToken can compare hash-alg OID + hashed bytes.
|
|
303
|
+
function _parseTstInfo(eContent) {
|
|
304
|
+
var root = asn1.readNode(eContent, 0);
|
|
305
|
+
if (root.tag !== asn1.TAG.SEQUENCE) {
|
|
306
|
+
throw new TsaError("tsa/malformed", "tsa: TSTInfo must be a SEQUENCE");
|
|
307
|
+
}
|
|
308
|
+
var c = asn1.readSequence(root.value);
|
|
309
|
+
if (c.length < 5) throw new TsaError("tsa/malformed", "tsa: TSTInfo too short");
|
|
310
|
+
var idx = 0;
|
|
311
|
+
idx += 1; // version
|
|
312
|
+
var policy = asn1.readOid(c[idx]); idx += 1;
|
|
313
|
+
var miNode = c[idx]; idx += 1;
|
|
314
|
+
var serialNode = c[idx]; idx += 1;
|
|
315
|
+
var genTime = _parseGeneralizedTime(c[idx]); idx += 1;
|
|
316
|
+
|
|
317
|
+
// Parse messageImprint { hashAlgorithm AlgId, hashedMessage OCTET STRING }.
|
|
318
|
+
var mi = asn1.readSequence(miNode.value);
|
|
319
|
+
var miAlg = asn1.readSequence(mi[0].value);
|
|
320
|
+
var miHashOid = asn1.readOid(miAlg[0]);
|
|
321
|
+
var miHash = asn1.readOctetString(mi[1]);
|
|
322
|
+
|
|
323
|
+
var accuracy = null;
|
|
324
|
+
var nonce = null;
|
|
325
|
+
for (; idx < c.length; idx += 1) {
|
|
326
|
+
var n = c[idx];
|
|
327
|
+
if (n.tagClass === asn1.TAG_CLASS.UNIVERSAL && n.tag === asn1.TAG.SEQUENCE && accuracy === null) {
|
|
328
|
+
accuracy = _parseAccuracy(n);
|
|
329
|
+
} else if (n.tagClass === asn1.TAG_CLASS.UNIVERSAL && n.tag === asn1.TAG.INTEGER) {
|
|
330
|
+
nonce = n.value; // raw INTEGER bytes
|
|
331
|
+
}
|
|
332
|
+
// ordering BOOLEAN, [0] tsa, [1] extensions are read but not surfaced in v1.
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
var serialHex = Buffer.isBuffer(serialNode.value) ? serialNode.value.toString("hex") : null;
|
|
336
|
+
return {
|
|
337
|
+
policy: policy,
|
|
338
|
+
genTime: genTime,
|
|
339
|
+
accuracy: accuracy,
|
|
340
|
+
nonce: nonce,
|
|
341
|
+
serialHex: serialHex,
|
|
342
|
+
imprintHashOid: miHashOid,
|
|
343
|
+
imprintHash: miHash,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function _parseAccuracy(node) {
|
|
348
|
+
var c = asn1.readSequence(node.value);
|
|
349
|
+
var out = { seconds: 0, millis: 0, micros: 0 };
|
|
350
|
+
for (var i = 0; i < c.length; i += 1) {
|
|
351
|
+
var n = c[i];
|
|
352
|
+
if (n.tagClass === asn1.TAG_CLASS.UNIVERSAL && n.tag === asn1.TAG.INTEGER) {
|
|
353
|
+
out.seconds = asn1.readUnsignedInt(n);
|
|
354
|
+
} else if (n.tagClass === asn1.TAG_CLASS.CONTEXT_SPECIFIC && n.tag === 0) {
|
|
355
|
+
out.millis = _ctxInt(n);
|
|
356
|
+
} else if (n.tagClass === asn1.TAG_CLASS.CONTEXT_SPECIFIC && n.tag === 1) {
|
|
357
|
+
out.micros = _ctxInt(n);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return out;
|
|
361
|
+
}
|
|
362
|
+
function _ctxInt(node) {
|
|
363
|
+
// [n] IMPLICIT INTEGER — value bytes are the integer content directly.
|
|
364
|
+
var v = node.value, n = 0;
|
|
365
|
+
for (var i = 0; i < v.length; i += 1) n = (n * 256) + v[i]; // allow:raw-byte-literal — base-256 integer accumulation
|
|
366
|
+
return n;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Walk a certificate's extensions for one OID to { critical, valueBytes } or null.
|
|
370
|
+
function _certExtension(certDer, wantOid) {
|
|
371
|
+
var cert = asn1.readNode(certDer, 0);
|
|
372
|
+
var top = asn1.readSequence(cert.value); // [ tbs, sigAlg, sigValue ]
|
|
373
|
+
var tbs = asn1.readSequence(top[0].value);
|
|
374
|
+
// Find the [3] EXPLICIT extensions wrapper.
|
|
375
|
+
var extsWrapper = asn1.findChild(tbs, function (n) {
|
|
376
|
+
return n.tagClass === asn1.TAG_CLASS.CONTEXT_SPECIFIC && n.tag === 3;
|
|
377
|
+
});
|
|
378
|
+
if (!extsWrapper) return null;
|
|
379
|
+
var extsSeq = asn1.unwrapExplicit(extsWrapper, 3);
|
|
380
|
+
var exts = asn1.readSequence(extsSeq.value);
|
|
381
|
+
for (var i = 0; i < exts.length; i += 1) {
|
|
382
|
+
var ec = asn1.readSequence(exts[i].value);
|
|
383
|
+
var oid = asn1.readOid(ec[0]);
|
|
384
|
+
if (oid !== wantOid) continue;
|
|
385
|
+
var critical = false;
|
|
386
|
+
var valueNode = null;
|
|
387
|
+
for (var j = 1; j < ec.length; j += 1) {
|
|
388
|
+
if (ec[j].tag === asn1.TAG.BOOLEAN) critical = ec[j].value[0] !== 0;
|
|
389
|
+
else if (ec[j].tag === asn1.TAG.OCTET_STRING) valueNode = ec[j];
|
|
390
|
+
}
|
|
391
|
+
if (!valueNode) return null;
|
|
392
|
+
return { critical: critical, valueBytes: asn1.readOctetString(valueNode) };
|
|
393
|
+
}
|
|
394
|
+
return null;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// RFC 3161 §2.3: the signing cert's EKU MUST contain id-kp-timeStamping,
|
|
398
|
+
// MUST be critical, and MUST be the only key purpose.
|
|
399
|
+
function _checkTimestampingEku(certDer) {
|
|
400
|
+
var ext = _certExtension(certDer, OID_EXT_EKU);
|
|
401
|
+
if (!ext) {
|
|
402
|
+
throw new TsaError("tsa/bad-eku",
|
|
403
|
+
"tsa.verifyToken: signer certificate has no extendedKeyUsage (RFC 3161 §2.3 requires id-kp-timeStamping)");
|
|
404
|
+
}
|
|
405
|
+
if (!ext.critical) {
|
|
406
|
+
throw new TsaError("tsa/bad-eku",
|
|
407
|
+
"tsa.verifyToken: extendedKeyUsage is not marked critical (RFC 3161 §2.3)");
|
|
408
|
+
}
|
|
409
|
+
var purposes = asn1.readSequence(asn1.readNode(ext.valueBytes, 0).value).map(asn1.readOid);
|
|
410
|
+
if (purposes.length !== 1 || purposes[0] !== OID_KP_TIMESTAMPING) {
|
|
411
|
+
throw new TsaError("tsa/bad-eku",
|
|
412
|
+
"tsa.verifyToken: extendedKeyUsage must be exactly { id-kp-timeStamping } (got " +
|
|
413
|
+
purposes.join(", ") + ")");
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Pick the signing cert from the token: prefer the one whose serial
|
|
418
|
+
// matches the SignerInfo sid (IssuerAndSerialNumber); else fall through
|
|
419
|
+
// to all certs (the EKU + signature checks remain authoritative).
|
|
420
|
+
function _candidateSigners(sidRaw, certs) {
|
|
421
|
+
var wantSerial = null;
|
|
422
|
+
try {
|
|
423
|
+
var sid = asn1.readNode(sidRaw, 0);
|
|
424
|
+
if (sid.tag === asn1.TAG.SEQUENCE && sid.tagClass === asn1.TAG_CLASS.UNIVERSAL) {
|
|
425
|
+
var parts = asn1.readSequence(sid.value); // IssuerAndSerialNumber { issuer, serial }
|
|
426
|
+
var serialNode = parts[parts.length - 1];
|
|
427
|
+
if (serialNode && serialNode.tag === asn1.TAG.INTEGER) {
|
|
428
|
+
wantSerial = _normHex(serialNode.value.toString("hex"));
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
} catch (_e) { wantSerial = null; }
|
|
432
|
+
if (wantSerial === null) return certs.slice();
|
|
433
|
+
var matched = certs.filter(function (d) {
|
|
434
|
+
try { return _normHex(new nodeCrypto.X509Certificate(d).serialNumber) === wantSerial; }
|
|
435
|
+
catch (_e) { return false; }
|
|
436
|
+
});
|
|
437
|
+
return matched.length ? matched : certs.slice();
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function _verifyCmsSignature(si, eContent, signerCertDer) {
|
|
441
|
+
var alg = SIG_ALGS[si.sigAlgOid];
|
|
442
|
+
if (!alg) {
|
|
443
|
+
throw new TsaError("tsa/bad-sig-alg",
|
|
444
|
+
"tsa.verifyToken: signer signature algorithm " + si.sigAlgOid +
|
|
445
|
+
" is not a supported RSA / ECDSA timestamp algorithm");
|
|
446
|
+
}
|
|
447
|
+
if (!si.signedAttrsRaw) {
|
|
448
|
+
throw new TsaError("tsa/no-signed-attrs",
|
|
449
|
+
"tsa.verifyToken: SignerInfo has no signed attributes (RFC 3161 tokens always sign attributes)");
|
|
450
|
+
}
|
|
451
|
+
var digestNode = DIGEST_OID_TO_NODE[si.digestAlgOid];
|
|
452
|
+
if (!digestNode) {
|
|
453
|
+
throw new TsaError("tsa/bad-digest",
|
|
454
|
+
"tsa.verifyToken: SignerInfo digest algorithm " + si.digestAlgOid + " unsupported");
|
|
455
|
+
}
|
|
456
|
+
// The eContent digest must equal the messageDigest signed attribute,
|
|
457
|
+
// and the contentType attribute must be id-ct-TSTInfo.
|
|
458
|
+
var attrs = _parseSignedAttrs(si.signedAttrsRaw);
|
|
459
|
+
if (!attrs.messageDigest) {
|
|
460
|
+
throw new TsaError("tsa/no-message-digest", "tsa.verifyToken: signed attrs missing messageDigest");
|
|
461
|
+
}
|
|
462
|
+
var actual = nodeCrypto.createHash(digestNode).update(eContent).digest();
|
|
463
|
+
if (!bCrypto.timingSafeEqual(actual, attrs.messageDigest)) {
|
|
464
|
+
throw new TsaError("tsa/message-digest-mismatch",
|
|
465
|
+
"tsa.verifyToken: recomputed eContent digest does not match the messageDigest attribute");
|
|
466
|
+
}
|
|
467
|
+
if (attrs.contentType && attrs.contentType !== OID_TST_INFO) {
|
|
468
|
+
throw new TsaError("tsa/bad-content-type-attr",
|
|
469
|
+
"tsa.verifyToken: signed contentType attribute is " + attrs.contentType + ", expected id-ct-TSTInfo");
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
var pubKey = new nodeCrypto.X509Certificate(signerCertDer).publicKey;
|
|
473
|
+
// For algorithm ids that don't name the hash (bare rsaEncryption /
|
|
474
|
+
// id-ecPublicKey / RSASSA-PSS), the hash comes from the digestAlgorithm.
|
|
475
|
+
var hashName = alg.hash || digestNode;
|
|
476
|
+
var keyParam = pubKey;
|
|
477
|
+
if (alg.scheme === "pss") {
|
|
478
|
+
keyParam = { key: pubKey, padding: nodeCrypto.constants.RSA_PKCS1_PSS_PADDING,
|
|
479
|
+
saltLength: nodeCrypto.constants.RSA_PSS_SALTLEN_AUTO };
|
|
480
|
+
}
|
|
481
|
+
var ok;
|
|
482
|
+
try { ok = nodeCrypto.verify(hashName, si.signedAttrsRaw, keyParam, si.signature); }
|
|
483
|
+
catch (e) {
|
|
484
|
+
throw new TsaError("tsa/verify-threw",
|
|
485
|
+
"tsa.verifyToken: signature verification threw: " + ((e && e.message) || e));
|
|
486
|
+
}
|
|
487
|
+
if (!ok) {
|
|
488
|
+
throw new TsaError("tsa/bad-signature",
|
|
489
|
+
"tsa.verifyToken: CMS signature over the signed attributes did not verify");
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Parse the universal-SET signedAttrs bytes for contentType + messageDigest.
|
|
494
|
+
function _parseSignedAttrs(signedAttrsRaw) {
|
|
495
|
+
var set = asn1.readNode(signedAttrsRaw, 0);
|
|
496
|
+
var attrs = asn1.readSequence(set.value);
|
|
497
|
+
var out = { contentType: null, messageDigest: null };
|
|
498
|
+
for (var i = 0; i < attrs.length; i += 1) {
|
|
499
|
+
var a = asn1.readSequence(attrs[i].value); // Attribute { type OID, values SET }
|
|
500
|
+
var type = asn1.readOid(a[0]);
|
|
501
|
+
var valueSet = asn1.readSequence(a[1].value);
|
|
502
|
+
if (!valueSet.length) continue;
|
|
503
|
+
if (type === OID_MESSAGE_DIGEST) out.messageDigest = asn1.readOctetString(valueSet[0]);
|
|
504
|
+
else if (type === OID_CONTENT_TYPE) out.contentType = asn1.readOid(valueSet[0]);
|
|
505
|
+
}
|
|
506
|
+
return out;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Optional: verify the signer chains to a trust anchor and is valid at `at`.
|
|
510
|
+
function _verifyChain(signerCertDer, tokenCerts, trustAnchorsPem, at) {
|
|
511
|
+
var anchors = trustAnchorsPem.map(function (p) { return new nodeCrypto.X509Certificate(p); });
|
|
512
|
+
var pool = tokenCerts.map(function (d) { return new nodeCrypto.X509Certificate(d); });
|
|
513
|
+
var current = new nodeCrypto.X509Certificate(signerCertDer);
|
|
514
|
+
var seen = 0;
|
|
515
|
+
var atTime = at.getTime();
|
|
516
|
+
while (seen <= pool.length + 1) {
|
|
517
|
+
_assertValidAt(current, atTime);
|
|
518
|
+
// Anchor reached?
|
|
519
|
+
for (var a = 0; a < anchors.length; a += 1) {
|
|
520
|
+
if (_issued(anchors[a], current)) { _assertValidAt(anchors[a], atTime); return; }
|
|
521
|
+
if (current.fingerprint256 === anchors[a].fingerprint256) return;
|
|
522
|
+
}
|
|
523
|
+
// Walk up through the token's intermediates.
|
|
524
|
+
var parent = null;
|
|
525
|
+
for (var p = 0; p < pool.length; p += 1) {
|
|
526
|
+
if (pool[p].fingerprint256 !== current.fingerprint256 && _issued(pool[p], current)) {
|
|
527
|
+
parent = pool[p]; break;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (!parent) {
|
|
531
|
+
throw new TsaError("tsa/untrusted-chain",
|
|
532
|
+
"tsa.verifyToken: signer certificate does not chain to any supplied trust anchor");
|
|
533
|
+
}
|
|
534
|
+
current = parent;
|
|
535
|
+
seen += 1;
|
|
536
|
+
}
|
|
537
|
+
throw new TsaError("tsa/chain-loop", "tsa.verifyToken: certificate chain did not terminate");
|
|
538
|
+
}
|
|
539
|
+
function _issued(issuer, subject) {
|
|
540
|
+
try { return subject.checkIssued(issuer) && subject.verify(issuer.publicKey); }
|
|
541
|
+
catch (_e) { return false; }
|
|
542
|
+
}
|
|
543
|
+
function _assertValidAt(cert, atMs) {
|
|
544
|
+
if (atMs < cert.validFromDate.getTime() || atMs > cert.validToDate.getTime()) {
|
|
545
|
+
throw new TsaError("tsa/cert-expired",
|
|
546
|
+
"tsa.verifyToken: certificate '" + cert.subject + "' is not valid at the asserted time");
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* @primitive b.tsa.verifyToken
|
|
552
|
+
* @signature b.tsa.verifyToken(token, opts)
|
|
553
|
+
* @since 0.12.38
|
|
554
|
+
* @status experimental
|
|
555
|
+
* @compliance soc2
|
|
556
|
+
* @related b.tsa.buildRequest, b.cms.parseSignedData
|
|
557
|
+
*
|
|
558
|
+
* Verify an RFC 3161 timestamp token against your data and return the
|
|
559
|
+
* asserted time. Performs the full §2.4.2 / §2.3 check: eContentType is
|
|
560
|
+
* <code>id-ct-TSTInfo</code>, the message imprint equals the hash of
|
|
561
|
+
* <code>opts.data</code> (or <code>opts.hash</code>), a sent nonce
|
|
562
|
+
* round-trips, the signer cert's extendedKeyUsage is a critical, sole
|
|
563
|
+
* <code>id-kp-timeStamping</code>, and the CMS signature verifies. Pass
|
|
564
|
+
* <code>opts.trustAnchorsPem</code> to also verify the certificate
|
|
565
|
+
* chain and validity at the asserted time.
|
|
566
|
+
*
|
|
567
|
+
* @opts
|
|
568
|
+
* {
|
|
569
|
+
* data: Buffer, // the timestamped data (hashed with hashAlg)
|
|
570
|
+
* hash: Buffer, // OR a pre-computed digest (with hashAlg)
|
|
571
|
+
* hashAlg: string, // default "SHA-512" — must match the imprint
|
|
572
|
+
* nonce: Buffer, // require the token nonce to match (from buildRequest)
|
|
573
|
+
* trustAnchorsPem: string|string[], // PEM root(s) — enables chain + validity verification
|
|
574
|
+
* at: Date, // validity instant for chain check (default: genTime); must be a valid Date
|
|
575
|
+
* }
|
|
576
|
+
*
|
|
577
|
+
* @example
|
|
578
|
+
* var out = b.tsa.verifyToken(resp.token, { data: tarball, hashAlg: "SHA-512", nonce: req.nonce });
|
|
579
|
+
* // → { genTime, policy, serialHex, accuracy, hashAlg, signerCertPem }
|
|
580
|
+
*/
|
|
581
|
+
function verifyToken(token, opts) {
|
|
582
|
+
validateOpts.requireObject(opts, "tsa.verifyToken", TsaError);
|
|
583
|
+
validateOpts(opts, ["data", "hash", "hashAlg", "nonce", "trustAnchorsPem", "at"], "tsa.verifyToken");
|
|
584
|
+
if (opts.data == null && opts.hash == null) {
|
|
585
|
+
throw new TsaError("tsa/no-data", "tsa.verifyToken: pass opts.data or opts.hash to bind the token");
|
|
586
|
+
}
|
|
587
|
+
var imp = _imprint(opts.hash != null ? opts.hash : opts.data,
|
|
588
|
+
{ hashAlg: opts.hashAlg, hashed: opts.hash != null }, "tsa.verifyToken");
|
|
589
|
+
|
|
590
|
+
var sd;
|
|
591
|
+
try { sd = cms.parseSignedData(_bytes(token, "token")); }
|
|
592
|
+
catch (e) {
|
|
593
|
+
throw new TsaError("tsa/not-cms", "tsa.verifyToken: token is not CMS SignedData: " + ((e && e.message) || e));
|
|
594
|
+
}
|
|
595
|
+
if (sd.encapContent.eContentType !== OID_TST_INFO) {
|
|
596
|
+
throw new TsaError("tsa/not-tst",
|
|
597
|
+
"tsa.verifyToken: eContentType is " + sd.encapContent.eContentType + ", expected id-ct-TSTInfo");
|
|
598
|
+
}
|
|
599
|
+
if (!sd.encapContent.eContent) {
|
|
600
|
+
throw new TsaError("tsa/detached", "tsa.verifyToken: timestamp token has no embedded TSTInfo (detached not allowed)");
|
|
601
|
+
}
|
|
602
|
+
if (!sd.signerInfos.length) {
|
|
603
|
+
throw new TsaError("tsa/no-signer", "tsa.verifyToken: token has no SignerInfo");
|
|
604
|
+
}
|
|
605
|
+
if (!sd.certificates.length) {
|
|
606
|
+
throw new TsaError("tsa/no-cert",
|
|
607
|
+
"tsa.verifyToken: token carries no certificate — request one with certReq (the default)");
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
var tst = _parseTstInfo(sd.encapContent.eContent);
|
|
611
|
+
|
|
612
|
+
// (3) message imprint must match the data.
|
|
613
|
+
if (OID_TO_IMPRINT_HASH[tst.imprintHashOid] !== imp.hashName) {
|
|
614
|
+
throw new TsaError("tsa/imprint-alg-mismatch",
|
|
615
|
+
"tsa.verifyToken: token imprint hash (" + tst.imprintHashOid + ") differs from " + imp.hashName);
|
|
616
|
+
}
|
|
617
|
+
if (!bCrypto.timingSafeEqual(tst.imprintHash, imp.digest)) {
|
|
618
|
+
throw new TsaError("tsa/imprint-mismatch",
|
|
619
|
+
"tsa.verifyToken: token message imprint does not match the supplied data");
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// (4) nonce round-trip.
|
|
623
|
+
if (opts.nonce != null) {
|
|
624
|
+
var want = _bytes(opts.nonce, "nonce");
|
|
625
|
+
var got = tst.nonce == null ? Buffer.alloc(0) : Buffer.from(tst.nonce);
|
|
626
|
+
// Compare as unsigned integers (ignore a DER sign-pad byte difference).
|
|
627
|
+
if (_normHex(want.toString("hex")) !== _normHex(got.toString("hex"))) {
|
|
628
|
+
throw new TsaError("tsa/nonce-mismatch", "tsa.verifyToken: token nonce does not match the request nonce");
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// (5)+(6) signer cert + EKU + CMS signature.
|
|
633
|
+
var si = sd.signerInfos[0];
|
|
634
|
+
var candidates = _candidateSigners(si.sid, sd.certificates);
|
|
635
|
+
var signerCertDer = null;
|
|
636
|
+
var lastErr = null;
|
|
637
|
+
for (var i = 0; i < candidates.length; i += 1) {
|
|
638
|
+
try {
|
|
639
|
+
_checkTimestampingEku(candidates[i]);
|
|
640
|
+
_verifyCmsSignature(si, sd.encapContent.eContent, candidates[i]);
|
|
641
|
+
signerCertDer = candidates[i];
|
|
642
|
+
break;
|
|
643
|
+
} catch (e) { lastErr = e; }
|
|
644
|
+
}
|
|
645
|
+
if (!signerCertDer) {
|
|
646
|
+
throw lastErr || new TsaError("tsa/no-valid-signer",
|
|
647
|
+
"tsa.verifyToken: no certificate in the token both carries the timestamping EKU and verifies the signature");
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// (7) optional chain + validity. Accept a single PEM string or an
|
|
651
|
+
// array — never silently skip chain verification when the caller
|
|
652
|
+
// supplied an anchor in an unexpected shape (a fail-open).
|
|
653
|
+
if (opts.trustAnchorsPem !== undefined && opts.trustAnchorsPem !== null) {
|
|
654
|
+
var anchors = typeof opts.trustAnchorsPem === "string" ? [opts.trustAnchorsPem] : opts.trustAnchorsPem;
|
|
655
|
+
if (!Array.isArray(anchors) || anchors.length === 0 ||
|
|
656
|
+
!anchors.every(function (a) { return typeof a === "string" && a.length > 0; })) {
|
|
657
|
+
throw new TsaError("tsa/bad-trust-anchors",
|
|
658
|
+
"tsa.verifyToken: trustAnchorsPem must be a non-empty PEM string or array of PEM strings");
|
|
659
|
+
}
|
|
660
|
+
// A supplied opts.at must be a valid Date — an Invalid Date would
|
|
661
|
+
// make every validity-window comparison NaN (silently disabling it).
|
|
662
|
+
var at = tst.genTime;
|
|
663
|
+
if (opts.at !== undefined && opts.at !== null) {
|
|
664
|
+
if (!(opts.at instanceof Date) || !isFinite(opts.at.getTime())) {
|
|
665
|
+
throw new TsaError("tsa/bad-at", "tsa.verifyToken: opts.at must be a valid Date");
|
|
666
|
+
}
|
|
667
|
+
at = opts.at;
|
|
668
|
+
}
|
|
669
|
+
_verifyChain(signerCertDer, sd.certificates, anchors, at);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
return {
|
|
673
|
+
genTime: tst.genTime,
|
|
674
|
+
policy: tst.policy,
|
|
675
|
+
serialHex: tst.serialHex,
|
|
676
|
+
accuracy: tst.accuracy,
|
|
677
|
+
hashAlg: imp.hashName,
|
|
678
|
+
signerCertPem: new nodeCrypto.X509Certificate(signerCertDer).toString(),
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
module.exports = {
|
|
683
|
+
buildRequest: buildRequest,
|
|
684
|
+
parseResponse: parseResponse,
|
|
685
|
+
verifyToken: verifyToken,
|
|
686
|
+
IMPRINT_HASHES: IMPRINT_HASHES,
|
|
687
|
+
TsaError: TsaError,
|
|
688
|
+
};
|