@attestplane/attestplane 0.0.1 → 0.0.4-alpha

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.
Files changed (106) hide show
  1. package/README.md +23 -9
  2. package/dist/adapter_conformance.d.ts +46 -0
  3. package/dist/adapter_conformance.d.ts.map +1 -0
  4. package/dist/adapter_conformance.js +160 -0
  5. package/dist/adapter_conformance.js.map +1 -0
  6. package/dist/adapters/langfuse.d.ts +51 -0
  7. package/dist/adapters/langfuse.d.ts.map +1 -0
  8. package/dist/adapters/langfuse.js +157 -0
  9. package/dist/adapters/langfuse.js.map +1 -0
  10. package/dist/adapters/langsmith.d.ts +53 -0
  11. package/dist/adapters/langsmith.d.ts.map +1 -0
  12. package/dist/adapters/langsmith.js +173 -0
  13. package/dist/adapters/langsmith.js.map +1 -0
  14. package/dist/adapters.d.ts +88 -0
  15. package/dist/adapters.d.ts.map +1 -0
  16. package/dist/adapters.js +109 -0
  17. package/dist/adapters.js.map +1 -0
  18. package/dist/anchoring.d.ts +119 -0
  19. package/dist/anchoring.d.ts.map +1 -0
  20. package/dist/anchoring.js +340 -0
  21. package/dist/anchoring.js.map +1 -0
  22. package/dist/canonical.d.ts +11 -2
  23. package/dist/canonical.d.ts.map +1 -1
  24. package/dist/canonical.js +44 -31
  25. package/dist/canonical.js.map +1 -1
  26. package/dist/canonical_text.d.ts +30 -0
  27. package/dist/canonical_text.d.ts.map +1 -0
  28. package/dist/canonical_text.js +100 -0
  29. package/dist/canonical_text.js.map +1 -0
  30. package/dist/der.d.ts +55 -0
  31. package/dist/der.d.ts.map +1 -0
  32. package/dist/der.js +200 -0
  33. package/dist/der.js.map +1 -0
  34. package/dist/event_payloads.d.ts +118 -0
  35. package/dist/event_payloads.d.ts.map +1 -0
  36. package/dist/event_payloads.js +348 -0
  37. package/dist/event_payloads.js.map +1 -0
  38. package/dist/event_types.d.ts +47 -0
  39. package/dist/event_types.d.ts.map +1 -0
  40. package/dist/event_types.js +63 -0
  41. package/dist/event_types.js.map +1 -0
  42. package/dist/hashchain.d.ts +1 -0
  43. package/dist/hashchain.d.ts.map +1 -1
  44. package/dist/hashchain.js +25 -1
  45. package/dist/hashchain.js.map +1 -1
  46. package/dist/index.d.ts +23 -2
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +24 -2
  49. package/dist/index.js.map +1 -1
  50. package/dist/index_version.d.ts +9 -0
  51. package/dist/index_version.d.ts.map +1 -0
  52. package/dist/index_version.js +11 -0
  53. package/dist/index_version.js.map +1 -0
  54. package/dist/intoto.d.ts +48 -0
  55. package/dist/intoto.d.ts.map +1 -0
  56. package/dist/intoto.js +106 -0
  57. package/dist/intoto.js.map +1 -0
  58. package/dist/obligations.d.ts +41 -0
  59. package/dist/obligations.d.ts.map +1 -0
  60. package/dist/obligations.js +312 -0
  61. package/dist/obligations.js.map +1 -0
  62. package/dist/proof_bundle.d.ts +186 -0
  63. package/dist/proof_bundle.d.ts.map +1 -0
  64. package/dist/proof_bundle.js +299 -0
  65. package/dist/proof_bundle.js.map +1 -0
  66. package/dist/reason_codes.d.ts +38 -0
  67. package/dist/reason_codes.d.ts.map +1 -0
  68. package/dist/reason_codes.js +97 -0
  69. package/dist/reason_codes.js.map +1 -0
  70. package/dist/replay_verifier.d.ts +43 -0
  71. package/dist/replay_verifier.d.ts.map +1 -0
  72. package/dist/replay_verifier.js +98 -0
  73. package/dist/replay_verifier.js.map +1 -0
  74. package/dist/rfc3161.d.ts +52 -0
  75. package/dist/rfc3161.d.ts.map +1 -0
  76. package/dist/rfc3161.js +640 -0
  77. package/dist/rfc3161.js.map +1 -0
  78. package/dist/settlement_verifier.d.ts +34 -0
  79. package/dist/settlement_verifier.d.ts.map +1 -0
  80. package/dist/settlement_verifier.js +139 -0
  81. package/dist/settlement_verifier.js.map +1 -0
  82. package/dist/signing/base.d.ts +101 -0
  83. package/dist/signing/base.d.ts.map +1 -0
  84. package/dist/signing/base.js +144 -0
  85. package/dist/signing/base.js.map +1 -0
  86. package/dist/signing/providers.d.ts +113 -0
  87. package/dist/signing/providers.d.ts.map +1 -0
  88. package/dist/signing/providers.js +230 -0
  89. package/dist/signing/providers.js.map +1 -0
  90. package/dist/signing/signer.d.ts +66 -0
  91. package/dist/signing/signer.d.ts.map +1 -0
  92. package/dist/signing/signer.js +146 -0
  93. package/dist/signing/signer.js.map +1 -0
  94. package/dist/signing/trust_roots.d.ts +71 -0
  95. package/dist/signing/trust_roots.d.ts.map +1 -0
  96. package/dist/signing/trust_roots.js +267 -0
  97. package/dist/signing/trust_roots.js.map +1 -0
  98. package/dist/signing/verifier_ext.d.ts +77 -0
  99. package/dist/signing/verifier_ext.d.ts.map +1 -0
  100. package/dist/signing/verifier_ext.js +340 -0
  101. package/dist/signing/verifier_ext.js.map +1 -0
  102. package/dist/verifier.d.ts +39 -0
  103. package/dist/verifier.d.ts.map +1 -0
  104. package/dist/verifier.js +374 -0
  105. package/dist/verifier.js.map +1 -0
  106. package/package.json +2 -2
@@ -0,0 +1,640 @@
1
+ // SPDX-FileCopyrightText: 2026 The Attestplane Authors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * RFC-3161 TimeStampResp parsing + TimeStampToken signature verification
5
+ * (TypeScript port of `sdk/python/src/attestplane/anchoring/rfc3161.py`).
6
+ *
7
+ * Built on `src/der.ts` (hand-rolled DER reader) + Node's stdlib
8
+ * `crypto.verify` for RSA/ECDSA CMS signatures. No additional npm
9
+ * dependencies — the TypeScript SDK keeps the same lean dep tree as
10
+ * v0.0.1-alpha while gaining real signature verification.
11
+ *
12
+ * The parser handles the specific structures defined in RFC-3161,
13
+ * RFC-5652 (CMS SignedData), and the X.509 subset needed to extract
14
+ * the public key from a leaf cert's SubjectPublicKeyInfo.
15
+ *
16
+ * Cross-language conformance: TS reproduces Python's
17
+ * parse_timestamp_response semantics byte-for-byte. The
18
+ * anchor_vectors.json fixtures verify in both languages.
19
+ */
20
+ import { createHash, createPublicKey, createVerify } from 'node:crypto';
21
+ import { AnchorVerificationError } from './anchoring.js';
22
+ import { DerParseError, getValueBytes, readBitString, readGeneralizedTime, readInteger, readOctetString, readOid, readSequence, readTlv, tlvDer, } from './der.js';
23
+ // Well-known OIDs used in the structures we parse.
24
+ const OID_SIGNED_DATA = '1.2.840.113549.1.7.2';
25
+ const OID_TST_INFO = '1.2.840.113549.1.9.16.1.4';
26
+ const OID_CONTENT_TYPE_ATTR = '1.2.840.113549.1.9.3';
27
+ const OID_MESSAGE_DIGEST_ATTR = '1.2.840.113549.1.9.4';
28
+ const OID_SHA256 = '2.16.840.1.101.3.4.2.1';
29
+ const OID_SHA384 = '2.16.840.1.101.3.4.2.2';
30
+ const OID_SHA512 = '2.16.840.1.101.3.4.2.3';
31
+ const OID_RSA_PKCS1 = '1.2.840.113549.1.1.1';
32
+ const OID_SHA256_RSA = '1.2.840.113549.1.1.11';
33
+ const OID_SHA384_RSA = '1.2.840.113549.1.1.12';
34
+ const OID_SHA512_RSA = '1.2.840.113549.1.1.13';
35
+ const OID_SHA256_ECDSA = '1.2.840.10045.4.3.2';
36
+ const OID_SHA384_ECDSA = '1.2.840.10045.4.3.3';
37
+ const OID_SHA512_ECDSA = '1.2.840.10045.4.3.4';
38
+ function topSequence(buffer) {
39
+ const tlv = readTlv(buffer, 0);
40
+ if (tlv.tag !== 0x30) {
41
+ throw new AnchorVerificationError(`expected SEQUENCE (0x30) at top, got 0x${tlv.tag.toString(16)}`);
42
+ }
43
+ return tlv;
44
+ }
45
+ function tryWrapAsSet(buffer) {
46
+ // Some CMS SignerInfo signedAttrs are encoded with IMPLICIT [0]
47
+ // (tag 0xA0) but the signature input is computed over the SET-tagged
48
+ // form (0x31). Rewrite the first byte to 0x31 to match.
49
+ if (buffer.length === 0)
50
+ return buffer;
51
+ const out = new Uint8Array(buffer);
52
+ if (out[0] === 0xa0 || out[0] === 0xa1) {
53
+ out[0] = 0x31;
54
+ }
55
+ return out;
56
+ }
57
+ /** Parse a DER-encoded TimeStampResp into the substantive fields we verify. */
58
+ export function parseTimestampResponse(responseDer) {
59
+ let response;
60
+ try {
61
+ response = topSequence(responseDer);
62
+ }
63
+ catch (exc) {
64
+ if (exc instanceof DerParseError) {
65
+ throw new AnchorVerificationError(`timestamp response is not valid DER: ${exc.message}`);
66
+ }
67
+ throw exc;
68
+ }
69
+ const responseFields = readSequence(response);
70
+ if (responseFields.length < 1) {
71
+ throw new AnchorVerificationError('timestamp response: missing PKIStatusInfo');
72
+ }
73
+ // PKIStatusInfo ::= SEQUENCE { status INTEGER, statusString OPTIONAL, failInfo OPTIONAL }
74
+ const statusInfo = responseFields[0];
75
+ if (statusInfo.tag !== 0x30) {
76
+ throw new AnchorVerificationError('PKIStatusInfo is not a SEQUENCE');
77
+ }
78
+ const statusItems = readSequence(statusInfo);
79
+ if (statusItems.length === 0) {
80
+ throw new AnchorVerificationError('PKIStatusInfo: missing status');
81
+ }
82
+ const status = Number(readInteger(statusItems[0]));
83
+ // 0 = granted, 1 = grantedWithMods, others = rejection.
84
+ if (status !== 0 && status !== 1) {
85
+ throw new AnchorVerificationError(`TSA refused request: PKIStatus=${status}`);
86
+ }
87
+ if (responseFields.length < 2) {
88
+ throw new AnchorVerificationError('TimeStampResp: missing TimeStampToken');
89
+ }
90
+ // TimeStampToken is a ContentInfo (CMS SignedData).
91
+ const contentInfo = responseFields[1];
92
+ if (contentInfo.tag !== 0x30) {
93
+ throw new AnchorVerificationError('TimeStampToken is not a ContentInfo SEQUENCE');
94
+ }
95
+ const contentInfoFields = readSequence(contentInfo);
96
+ if (contentInfoFields.length < 2) {
97
+ throw new AnchorVerificationError('ContentInfo: missing fields');
98
+ }
99
+ const contentTypeOid = readOid(contentInfoFields[0]);
100
+ if (contentTypeOid !== OID_SIGNED_DATA) {
101
+ throw new AnchorVerificationError(`TimeStampToken content_type is not signed_data: ${contentTypeOid}`);
102
+ }
103
+ const contentExplicit = contentInfoFields[1];
104
+ if (contentExplicit.tag !== 0xa0) {
105
+ throw new AnchorVerificationError('ContentInfo content not in EXPLICIT [0]');
106
+ }
107
+ const signedDataItems = readSequence(readSequence(contentExplicit)[0]);
108
+ // SignedData ::= SEQUENCE { version, digestAlgorithms SET, encapContentInfo,
109
+ // certificates [0] IMPLICIT OPTIONAL, crls [1] IMPLICIT OPTIONAL,
110
+ // signerInfos SET }
111
+ // version is first INTEGER; digestAlgorithms is SET; encapContentInfo is SEQUENCE.
112
+ if (signedDataItems.length < 4) {
113
+ throw new AnchorVerificationError('SignedData: too few fields');
114
+ }
115
+ // signedDataItems[0] = version, [1] = digestAlgorithms SET, [2] = encapContentInfo SEQUENCE.
116
+ const encapContentInfo = signedDataItems[2];
117
+ if (encapContentInfo.tag !== 0x30) {
118
+ throw new AnchorVerificationError('EncapsulatedContentInfo not a SEQUENCE');
119
+ }
120
+ const encapFields = readSequence(encapContentInfo);
121
+ if (encapFields.length < 2) {
122
+ throw new AnchorVerificationError('EncapsulatedContentInfo missing eContent');
123
+ }
124
+ const eContentTypeOid = readOid(encapFields[0]);
125
+ if (eContentTypeOid !== OID_TST_INFO) {
126
+ throw new AnchorVerificationError(`encap content_type is not tst_info: ${eContentTypeOid}`);
127
+ }
128
+ const eContentExplicit = encapFields[1];
129
+ if (eContentExplicit.tag !== 0xa0) {
130
+ throw new AnchorVerificationError('eContent not in EXPLICIT [0]');
131
+ }
132
+ // Inside EXPLICIT [0] is OCTET STRING containing TSTInfo DER bytes.
133
+ const eContentInner = readTlv(eContentExplicit.buffer, eContentExplicit.valueStart);
134
+ const tstInfoDer = readOctetString(eContentInner);
135
+ const tstInfo = topSequence(tstInfoDer);
136
+ const tstFields = readSequence(tstInfo);
137
+ // TSTInfo ::= SEQUENCE { version, policy OID, messageImprint, serialNumber, genTime,
138
+ // accuracy OPTIONAL, ordering BOOLEAN OPTIONAL, nonce OPTIONAL,
139
+ // tsa [0] OPTIONAL, extensions [1] OPTIONAL }
140
+ if (tstFields.length < 5) {
141
+ throw new AnchorVerificationError('TSTInfo: too few fields');
142
+ }
143
+ const policyOid = readOid(tstFields[1]);
144
+ const messageImprintSeq = tstFields[2];
145
+ if (messageImprintSeq.tag !== 0x30) {
146
+ throw new AnchorVerificationError('MessageImprint is not a SEQUENCE');
147
+ }
148
+ const messageImprintFields = readSequence(messageImprintSeq);
149
+ // MessageImprint ::= SEQUENCE { hashAlgorithm AlgorithmIdentifier, hashedMessage OCTET STRING }
150
+ const hashAlgOid = readOid(readSequence(messageImprintFields[0])[0]);
151
+ const hashAlgorithm = digestAlgorithmName(hashAlgOid) ?? hashAlgOid;
152
+ const hashedMessage = readOctetString(messageImprintFields[1]);
153
+ const serialNumber = readInteger(tstFields[3]);
154
+ const genTime = readGeneralizedTime(tstFields[4]);
155
+ // Optional nonce (INTEGER, tag 0x02).
156
+ let nonce = null;
157
+ for (let i = 5; i < tstFields.length; i++) {
158
+ const f = tstFields[i];
159
+ if (f.tag === 0x02) {
160
+ nonce = readInteger(f);
161
+ break;
162
+ }
163
+ }
164
+ // Extract the leaf cert from the optional certificates [0] IMPLICIT field.
165
+ // It's tag 0xA0 (context [0] constructed) right after encapContentInfo.
166
+ let certsField = null;
167
+ for (let i = 3; i < signedDataItems.length; i++) {
168
+ const f = signedDataItems[i];
169
+ if (f.tag === 0xa0) {
170
+ certsField = f;
171
+ break;
172
+ }
173
+ }
174
+ if (certsField === null) {
175
+ throw new AnchorVerificationError('SignedData has no certificates');
176
+ }
177
+ const certTlvs = readSequence(certsField);
178
+ if (certTlvs.length === 0) {
179
+ throw new AnchorVerificationError('certificates set is empty');
180
+ }
181
+ const certsDer = certTlvs.map((cert) => tlvDer(cert));
182
+ // Find signerInfos SET (last SET in signedDataItems).
183
+ let signerInfosField = null;
184
+ for (let i = signedDataItems.length - 1; i >= 0; i--) {
185
+ const f = signedDataItems[i];
186
+ if (f.tag === 0x31) {
187
+ signerInfosField = f;
188
+ break;
189
+ }
190
+ }
191
+ if (signerInfosField === null) {
192
+ throw new AnchorVerificationError('SignerInfos SET not found');
193
+ }
194
+ const signerInfos = readSequence(signerInfosField);
195
+ if (signerInfos.length !== 1) {
196
+ throw new AnchorVerificationError(`expected exactly one SignerInfo, got ${signerInfos.length}`);
197
+ }
198
+ const signerInfo = signerInfos[0];
199
+ // SignerInfo ::= SEQUENCE { version, sid, digestAlgorithm, signedAttrs [0] IMPLICIT OPTIONAL,
200
+ // signatureAlgorithm, signature, unsignedAttrs [1] OPTIONAL }
201
+ const siFields = readSequence(signerInfo);
202
+ // Find signedAttrs (tag 0xA0).
203
+ let signedAttrs = null;
204
+ let digestAlgo = null;
205
+ let signatureAlgo = null;
206
+ let signatureField = null;
207
+ let signerSid = null;
208
+ let sawSid = false;
209
+ for (let i = 0; i < siFields.length; i++) {
210
+ const f = siFields[i];
211
+ if (i === 0)
212
+ continue; // version
213
+ if (!sawSid) {
214
+ signerSid = f;
215
+ sawSid = true;
216
+ continue; // sid
217
+ }
218
+ if (digestAlgo === null && f.tag === 0x30) {
219
+ digestAlgo = f;
220
+ continue;
221
+ }
222
+ if (f.tag === 0xa0 && signedAttrs === null) {
223
+ signedAttrs = f;
224
+ continue;
225
+ }
226
+ if (signatureAlgo === null && f.tag === 0x30) {
227
+ signatureAlgo = f;
228
+ continue;
229
+ }
230
+ if (signatureField === null && f.tag === 0x04) {
231
+ signatureField = f;
232
+ break;
233
+ }
234
+ }
235
+ if (signedAttrs === null ||
236
+ signatureAlgo === null ||
237
+ signatureField === null ||
238
+ digestAlgo === null) {
239
+ throw new AnchorVerificationError('SignerInfo missing required fields');
240
+ }
241
+ const signedAttrsDer = tryWrapAsSet(tlvDer(signedAttrs));
242
+ const signature = readOctetString(signatureField);
243
+ const digestAlgorithmOid = readOid(readSequence(digestAlgo)[0]);
244
+ const signatureAlgorithmOid = readOid(readSequence(signatureAlgo)[0]);
245
+ const digestAlgorithm = digestAlgorithmName(digestAlgorithmOid);
246
+ if (digestAlgorithm === null) {
247
+ throw new AnchorVerificationError(`unsupported SignerInfo digest algorithm: ${digestAlgorithmOid}`);
248
+ }
249
+ validateSignedAttrs(signedAttrsDer, tstInfoDer, digestAlgorithm);
250
+ if (signerSid === null) {
251
+ throw new AnchorVerificationError('SignerInfo missing sid');
252
+ }
253
+ const { issuerDer, serialNumber: signerSerialNumber } = parseIssuerAndSerialSignerId(signerSid);
254
+ const leafCertDer = selectSignerCertificate(certsDer, issuerDer, signerSerialNumber);
255
+ return {
256
+ policyOid,
257
+ hashAlgorithm,
258
+ messageImprint: hashedMessage,
259
+ genTime,
260
+ serialNumber,
261
+ nonce,
262
+ leafCertDer,
263
+ signedAttrsDer,
264
+ signature,
265
+ digestAlgorithmOid,
266
+ signatureAlgorithmOid,
267
+ };
268
+ }
269
+ function digestAlgorithmName(oid) {
270
+ if (oid === OID_SHA256)
271
+ return 'sha256';
272
+ if (oid === OID_SHA384)
273
+ return 'sha384';
274
+ if (oid === OID_SHA512)
275
+ return 'sha512';
276
+ return null;
277
+ }
278
+ function hashBytes(data, algorithm) {
279
+ return new Uint8Array(createHash(algorithm).update(Buffer.from(data)).digest());
280
+ }
281
+ function parseIssuerAndSerialSignerId(sid) {
282
+ if (sid.tag !== 0x30) {
283
+ throw new AnchorVerificationError('SignerInfo.sid subjectKeyIdentifier is not supported');
284
+ }
285
+ const sidFields = readSequence(sid);
286
+ if (sidFields.length < 2) {
287
+ throw new AnchorVerificationError('SignerInfo.sid issuerAndSerialNumber is incomplete');
288
+ }
289
+ return {
290
+ issuerDer: tlvDer(sidFields[0]),
291
+ serialNumber: readInteger(sidFields[1]),
292
+ };
293
+ }
294
+ function validateSignedAttrs(signedAttrsDer, tstInfoDer, digestAlgorithm) {
295
+ const attrsSet = readTlv(signedAttrsDer, 0);
296
+ if (attrsSet.tag !== 0x31) {
297
+ throw new AnchorVerificationError(`SignerInfo signed_attrs signature input is not a SET: 0x${attrsSet.tag.toString(16)}`);
298
+ }
299
+ const attrs = readSequence(attrsSet);
300
+ let contentTypeOk = false;
301
+ let messageDigestOk = false;
302
+ const expectedDigest = hashBytes(tstInfoDer, digestAlgorithm);
303
+ for (const attr of attrs) {
304
+ if (attr.tag !== 0x30)
305
+ continue;
306
+ const attrFields = readSequence(attr);
307
+ if (attrFields.length < 2)
308
+ continue;
309
+ const attrOid = readOid(attrFields[0]);
310
+ const valuesSet = attrFields[1];
311
+ const values = readSequence(valuesSet);
312
+ if (values.length === 0)
313
+ continue;
314
+ if (attrOid === OID_CONTENT_TYPE_ATTR) {
315
+ contentTypeOk = readOid(values[0]) === OID_TST_INFO;
316
+ continue;
317
+ }
318
+ if (attrOid === OID_MESSAGE_DIGEST_ATTR) {
319
+ messageDigestOk = bytesEqual(readOctetString(values[0]), expectedDigest);
320
+ }
321
+ }
322
+ if (!contentTypeOk) {
323
+ throw new AnchorVerificationError('SignerInfo signed_attrs content_type is not tst_info');
324
+ }
325
+ if (!messageDigestOk) {
326
+ throw new AnchorVerificationError('SignerInfo signed_attrs message_digest mismatch');
327
+ }
328
+ }
329
+ function selectSignerCertificate(certsDer, issuerDer, serialNumber) {
330
+ for (const der of certsDer) {
331
+ const cert = parseCertificate(der);
332
+ if (cert.serialNumber === serialNumber && bytesEqual(cert.issuerDer, issuerDer)) {
333
+ return der;
334
+ }
335
+ }
336
+ throw new AnchorVerificationError('SignerInfo signer certificate not found by issuer+serial');
337
+ }
338
+ function readUtcOrGeneralizedTime(tlv) {
339
+ // UTCTime (0x17) format: YYMMDDhhmmssZ
340
+ // GeneralizedTime (0x18) format: YYYYMMDDhhmmss[.fff]Z
341
+ if (tlv.tag === 0x18) {
342
+ return readGeneralizedTime(tlv);
343
+ }
344
+ if (tlv.tag !== 0x17) {
345
+ throw new AnchorVerificationError(`expected UTCTime/GeneralizedTime, got 0x${tlv.tag.toString(16)}`);
346
+ }
347
+ const text = new TextDecoder('ascii').decode(getValueBytes(tlv));
348
+ if (!text.endsWith('Z')) {
349
+ throw new AnchorVerificationError(`UTCTime not in UTC Z form: ${text}`);
350
+ }
351
+ const stripped = text.slice(0, -1);
352
+ const m = /^(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/.exec(stripped);
353
+ if (!m) {
354
+ throw new AnchorVerificationError(`UTCTime not parseable: ${text}`);
355
+ }
356
+ const yy = Number(m[1]);
357
+ const year = yy < 50 ? 2000 + yy : 1900 + yy;
358
+ return new Date(`${year}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:${m[6]}Z`);
359
+ }
360
+ function parseCertificate(der) {
361
+ const cert = topSequence(der);
362
+ const certFields = readSequence(cert);
363
+ if (certFields.length < 3) {
364
+ throw new AnchorVerificationError('Certificate: too few fields');
365
+ }
366
+ const tbs = certFields[0];
367
+ const sigAlg = certFields[1];
368
+ const sigField = certFields[2];
369
+ const tbsFields = readSequence(tbs);
370
+ // TBSCertificate ::= SEQUENCE { [0] EXPLICIT Version OPTIONAL, serialNumber INTEGER,
371
+ // signature AlgorithmIdentifier, issuer Name, validity Validity,
372
+ // subject Name, subjectPublicKeyInfo SubjectPublicKeyInfo,
373
+ // ... extensions [3] EXPLICIT OPTIONAL }
374
+ let cursor = 0;
375
+ // Optional version [0] EXPLICIT.
376
+ if (tbsFields[cursor].tag === 0xa0) {
377
+ cursor += 1;
378
+ }
379
+ const serialNumber = readInteger(tbsFields[cursor]);
380
+ cursor += 1; // serialNumber
381
+ cursor += 1; // signature AlgorithmIdentifier
382
+ const issuerDer = tlvDer(tbsFields[cursor]);
383
+ cursor += 1;
384
+ const validity = tbsFields[cursor];
385
+ cursor += 1;
386
+ const subjectDer = tlvDer(tbsFields[cursor]);
387
+ cursor += 1;
388
+ const spki = tbsFields[cursor];
389
+ cursor += 1;
390
+ const validityFields = readSequence(validity);
391
+ const notBefore = readUtcOrGeneralizedTime(validityFields[0]);
392
+ const notAfter = readUtcOrGeneralizedTime(validityFields[1]);
393
+ // Convert SPKI to a Node KeyObject.
394
+ let publicKey;
395
+ try {
396
+ publicKey = createPublicKey({ key: Buffer.from(tlvDer(spki)), format: 'der', type: 'spki' });
397
+ }
398
+ catch (exc) {
399
+ throw new AnchorVerificationError(`failed to parse SubjectPublicKeyInfo: ${exc instanceof Error ? exc.message : String(exc)}`);
400
+ }
401
+ // BasicConstraints CA flag: walk extensions if present.
402
+ let isCa = false;
403
+ let tsaEkuCritical = false;
404
+ let hasTsaEku = false;
405
+ while (cursor < tbsFields.length) {
406
+ const f = tbsFields[cursor];
407
+ cursor += 1;
408
+ if (f.tag !== 0xa3)
409
+ continue; // extensions are [3] EXPLICIT
410
+ const extsSeq = readSequence(f)[0];
411
+ const exts = readSequence(extsSeq);
412
+ for (const ext of exts) {
413
+ // Each ext: SEQUENCE { extnID OID, critical BOOLEAN OPT, extnValue OCTET STRING }
414
+ const extFields = readSequence(ext);
415
+ const oid = readOid(extFields[0]);
416
+ const critical = extFields.length >= 3 &&
417
+ extFields[1].tag === 0x01 &&
418
+ extFields[1].buffer[extFields[1].valueStart] !== 0;
419
+ const valueBytes = readOctetString(extFields[extFields.length - 1]);
420
+ if (oid === '2.5.29.19') {
421
+ // basicConstraints
422
+ const bcSeq = readTlv(valueBytes, 0);
423
+ const bcFields = readSequence(bcSeq);
424
+ if (bcFields.length > 0) {
425
+ const cTlv = bcFields[0];
426
+ if (cTlv.tag === 0x01) {
427
+ // BOOLEAN: cA. Non-zero value = true.
428
+ isCa = cTlv.buffer[cTlv.valueStart] !== 0;
429
+ }
430
+ }
431
+ continue;
432
+ }
433
+ if (oid !== '2.5.29.37')
434
+ continue; // extendedKeyUsage
435
+ const bcSeq = readTlv(valueBytes, 0);
436
+ for (const eku of readSequence(bcSeq)) {
437
+ if (readOid(eku) === '1.3.6.1.5.5.7.3.8') {
438
+ hasTsaEku = true;
439
+ tsaEkuCritical = critical;
440
+ }
441
+ }
442
+ }
443
+ }
444
+ // signature BIT STRING.
445
+ const { unusedBits: _unused, bytes: sigBytes } = readBitString(sigField);
446
+ const signatureAlgorithmOid = readOid(readSequence(sigAlg)[0]);
447
+ return {
448
+ der,
449
+ serialNumber,
450
+ subjectDer,
451
+ issuerDer,
452
+ notBefore,
453
+ notAfter,
454
+ publicKey,
455
+ tbsBytes: tlvDer(tbs),
456
+ signatureAlgorithmOid,
457
+ signature: sigBytes,
458
+ isCa,
459
+ tsaEkuCritical,
460
+ hasTsaEku,
461
+ };
462
+ }
463
+ function bytesEqual(a, b) {
464
+ if (a.length !== b.length)
465
+ return false;
466
+ for (let i = 0; i < a.length; i++) {
467
+ if (a[i] !== b[i])
468
+ return false;
469
+ }
470
+ return true;
471
+ }
472
+ function verifyAllowedSignature(publicKey, data, signature, signatureAlgorithmOid, digestAlgorithm) {
473
+ const nodeAlgorithm = verificationAlgorithm(publicKey, signatureAlgorithmOid, digestAlgorithm);
474
+ const v = createVerify(nodeAlgorithm);
475
+ v.update(Buffer.from(data));
476
+ v.end();
477
+ return v.verify(publicKey, Buffer.from(signature));
478
+ }
479
+ function verificationAlgorithm(publicKey, signatureAlgorithmOid, digestAlgorithm) {
480
+ const digestUpper = digestAlgorithm.toUpperCase();
481
+ if (publicKey.asymmetricKeyType === 'rsa' &&
482
+ (signatureAlgorithmOid === OID_RSA_PKCS1 ||
483
+ signatureAlgorithmOid === OID_SHA256_RSA ||
484
+ signatureAlgorithmOid === OID_SHA384_RSA ||
485
+ signatureAlgorithmOid === OID_SHA512_RSA)) {
486
+ const expectedDigest = signatureAlgorithmOid === OID_SHA256_RSA
487
+ ? 'sha256'
488
+ : signatureAlgorithmOid === OID_SHA384_RSA
489
+ ? 'sha384'
490
+ : signatureAlgorithmOid === OID_SHA512_RSA
491
+ ? 'sha512'
492
+ : digestAlgorithm;
493
+ if (expectedDigest !== digestAlgorithm) {
494
+ throw new AnchorVerificationError(`signature algorithm digest ${expectedDigest} does not match digest algorithm ${digestAlgorithm}`);
495
+ }
496
+ return `RSA-${digestUpper}`;
497
+ }
498
+ if (publicKey.asymmetricKeyType === 'ec' &&
499
+ ((signatureAlgorithmOid === OID_SHA256_ECDSA && digestAlgorithm === 'sha256') ||
500
+ (signatureAlgorithmOid === OID_SHA384_ECDSA && digestAlgorithm === 'sha384') ||
501
+ (signatureAlgorithmOid === OID_SHA512_ECDSA && digestAlgorithm === 'sha512'))) {
502
+ return digestUpper;
503
+ }
504
+ throw new AnchorVerificationError(`unsupported signature algorithm/key combination: oid=${signatureAlgorithmOid}, digest=${digestAlgorithm}, key=${publicKey.asymmetricKeyType ?? 'unknown'}`);
505
+ }
506
+ /**
507
+ * Verify a parsed TimeStampToken against the expected message digest +
508
+ * trust roots + intermediates.
509
+ *
510
+ * Mirrors the Python `verify_timestamp_token` API including multi-hop
511
+ * intermediate chain walking, time-validity checks at every cert, and
512
+ * BasicConstraints.cA enforcement on each intermediate.
513
+ *
514
+ * Throws `AnchorVerificationError` on any failure.
515
+ */
516
+ export function verifyTimestampToken(parsed, options) {
517
+ const { expectedDigest, trustRootsDer, intermediatesDer = [], verificationTime, maxChainDepth = 8, } = options;
518
+ if (expectedDigest.length !== 32) {
519
+ throw new AnchorVerificationError(`expectedDigest must be 32 bytes, got ${expectedDigest.length}`);
520
+ }
521
+ if (parsed.hashAlgorithm !== 'sha256') {
522
+ throw new AnchorVerificationError(`unexpected message-imprint hash algorithm: ${parsed.hashAlgorithm}`);
523
+ }
524
+ if (!bytesEqual(parsed.messageImprint, expectedDigest)) {
525
+ throw new AnchorVerificationError('message_imprint does not match expected digest');
526
+ }
527
+ let leaf;
528
+ try {
529
+ leaf = parseCertificate(parsed.leafCertDer);
530
+ }
531
+ catch (exc) {
532
+ throw new AnchorVerificationError(`leaf cert is not valid DER: ${exc instanceof Error ? exc.message : String(exc)}`);
533
+ }
534
+ if (!leaf.hasTsaEku || !leaf.tsaEkuCritical) {
535
+ throw new AnchorVerificationError('leaf cert EKU does not include critical timeStamping');
536
+ }
537
+ // Verify the TSA signature over signedAttrs using the leaf cert's
538
+ // public key.
539
+ const signerDigest = digestAlgorithmName(parsed.digestAlgorithmOid);
540
+ if (signerDigest === null) {
541
+ throw new AnchorVerificationError(`unsupported SignerInfo digest algorithm: ${parsed.digestAlgorithmOid}`);
542
+ }
543
+ if (!verifyAllowedSignature(leaf.publicKey, parsed.signedAttrsDer, parsed.signature, parsed.signatureAlgorithmOid, signerDigest)) {
544
+ throw new AnchorVerificationError('TSA signature does not verify against leaf cert');
545
+ }
546
+ // Time validity for the leaf.
547
+ const when = verificationTime ?? new Date();
548
+ if (when < leaf.notBefore) {
549
+ throw new AnchorVerificationError(`verification_time ${when.toISOString()} precedes leaf cert not_before ${leaf.notBefore.toISOString()}`);
550
+ }
551
+ if (when > leaf.notAfter) {
552
+ throw new AnchorVerificationError(`verification_time ${when.toISOString()} exceeds leaf cert not_after ${leaf.notAfter.toISOString()}`);
553
+ }
554
+ // Chain walk.
555
+ const intermediates = [];
556
+ for (const der of intermediatesDer) {
557
+ try {
558
+ intermediates.push(parseCertificate(der));
559
+ }
560
+ catch {
561
+ // Skip malformed intermediates.
562
+ }
563
+ }
564
+ const roots = [];
565
+ for (const der of trustRootsDer) {
566
+ try {
567
+ roots.push(parseCertificate(der));
568
+ }
569
+ catch {
570
+ // Skip malformed roots.
571
+ }
572
+ }
573
+ if (roots.length === 0) {
574
+ throw new AnchorVerificationError('no parseable trust roots provided');
575
+ }
576
+ let current = leaf;
577
+ const visited = new Set();
578
+ visited.add(`${bytesToHexString(current.subjectDer)}:${bytesToHexString(current.tbsBytes.slice(0, 4))}`);
579
+ for (let hop = 0; hop < maxChainDepth; hop++) {
580
+ const matchedRoot = findIssuer(current, roots);
581
+ if (matchedRoot !== null) {
582
+ verifyLink(current, matchedRoot, when);
583
+ return;
584
+ }
585
+ const matchedIntermediate = findIssuer(current, intermediates);
586
+ if (matchedIntermediate === null) {
587
+ throw new AnchorVerificationError(`at hop ${hop}: cert issuer not in trust roots or intermediates`);
588
+ }
589
+ if (!matchedIntermediate.isCa) {
590
+ throw new AnchorVerificationError(`at hop ${hop}: candidate issuer is not a CA (missing BasicConstraints.cA=True)`);
591
+ }
592
+ verifyLink(current, matchedIntermediate, when);
593
+ const key = bytesToHexString(matchedIntermediate.subjectDer);
594
+ if (visited.has(key)) {
595
+ throw new AnchorVerificationError(`chain cycle detected at hop ${hop}`);
596
+ }
597
+ visited.add(key);
598
+ current = matchedIntermediate;
599
+ }
600
+ throw new AnchorVerificationError(`chain depth exceeded maxChainDepth=${maxChainDepth} without reaching a configured trust root`);
601
+ }
602
+ function findIssuer(child, pool) {
603
+ for (const cand of pool) {
604
+ if (bytesEqual(cand.subjectDer, child.issuerDer))
605
+ return cand;
606
+ }
607
+ return null;
608
+ }
609
+ function verifyLink(child, issuer, when) {
610
+ if (when < issuer.notBefore) {
611
+ throw new AnchorVerificationError(`verification_time precedes issuer cert not_before ${issuer.notBefore.toISOString()}`);
612
+ }
613
+ if (when > issuer.notAfter) {
614
+ throw new AnchorVerificationError(`verification_time exceeds issuer cert not_after ${issuer.notAfter.toISOString()}`);
615
+ }
616
+ const certDigest = certSignatureDigest(child.signatureAlgorithmOid);
617
+ if (!verifyAllowedSignature(issuer.publicKey, child.tbsBytes, child.signature, child.signatureAlgorithmOid, certDigest)) {
618
+ throw new AnchorVerificationError('cert signature does not verify against issuer');
619
+ }
620
+ }
621
+ function certSignatureDigest(signatureAlgorithmOid) {
622
+ if (signatureAlgorithmOid === OID_SHA256_RSA || signatureAlgorithmOid === OID_SHA256_ECDSA) {
623
+ return 'sha256';
624
+ }
625
+ if (signatureAlgorithmOid === OID_SHA384_RSA || signatureAlgorithmOid === OID_SHA384_ECDSA) {
626
+ return 'sha384';
627
+ }
628
+ if (signatureAlgorithmOid === OID_SHA512_RSA || signatureAlgorithmOid === OID_SHA512_ECDSA) {
629
+ return 'sha512';
630
+ }
631
+ throw new AnchorVerificationError(`unsupported cert signature algorithm: ${signatureAlgorithmOid}`);
632
+ }
633
+ function bytesToHexString(b) {
634
+ let out = '';
635
+ for (let i = 0; i < b.length; i++) {
636
+ out += b[i].toString(16).padStart(2, '0');
637
+ }
638
+ return out;
639
+ }
640
+ //# sourceMappingURL=rfc3161.js.map