@int-loop/core 0.1.0 → 0.2.0

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.
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { type AnchorReport } from "./anchor.ts";
2
3
  import { LedgerEntry } from "./schema/ledger.ts";
3
4
  import { type VerificationFailure } from "./verify.ts";
4
5
  export declare const EVIDENCE_PACK_FORMAT = "int-loop-evidence-pack";
@@ -33,7 +34,7 @@ export declare const PackKeys: z.ZodObject<{
33
34
  export type PackKeys = z.infer<typeof PackKeys>;
34
35
  /** The hashes of free text a ledger entry references, which the pack should carry. */
35
36
  export declare function referencedTextHashes(entry: LedgerEntry): string[];
36
- export declare const PACK_README = "# int-loop evidence pack\n\nThis archive is a complete, signed record of decisions made through int-loop for one\ntenant. You can check it without trusting int-loop.\n\nRun:\n\n npx @int-loop/verify pack.zip\n\nor, to confirm the service keys are really int-loop's, download\nhttps://int-loop.com/.well-known/int-loop-keys.json and run:\n\n npx @int-loop/verify pack.zip --keys int-loop-keys.json\n\nThe verifier checks that:\n\n1. Every ledger entry links to the one before it (a SHA-256 hash chain from a zero genesis hash).\n2. Every entry carries a valid Ed25519 signature from an int-loop service key.\n3. Every passkey decision (L2 and above) carries a WebAuthn assertion from a passkey enrolled\n earlier in this ledger, by the person the decision names, over a challenge that commits to\n exactly what was displayed, the option chosen, and any reason, conditions or question typed.\n4. Every text in texts.jsonl matches the hash the ledger records for it.\n\nTexts marked \"erased\" were removed under the tenant's retention policy. Their hashes stay in\nthe ledger, so the chain still verifies.\n\nThe verifier is open source (Apache-2.0). This pack is evidence for your own assessment. It is\nnot legal advice and does not on its own show that any regulation is met.\n";
37
+ export declare const PACK_README = "# int-loop evidence pack\n\nThis archive is a complete, signed record of decisions made through int-loop for one\ntenant. You can check it without trusting int-loop.\n\nRun:\n\n npx @int-loop/verify pack.zip\n\nor, to confirm the service keys are really int-loop's, download\nhttps://int-loop.com/.well-known/int-loop-keys.json and run:\n\n npx @int-loop/verify pack.zip --keys int-loop-keys.json\n\nThe verifier checks that:\n\n1. Every ledger entry links to the one before it (a SHA-256 hash chain from a zero genesis hash).\n2. Every entry carries a valid Ed25519 signature from an int-loop service key.\n3. Every passkey decision (L2 and above) carries a WebAuthn assertion from a passkey enrolled\n earlier in this ledger, by the person the decision names, over a challenge that commits to\n exactly what was displayed, the option chosen, and any reason, conditions or question typed.\n4. Every text in texts.jsonl matches the hash the ledger records for it.\n5. Every anchor (an anchor.created entry) holds RFC 3161 timestamps from independent\n timestamp authorities, over a Merkle root that includes the anchored entry. Each\n timestamp must verify against a trusted timestamp authority root. This shows the\n ledger up to that entry existed, unchanged, no later than the stamped time.\n The verifier trusts a short built-in list of roots; pass --tsa-roots roots.pem to\n use your own.\n\nTexts marked \"erased\" were removed under the tenant's retention policy. Their hashes stay in\nthe ledger, so the chain still verifies.\n\nThe verifier is open source (Apache-2.0). This pack is evidence for your own assessment. It is\nnot legal advice and does not on its own show that any regulation is met.\n";
37
38
  export declare function crc32(data: Uint8Array): number;
38
39
  export declare function buildZip(files: {
39
40
  name: string;
@@ -49,11 +50,19 @@ export type PackReport = {
49
50
  texts_checked: number;
50
51
  texts_erased: number;
51
52
  keys_source: "pack" | "trusted";
53
+ anchors_checked: number;
54
+ /** The newest anchored entry, and the earliest time an independent TSA vouches for it. */
55
+ anchored_through: AnchorReport["anchored_through"];
56
+ tsa_roots_source: "default" | "supplied";
52
57
  failures: VerificationFailure[];
53
58
  };
59
+ export type PackVerifyOptions = {
60
+ /** Trusted timestamp authority roots, as DER certificates. Default: KNOWN_TSA_ROOTS. */
61
+ tsaRoots?: readonly Uint8Array[];
62
+ };
54
63
  /**
55
64
  * Verifies an evidence pack. With `trustedKeys` (from int-loop's published key list),
56
65
  * the pack's own keys.json must agree with them; without, the report says the keys came
57
66
  * only from the pack.
58
67
  */
59
- export declare function verifyEvidencePack(zip: Uint8Array, trustedKeys?: PackKeys): Promise<PackReport>;
68
+ export declare function verifyEvidencePack(zip: Uint8Array, trustedKeys?: PackKeys, options?: PackVerifyOptions): Promise<PackReport>;
@@ -10,9 +10,11 @@
10
10
  // The zip is stored, not compressed, with fixed timestamps: the same records always
11
11
  // produce the same bytes, and the reader needs no decompressor.
12
12
  import { z } from "zod";
13
+ import { verifyAnchors } from "./anchor.js";
13
14
  import { bytesEqual, fromBase64Url, sha256Hex, utf8 } from "./hash.js";
14
15
  import { Base64Url, Sha256Hex, TenantId, Timestamp } from "./schema/common.js";
15
16
  import { LedgerEntry, ServiceKeyId } from "./schema/ledger.js";
17
+ import { knownTsaRootsDer } from "./tsa-roots.js";
16
18
  import { verifyLedger } from "./verify.js";
17
19
  export const EVIDENCE_PACK_FORMAT = "int-loop-evidence-pack";
18
20
  export const PackManifest = z.strictObject({
@@ -75,6 +77,12 @@ The verifier checks that:
75
77
  earlier in this ledger, by the person the decision names, over a challenge that commits to
76
78
  exactly what was displayed, the option chosen, and any reason, conditions or question typed.
77
79
  4. Every text in texts.jsonl matches the hash the ledger records for it.
80
+ 5. Every anchor (an anchor.created entry) holds RFC 3161 timestamps from independent
81
+ timestamp authorities, over a Merkle root that includes the anchored entry. Each
82
+ timestamp must verify against a trusted timestamp authority root. This shows the
83
+ ledger up to that entry existed, unchanged, no later than the stamped time.
84
+ The verifier trusts a short built-in list of roots; pass --tsa-roots roots.pem to
85
+ use your own.
78
86
 
79
87
  Texts marked "erased" were removed under the tenant's retention policy. Their hashes stay in
80
88
  the ledger, so the chain still verifies.
@@ -201,7 +209,7 @@ export function readZip(zip) {
201
209
  * the pack's own keys.json must agree with them; without, the report says the keys came
202
210
  * only from the pack.
203
211
  */
204
- export async function verifyEvidencePack(zip, trustedKeys) {
212
+ export async function verifyEvidencePack(zip, trustedKeys, options = {}) {
205
213
  const failures = [];
206
214
  const report = (extra) => ({
207
215
  ok: false,
@@ -211,6 +219,9 @@ export async function verifyEvidencePack(zip, trustedKeys) {
211
219
  texts_checked: 0,
212
220
  texts_erased: 0,
213
221
  keys_source: trustedKeys ? "trusted" : "pack",
222
+ anchors_checked: 0,
223
+ anchored_through: null,
224
+ tsa_roots_source: options.tsaRoots ? "supplied" : "default",
214
225
  failures,
215
226
  ...extra,
216
227
  });
@@ -329,5 +340,16 @@ export async function verifyEvidencePack(zip, trustedKeys) {
329
340
  if (!present.has(h))
330
341
  fail("texts", `entry ${e.seq} references text ${h}, which is not in the pack`, e.seq);
331
342
  }
332
- return report({ ok: failures.length === 0, manifest, entries_checked: ledger.entries_checked, decisions_checked: ledger.decisions_checked, texts_checked, texts_erased });
343
+ const anchors = await verifyAnchors(parsed, options.tsaRoots ?? knownTsaRootsDer());
344
+ failures.push(...anchors.failures);
345
+ return report({
346
+ ok: failures.length === 0,
347
+ manifest,
348
+ entries_checked: ledger.entries_checked,
349
+ decisions_checked: ledger.decisions_checked,
350
+ texts_checked,
351
+ texts_erased,
352
+ anchors_checked: anchors.anchors_checked,
353
+ anchored_through: anchors.anchored_through,
354
+ });
333
355
  }
package/dist/index.d.ts CHANGED
@@ -2,11 +2,17 @@ export * from "./jcs.ts";
2
2
  export * from "./hash.ts";
3
3
  export * from "./cbor.ts";
4
4
  export * from "./binding.ts";
5
+ export * from "./comprehension.ts";
5
6
  export * from "./webauthn.ts";
6
7
  export * from "./ledger.ts";
7
8
  export * from "./verify.ts";
8
9
  export * from "./evidence-pack.ts";
9
10
  export * from "./receipt.ts";
11
+ export * from "./der.ts";
12
+ export * from "./x509.ts";
13
+ export * from "./tsp.ts";
14
+ export * from "./tsa-roots.ts";
15
+ export * from "./anchor.ts";
10
16
  export * from "./schema/common.ts";
11
17
  export * from "./schema/request.ts";
12
18
  export * from "./schema/directory.ts";
package/dist/index.js CHANGED
@@ -2,11 +2,17 @@ export * from "./jcs.js";
2
2
  export * from "./hash.js";
3
3
  export * from "./cbor.js";
4
4
  export * from "./binding.js";
5
+ export * from "./comprehension.js";
5
6
  export * from "./webauthn.js";
6
7
  export * from "./ledger.js";
7
8
  export * from "./verify.js";
8
9
  export * from "./evidence-pack.js";
9
10
  export * from "./receipt.js";
11
+ export * from "./der.js";
12
+ export * from "./x509.js";
13
+ export * from "./tsp.js";
14
+ export * from "./tsa-roots.js";
15
+ export * from "./anchor.js";
10
16
  export * from "./schema/common.js";
11
17
  export * from "./schema/request.js";
12
18
  export * from "./schema/directory.js";
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- export declare const LEDGER_ENTRY_TYPES: readonly ["request.created", "request.routed", "notification.sent", "notification.failed", "request.viewed", "decision.cast", "decision.invalidated", "request.info_requested", "request.resolved", "request.expired", "request.cancelled", "policy.loaded", "directory.changed", "passkey.enrolled", "passkey.revoked", "export.created", "pack.activated", "pack.deactivated", "anchor.created"];
2
+ export declare const LEDGER_ENTRY_TYPES: readonly ["request.created", "request.routed", "notification.sent", "notification.failed", "request.viewed", "decision.cast", "decision.invalidated", "request.info_requested", "request.resolved", "request.expired", "request.cancelled", "receipt.redeemed", "policy.loaded", "directory.changed", "passkey.enrolled", "passkey.revoked", "export.created", "pack.activated", "pack.deactivated", "anchor.created"];
3
3
  export declare const LedgerEntryType: z.ZodEnum<{
4
4
  "anchor.created": "anchor.created";
5
5
  "decision.cast": "decision.cast";
@@ -13,6 +13,7 @@ export declare const LedgerEntryType: z.ZodEnum<{
13
13
  "passkey.enrolled": "passkey.enrolled";
14
14
  "passkey.revoked": "passkey.revoked";
15
15
  "policy.loaded": "policy.loaded";
16
+ "receipt.redeemed": "receipt.redeemed";
16
17
  "request.cancelled": "request.cancelled";
17
18
  "request.created": "request.created";
18
19
  "request.expired": "request.expired";
@@ -39,6 +40,7 @@ export declare const LedgerEntry: z.ZodObject<{
39
40
  "passkey.enrolled": "passkey.enrolled";
40
41
  "passkey.revoked": "passkey.revoked";
41
42
  "policy.loaded": "policy.loaded";
43
+ "receipt.redeemed": "receipt.redeemed";
42
44
  "request.cancelled": "request.cancelled";
43
45
  "request.created": "request.created";
44
46
  "request.expired": "request.expired";
@@ -13,6 +13,8 @@ export const LEDGER_ENTRY_TYPES = [
13
13
  "request.resolved",
14
14
  "request.expired",
15
15
  "request.cancelled",
16
+ // The executor used the request's receipt. A receipt is redeemed at most once.
17
+ "receipt.redeemed",
16
18
  "policy.loaded",
17
19
  "directory.changed",
18
20
  "passkey.enrolled",
@@ -136,6 +136,7 @@ export declare const Rule: z.ZodObject<{
136
136
  L4: "L4";
137
137
  }>>;
138
138
  respond_within: z.ZodOptional<z.ZodString>;
139
+ remind_after: z.ZodOptional<z.ZodString>;
139
140
  separation_of_duties: z.ZodOptional<z.ZodBoolean>;
140
141
  }, z.core.$strict>;
141
142
  export type Rule = z.infer<typeof Rule>;
@@ -150,6 +151,7 @@ export declare const Policy: z.ZodObject<{
150
151
  L4: "L4";
151
152
  }>>;
152
153
  respond_within: z.ZodDefault<z.ZodString>;
154
+ remind_after: z.ZodOptional<z.ZodString>;
153
155
  on_timeout: z.ZodDefault<z.ZodLiteral<"expired">>;
154
156
  deny_is_veto: z.ZodDefault<z.ZodBoolean>;
155
157
  }, z.core.$strict>;
@@ -206,6 +208,7 @@ export declare const Policy: z.ZodObject<{
206
208
  L4: "L4";
207
209
  }>>;
208
210
  respond_within: z.ZodOptional<z.ZodString>;
211
+ remind_after: z.ZodOptional<z.ZodString>;
209
212
  separation_of_duties: z.ZodOptional<z.ZodBoolean>;
210
213
  }, z.core.$strict>>;
211
214
  }, z.core.$strict>;
@@ -45,6 +45,8 @@ export const Rule = z.strictObject({
45
45
  stages: z.array(Stage).min(1),
46
46
  assurance: AssuranceLevel.optional(),
47
47
  respond_within: Duration.optional(),
48
+ // Remind approvers who have not decided once this long after the request is sent. The shortest applicable wins.
49
+ remind_after: Duration.optional(),
48
50
  separation_of_duties: z.boolean().optional(),
49
51
  });
50
52
  export const Policy = z
@@ -54,6 +56,8 @@ export const Policy = z
54
56
  defaults: z.strictObject({
55
57
  assurance: AssuranceLevel.default("L2"),
56
58
  respond_within: Duration.default("24h"),
59
+ // Optional: with no remind_after anywhere, nobody is reminded.
60
+ remind_after: Duration.optional(),
57
61
  // Timeouts resolve to expired, never to approval. There is no other value.
58
62
  on_timeout: z.literal("expired").default("expired"),
59
63
  deny_is_veto: z.boolean().default(true),
@@ -247,5 +247,6 @@ export declare const DecisionStatus: z.ZodObject<{
247
247
  }, z.core.$strict>;
248
248
  sig: z.ZodString;
249
249
  }, z.core.$strict>>;
250
+ receipt_redeemed_at: z.ZodNullable<z.ZodISODateTime>;
250
251
  }, z.core.$strict>;
251
252
  export type DecisionStatus = z.infer<typeof DecisionStatus>;
@@ -97,4 +97,6 @@ export const DecisionStatus = z.strictObject({
97
97
  evidence_url: z.url(),
98
98
  // Signed proof of what was approved, for the system that carries out the action. Null unless approved or chosen.
99
99
  receipt: Receipt.nullable(),
100
+ // When the receipt was redeemed, if it has been. A redeemed receipt cannot be used again.
101
+ receipt_redeemed_at: Timestamp.nullable(),
100
102
  });
@@ -0,0 +1,9 @@
1
+ export type TsaRoot = {
2
+ id: string;
3
+ name: string;
4
+ sha256: string;
5
+ pem: string;
6
+ };
7
+ export declare const KNOWN_TSA_ROOTS: readonly TsaRoot[];
8
+ /** The default roots, as DER. */
9
+ export declare function knownTsaRootsDer(): Uint8Array<ArrayBuffer>[];
@@ -0,0 +1,150 @@
1
+ // Timestamp authority roots that int-loop and int-loop-verify trust by default. An auditor
2
+ // can replace this list with their own (int-loop-verify --tsa-roots roots.pem).
3
+ //
4
+ // Provenance, checked 2026-11-09 (SHA-256 fingerprints):
5
+ // FreeTSA Root CA a6379e7cecc05faa3cbf076013d745e327bbbaa38c0b9af22469d4701d18aabc
6
+ // from https://freetsa.org/files/cacert.pem, identical to the root FreeTSA sends
7
+ // DigiCert Trusted Root G4 552f7bdcf1a7af9e6ce672017f4f12abf77240c78e761ac203d1d9d20ac89988
8
+ // from the macOS system root store
9
+ // Sectigo Public Time Stamping Root R46
10
+ // 4941b001b8a97e961b7817c9d9e960ec4b056bfc915a8c1aabf6ef6b3ac046a5
11
+ // from the macOS system root store
12
+ //
13
+ // tests/core/tsp.test.ts checks each fingerprint, so a change here cannot go unnoticed.
14
+ import { fromPem } from "./der.js";
15
+ export const KNOWN_TSA_ROOTS = [
16
+ {
17
+ id: "freetsa",
18
+ name: "FreeTSA Root CA",
19
+ sha256: "a6379e7cecc05faa3cbf076013d745e327bbbaa38c0b9af22469d4701d18aabc",
20
+ pem: `
21
+ -----BEGIN CERTIFICATE-----
22
+ MIIH/zCCBeegAwIBAgIJAMHphhYNqOmAMA0GCSqGSIb3DQEBDQUAMIGVMREwDwYD
23
+ VQQKEwhGcmVlIFRTQTEQMA4GA1UECxMHUm9vdCBDQTEYMBYGA1UEAxMPd3d3LmZy
24
+ ZWV0c2Eub3JnMSIwIAYJKoZIhvcNAQkBFhNidXNpbGV6YXNAZ21haWwuY29tMRIw
25
+ EAYDVQQHEwlXdWVyemJ1cmcxDzANBgNVBAgTBkJheWVybjELMAkGA1UEBhMCREUw
26
+ HhcNMTYwMzEzMDE1MjEzWhcNNDEwMzA3MDE1MjEzWjCBlTERMA8GA1UEChMIRnJl
27
+ ZSBUU0ExEDAOBgNVBAsTB1Jvb3QgQ0ExGDAWBgNVBAMTD3d3dy5mcmVldHNhLm9y
28
+ ZzEiMCAGCSqGSIb3DQEJARYTYnVzaWxlemFzQGdtYWlsLmNvbTESMBAGA1UEBxMJ
29
+ V3VlcnpidXJnMQ8wDQYDVQQIEwZCYXllcm4xCzAJBgNVBAYTAkRFMIICIjANBgkq
30
+ hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtgKODjAy8REQ2WTNqUudAnjhlCrpE6ql
31
+ mQfNppeTmVvZrH4zutn+NwTaHAGpjSGv4/WRpZ1wZ3BRZ5mPUBZyLgq0YrIfQ5Fx
32
+ 0s/MRZPzc1r3lKWrMR9sAQx4mN4z11xFEO529L0dFJjPF9MD8Gpd2feWzGyptlel
33
+ b+PqT+++fOa2oY0+NaMM7l/xcNHPOaMz0/2olk0i22hbKeVhvokPCqhFhzsuhKsm
34
+ q4Of/o+t6dI7sx5h0nPMm4gGSRhfq+z6BTRgCrqQG2FOLoVFgt6iIm/BnNffUr7V
35
+ DYd3zZmIwFOj/H3DKHoGik/xK3E82YA2ZulVOFRW/zj4ApjPa5OFbpIkd0pmzxzd
36
+ EcL479hSA9dFiyVmSxPtY5ze1P+BE9bMU1PScpRzw8MHFXxyKqW13Qv7LWw4sbk3
37
+ SciB7GACbQiVGzgkvXG6y85HOuvWNvC5GLSiyP9GlPB0V68tbxz4JVTRdw/Xn/XT
38
+ FNzRBM3cq8lBOAVt/PAX5+uFcv1S9wFE8YjaBfWCP1jdBil+c4e+0tdywT2oJmYB
39
+ BF/kEt1wmGwMmHunNEuQNzh1FtJY54hbUfiWi38mASE7xMtMhfj/C4SvapiDN837
40
+ gYaPfs8x3KZxbX7C3YAsFnJinlwAUss1fdKar8Q/YVs7H/nU4c4Ixxxz4f67fcVq
41
+ M2ITKentbCMCAwEAAaOCAk4wggJKMAwGA1UdEwQFMAMBAf8wDgYDVR0PAQH/BAQD
42
+ AgHGMB0GA1UdDgQWBBT6VQ2MNGZRQ0z357OnbJWveuaklzCBygYDVR0jBIHCMIG/
43
+ gBT6VQ2MNGZRQ0z357OnbJWveuakl6GBm6SBmDCBlTERMA8GA1UEChMIRnJlZSBU
44
+ U0ExEDAOBgNVBAsTB1Jvb3QgQ0ExGDAWBgNVBAMTD3d3dy5mcmVldHNhLm9yZzEi
45
+ MCAGCSqGSIb3DQEJARYTYnVzaWxlemFzQGdtYWlsLmNvbTESMBAGA1UEBxMJV3Vl
46
+ cnpidXJnMQ8wDQYDVQQIEwZCYXllcm4xCzAJBgNVBAYTAkRFggkAwemGFg2o6YAw
47
+ MwYDVR0fBCwwKjAooCagJIYiaHR0cDovL3d3dy5mcmVldHNhLm9yZy9yb290X2Nh
48
+ LmNybDCBzwYDVR0gBIHHMIHEMIHBBgorBgEEAYHyJAEBMIGyMDMGCCsGAQUFBwIB
49
+ FidodHRwOi8vd3d3LmZyZWV0c2Eub3JnL2ZyZWV0c2FfY3BzLmh0bWwwMgYIKwYB
50
+ BQUHAgEWJmh0dHA6Ly93d3cuZnJlZXRzYS5vcmcvZnJlZXRzYV9jcHMucGRmMEcG
51
+ CCsGAQUFBwICMDsaOUZyZWVUU0EgdHJ1c3RlZCB0aW1lc3RhbXBpbmcgU29mdHdh
52
+ cmUgYXMgYSBTZXJ2aWNlIChTYWFTKTA3BggrBgEFBQcBAQQrMCkwJwYIKwYBBQUH
53
+ MAGGG2h0dHA6Ly93d3cuZnJlZXRzYS5vcmc6MjU2MDANBgkqhkiG9w0BAQ0FAAOC
54
+ AgEAaK9+v5OFYu9M6ztYC+L69sw1omdyli89lZAfpWMMh9CRmJhM6KBqM/ipwoLt
55
+ nxyxGsbCPhcQjuTvzm+ylN6VwTMmIlVyVSLKYZcdSjt/eCUN+41K7sD7GVmxZBAF
56
+ ILnBDmTGJmLkrU0KuuIpj8lI/E6Z6NnmuP2+RAQSHsfBQi6sssnXMo4HOW5gtPO7
57
+ gDrUpVXID++1P4XndkoKn7Svw5n0zS9fv1hxBcYIHPPQUze2u30bAQt0n0iIyRLz
58
+ aWuhtpAtd7ffwEbASgzB7E+NGF4tpV37e8KiA2xiGSRqT5ndu28fgpOY87gD3ArZ
59
+ DctZvvTCfHdAS5kEO3gnGGeZEVLDmfEsv8TGJa3AljVa5E40IQDsUXpQLi8G+UC4
60
+ 1DWZu8EVT4rnYaCw1VX7ShOR1PNCCvjb8S8tfdudd9zhU3gEB0rxdeTy1tVbNLXW
61
+ 99y90xcwr1ZIDUwM/xQ/noO8FRhm0LoPC73Ef+J4ZBdrvWwauF3zJe33d4ibxEcb
62
+ 8/pz5WzFkeixYM2nsHhqHsBKw7JPouKNXRnl5IAE1eFmqDyC7G/VT7OF669xM6hb
63
+ Ut5G21JE4cNK6NNucS+fzg1JPX0+3VhsYZjj7D5uljRvQXrJ8iHgr/M6j2oLHvTA
64
+ I2MLdq2qjZFDOCXsxBxJpbmLGBx9ow6ZerlUxzws2AWv2pk=
65
+ -----END CERTIFICATE-----
66
+ `,
67
+ },
68
+ {
69
+ id: "digicert-g4",
70
+ name: "DigiCert Trusted Root G4",
71
+ sha256: "552f7bdcf1a7af9e6ce672017f4f12abf77240c78e761ac203d1d9d20ac89988",
72
+ pem: `
73
+ -----BEGIN CERTIFICATE-----
74
+ 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
104
+ -----END CERTIFICATE-----
105
+ `,
106
+ },
107
+ {
108
+ id: "sectigo-r46",
109
+ name: "Sectigo Public Time Stamping Root R46",
110
+ sha256: "4941b001b8a97e961b7817c9d9e960ec4b056bfc915a8c1aabf6ef6b3ac046a5",
111
+ pem: `
112
+ -----BEGIN CERTIFICATE-----
113
+ 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
142
+ XftzTXTX48FRiJ6PA9o=
143
+ -----END CERTIFICATE-----
144
+ `,
145
+ },
146
+ ];
147
+ /** The default roots, as DER. */
148
+ export function knownTsaRootsDer() {
149
+ return KNOWN_TSA_ROOTS.flatMap((r) => fromPem(r.pem));
150
+ }
package/dist/tsp.d.ts ADDED
@@ -0,0 +1,37 @@
1
+ /** A TimeStampReq for a SHA-256 digest, asking for the signing certificate to be included. */
2
+ export declare function buildTimestampRequest(digest: Uint8Array, nonce: Uint8Array): Uint8Array<ArrayBuffer>;
3
+ export type TimestampReply = {
4
+ granted: true;
5
+ token: Uint8Array<ArrayBuffer>;
6
+ } | {
7
+ granted: false;
8
+ reason: string;
9
+ };
10
+ /** Reads a TimeStampResp. A refusal is a result, not an error; malformed bytes throw. */
11
+ export declare function parseTimestampReply(bytes: Uint8Array): TimestampReply;
12
+ export type TimestampExpectation = {
13
+ /** The SHA-256 digest that should have been stamped. */
14
+ digest: Uint8Array;
15
+ /** The nonce sent with the request, when checking a fresh reply. */
16
+ nonce?: Uint8Array;
17
+ /** Trusted TSA roots, as DER certificates. */
18
+ roots: readonly Uint8Array[];
19
+ };
20
+ export type TimestampInfo = {
21
+ /** The stamped time, as the TSA states it. */
22
+ gen_time: string;
23
+ serial: string;
24
+ policy: string;
25
+ /** Signing certificate first, trusted root last. */
26
+ chain: string[];
27
+ /** SHA-256 (hex) of the trusted root the chain ends at: which authority vouches for the time. */
28
+ root_sha256: string;
29
+ };
30
+ export type TimestampResult = ({
31
+ ok: true;
32
+ } & TimestampInfo) | {
33
+ ok: false;
34
+ reason: string;
35
+ };
36
+ /** Verifies a TimeStampToken. Never throws: anything malformed is a failure. */
37
+ export declare function verifyTimestampToken(token: Uint8Array, expect: TimestampExpectation): Promise<TimestampResult>;