@agent-custody/receipts 0.3.0 → 0.4.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.
package/README.md CHANGED
@@ -277,13 +277,14 @@ The design is two producers feeding one verifier. The SDK is the top of the funn
277
277
  - Remote log: the issuer can append to a log run by someone else over HTTP, whose key then signs the tree heads, so a verifier learns the receipt was in a log the operator could not rewrite. Includes the reference log server, bearer-token auth, and a root endpoint for auditors.
278
278
  - Framework adapters, each tested against the real package with a scripted model and no network: OpenAI Agents SDK (`wrapTools` enforces, `observeRunner` records from lifecycle events), Vercel AI SDK (`wrapTools` over a real `generateText` loop), LangChain (`ReceiptCallbackHandler` records, `issuer.wrap` enforces).
279
279
 
280
+ - A log for someone else: `hashOnly` sends leaf hashes so the log never holds a receipt; the reference server runs several tenant logs at `/t/<tenant>/` with their own tokens and ids; tree heads name their log and the verifier checks it with `--log-id`. Phase 1 of the hosted log, [issue #6](https://github.com/ch4r10t33r/agent-custody/issues/6).
280
281
  - OpenTelemetry export: with `otel` in either config, every receipt is also one span at the collector the team already runs, trace id equal to the receipt id, attributes for tool, agent, principal, status, decision, and log position; after the receipt, best effort, never on the evidence path.
281
282
  - The REST connector: a plain HTTP API described as tools in the gateway config, credentials from the environment, so an agent's direct API calls become receipted, policy-checked tool calls through the gateway.
282
283
  - Pre-commit authorization for consequential tools: named in `precommit`, a call is signed and logged before it is forwarded, withheld if the log will not take it, and its receipt carries the committed authorization with proof that it precedes the execution.
283
284
 
284
285
  **Next, in the order it pays off**
285
286
 
286
- 1. A log run by someone who is not the operator, hosted: a tenant with a bearer token per fleet, consistency proofs served, retention. The reference log server is here; the tenanted service is [issue #6](https://github.com/ch4r10t33r/agent-custody/issues/6), and it comes before everything below.
287
+ 1. The hosted log, [issue #6](https://github.com/ch4r10t33r/agent-custody/issues/6), phases 2 and 3: the Postgres store with one writer per tenant, tokens and rate limits, the signer as its own process, published checkpoints, and well-known keys fetched with `--log-url`. Phase 1 is done; it comes before everything below.
287
288
  2. Post-quantum signatures: ML-DSA beside Ed25519 in the same DSSE envelope, hybrid by default when a PQ key is present, in every signed artefact and in the browser verifier. [Issue #11](https://github.com/ch4r10t33r/agent-custody/issues/11).
288
289
  3. An HTTP transport for the gateway, with the grant presented per connection, for a shared deployment rather than one process per agent session.
289
290
  4. Delegation chains for sub-agents.
package/dist/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { parseArgs } from "node:util";
3
3
  import { readFileSync, writeFileSync } from "node:fs";
4
+ import { dirname, resolve } from "node:path";
4
5
  import { loadConfig, loadSdkConfig } from "./config.js";
5
6
  import { generateKeyPair, loadPrivateKey, loadPublicKey, writeKeyPair } from "./crypto.js";
6
7
  import { createDelegation } from "./delegation.js";
@@ -29,10 +30,24 @@ const USAGE = `agent-custody <command>
29
30
  prune --log <log.jsonl> --before <ISO instant> [--receipts <dir>]
30
31
  retention on the receipt log: replaces older leaves with their hashes, so proofs still verify and the content is gone
31
32
  log --file <log.jsonl> --key <log.key> [--port 8787] [--host 127.0.0.1] [--token-env <NAME>] reference log server
32
- verify <bundle.json> --issuer-key <pub> [--principal-key <pub>] [--log-key <pub>] [--upstream-key <pub>] [--stripe-secret-env NAME] [--github-secret-env NAME] [--log <log.jsonl>] [--json]
33
- audit --older <bundle.json> --newer <bundle.json> (--log <log.jsonl> | --log-url <url>) --issuer-key <pub> [--log-key <pub>] [--json]
33
+ verify <bundle.json> --issuer-key <pub> [--principal-key <pub>] [--log-key <pub>] [--log-id <id>] [--upstream-key <pub>] [--stripe-secret-env NAME] [--github-secret-env NAME] [--log <log.jsonl>] [--json]
34
+ audit --older <bundle.json> --newer <bundle.json> (--log <log.jsonl> | --log-url <url>) --issuer-key <pub> [--log-key <pub>] [--log-id <id>] [--json]
34
35
  checks that the newer receipt's log extends the older one's: nothing between them was rewritten
35
36
  `;
37
+ /** The tenants file for `log --tenants`: paths relative to the file, tokens from the environment, ids default to the tenant name. */
38
+ function loadTenants(path) {
39
+ const raw = JSON.parse(readFileSync(path, "utf8"));
40
+ const out = {};
41
+ for (const [name, t] of Object.entries(raw)) {
42
+ if (!/^[A-Za-z0-9_.-]+$/.test(name) || typeof t?.file !== "string")
43
+ throw new Error(`tenants: "${name}" needs a file, and its name must be a plain identifier`);
44
+ const token = t.tokenEnv ? process.env[t.tokenEnv] : undefined;
45
+ if (t.tokenEnv && !token)
46
+ throw new Error(`tenants: environment variable ${t.tokenEnv} for "${name}" is not set`);
47
+ out[name] = { file: resolve(dirname(resolve(path)), t.file), ...(token ? { tokens: [token] } : {}), ...(t.logId ? { logId: t.logId } : {}) };
48
+ }
49
+ return out;
50
+ }
36
51
  async function main(argv) {
37
52
  const [cmd, ...rest] = argv;
38
53
  switch (cmd) {
@@ -116,7 +131,7 @@ async function main(argv) {
116
131
  case "log": {
117
132
  const { values } = parseArgs({
118
133
  args: rest,
119
- options: { file: { type: "string" }, key: { type: "string" }, port: { type: "string", default: "8787" }, host: { type: "string", default: "127.0.0.1" }, "token-env": { type: "string" } },
134
+ options: { file: { type: "string" }, key: { type: "string" }, port: { type: "string", default: "8787" }, host: { type: "string", default: "127.0.0.1" }, "token-env": { type: "string" }, "log-id": { type: "string" }, tenants: { type: "string" } },
120
135
  });
121
136
  if (!values.file || !values.key)
122
137
  throw new Error("log needs --file and --key");
@@ -124,8 +139,10 @@ async function main(argv) {
124
139
  if (values["token-env"] && !token)
125
140
  throw new Error(`log: environment variable ${values["token-env"]} is not set`);
126
141
  const key = loadPrivateKey(values.key);
127
- const running = await serveLog(values.file, key, { port: Number(values.port), host: values.host, ...(token ? { tokens: [token] } : {}) });
128
- console.error(`agent-custody log: ${running.url} keyid=${key.keyid} file=${values.file} ${token ? "bearer token required" : "open, anyone may append"}`);
142
+ // --tenants names a JSON file { "<tenant>": { "file": "...", "tokenEnv": "NAME", "logId": "..." } }; each is reached at /t/<tenant>/.
143
+ const tenants = values.tenants ? loadTenants(values.tenants) : undefined;
144
+ const running = await serveLog(values.file, key, { port: Number(values.port), host: values.host, ...(token ? { tokens: [token] } : {}), ...(values["log-id"] ? { logId: values["log-id"] } : {}), ...(tenants ? { tenants } : {}) });
145
+ console.error(`agent-custody log: ${running.url} keyid=${key.keyid} file=${values.file}${values["log-id"] ? ` log=${values["log-id"]}` : ""} ${token ? "bearer token required" : "open, anyone may append"}${tenants ? ` tenants=${Object.keys(tenants).join(",")}` : ""}`);
129
146
  await new Promise((resolve) => process.once("SIGINT", resolve));
130
147
  await running.close();
131
148
  return 0;
@@ -139,6 +156,7 @@ async function main(argv) {
139
156
  "gateway-key": { type: "string", multiple: true },
140
157
  "principal-key": { type: "string", multiple: true },
141
158
  "log-key": { type: "string", multiple: true },
159
+ "log-id": { type: "string" },
142
160
  "upstream-key": { type: "string", multiple: true },
143
161
  "stripe-secret-env": { type: "string" },
144
162
  "github-secret-env": { type: "string" },
@@ -155,6 +173,7 @@ async function main(argv) {
155
173
  issuerKeys: issuerKeyFiles.map(loadPublicKey),
156
174
  principalKeys: (values["principal-key"] ?? []).map(loadPublicKey),
157
175
  ...(values["log-key"] ? { logKeys: values["log-key"].map(loadPublicKey) } : {}),
176
+ ...(values["log-id"] ? { logId: values["log-id"] } : {}),
158
177
  ...(values["upstream-key"] ? { upstreamKeys: values["upstream-key"].map(loadPublicKey) } : {}),
159
178
  ...(values["stripe-secret-env"] || values["github-secret-env"] ? { providerSecrets: { ...(values["stripe-secret-env"] ? { stripe: secretFrom(values["stripe-secret-env"]) } : {}), ...(values["github-secret-env"] ? { github: secretFrom(values["github-secret-env"]) } : {}) } } : {}),
160
179
  ...(values.log ? { logFile: values.log } : {}),
@@ -172,6 +191,7 @@ async function main(argv) {
172
191
  "log-url": { type: "string" },
173
192
  "issuer-key": { type: "string", multiple: true },
174
193
  "log-key": { type: "string", multiple: true },
194
+ "log-id": { type: "string" },
175
195
  json: { type: "boolean", default: false },
176
196
  },
177
197
  });
@@ -193,7 +213,7 @@ async function main(argv) {
193
213
  throw new Error(`log refused the consistency query: ${res.status}`);
194
214
  proof = (await res.json()).hashes;
195
215
  }
196
- const result = auditExtends(older, newer, proof, keyFiles.map(loadPublicKey));
216
+ const result = auditExtends(older, newer, proof, keyFiles.map(loadPublicKey), values["log-id"]);
197
217
  if (values.json)
198
218
  console.log(JSON.stringify(result, null, 2));
199
219
  else {
package/dist/config.d.ts CHANGED
@@ -174,6 +174,7 @@ export declare const GatewayConfigSchema: z.ZodObject<{
174
174
  log: z.ZodOptional<z.ZodObject<{
175
175
  url: z.ZodString;
176
176
  tokenEnv: z.ZodOptional<z.ZodString>;
177
+ hashOnly: z.ZodOptional<z.ZodBoolean>;
177
178
  }, z.core.$strip>>;
178
179
  otel: z.ZodOptional<z.ZodObject<{
179
180
  url: z.ZodString;
@@ -197,6 +198,7 @@ export declare const SdkConfigSchema: z.ZodObject<{
197
198
  log: z.ZodOptional<z.ZodObject<{
198
199
  url: z.ZodString;
199
200
  tokenEnv: z.ZodOptional<z.ZodString>;
201
+ hashOnly: z.ZodOptional<z.ZodBoolean>;
200
202
  }, z.core.$strip>>;
201
203
  otel: z.ZodOptional<z.ZodObject<{
202
204
  url: z.ZodString;
package/dist/config.js CHANGED
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { dirname, resolve } from "node:path";
4
4
  /** Where receipts are logged: a local file, or a log reached over HTTP whose bearer token comes from an environment variable. */
5
- const LogSchema = z.object({ url: z.string().url(), tokenEnv: z.string().min(1).optional() });
5
+ const LogSchema = z.object({ url: z.string().url(), tokenEnv: z.string().min(1).optional(), /** send leaf hashes only; the log never holds the receipt. Use it for any log run by someone else */ hashOnly: z.boolean().optional() });
6
6
  const oneLog = { message: "exactly one of logFile or log is required" };
7
7
  /** Optional OpenTelemetry export: every receipt also becomes a span at this OTLP/HTTP collector, after it is issued. Never on the evidence path. */
8
8
  const OtelSchema = z.object({ url: z.string().url(), headersEnv: z.record(z.string(), z.string().min(1)).optional(), serviceName: z.string().min(1).optional() });
@@ -17,6 +17,8 @@ export declare function fileLog(file: string, key: KeyPair): LogSink;
17
17
  export interface HttpLogOptions {
18
18
  /** sent as a bearer token; the log decides what it is worth */
19
19
  token?: string;
20
+ /** send only the leaf hash; the log then commits to the receipt without ever holding it. Use it for any log run by someone else. */
21
+ hashOnly?: boolean;
20
22
  fetch?: typeof fetch;
21
23
  }
22
24
  /** A log reached over HTTP: POST <url>/append with {leaf}, expecting a LogAppend back. */
@@ -26,6 +28,7 @@ export interface LogConfig {
26
28
  log?: {
27
29
  url: string;
28
30
  tokenEnv?: string | undefined;
31
+ hashOnly?: boolean | undefined;
29
32
  } | undefined;
30
33
  }
31
34
  /** The sink a config asks for: a remote log when `log` is set, otherwise the local file. */
@@ -33,6 +36,18 @@ export declare function openLog(cfg: LogConfig, key: KeyPair): LogSink;
33
36
  export interface LogServerOptions {
34
37
  /** bearer tokens accepted on append; when empty, anyone may append */
35
38
  tokens?: string[];
39
+ /** the id written into every tree head this log signs, so a head cannot be presented as another log's */
40
+ logId?: string;
41
+ /**
42
+ * More logs behind the same server, one per tenant, reached at /t/<tenant>/...: each with its own file, tokens,
43
+ * and id. The default log stays at the root paths. This is the shape a hosted log takes; the reference server
44
+ * runs it from a small JSON file.
45
+ */
46
+ tenants?: Record<string, {
47
+ file: string;
48
+ tokens?: string[];
49
+ logId?: string;
50
+ }>;
36
51
  }
37
52
  /**
38
53
  * The reference log server as a node:http request handler.
package/dist/log-sink.js CHANGED
@@ -6,11 +6,11 @@
6
6
  import { timingSafeEqual } from "node:crypto";
7
7
  import { createServer } from "node:http";
8
8
  import { dsseSign } from "./crypto.js";
9
- import { MerkleLog } from "./log.js";
9
+ import { leafHash, MerkleLog } from "./log.js";
10
10
  import { TREEHEAD_TYPE } from "./receipt.js";
11
- function appendSigned(log, key, leaf) {
12
- const e = log.append(leaf);
13
- const head = { treeSize: e.treeSize, rootHash: e.rootHash, timestamp: new Date().toISOString() };
11
+ function appendSigned(log, key, leaf, logId) {
12
+ const e = "leaf" in leaf ? log.append(leaf.leaf) : log.appendHash(leaf.leafHash);
13
+ const head = { treeSize: e.treeSize, rootHash: e.rootHash, timestamp: new Date().toISOString(), ...(logId ? { log: logId } : {}) };
14
14
  return { inclusion: { leafIndex: e.leafIndex, treeSize: e.treeSize, hashes: e.hashes }, treeHead: dsseSign(TREEHEAD_TYPE, head, key) };
15
15
  }
16
16
  /** A local JSONL Merkle log. Tree heads are signed with the given key, normally the issuer's own. */
@@ -20,7 +20,7 @@ export function fileLog(file, key) {
20
20
  kind: "file",
21
21
  where: file,
22
22
  async append(leaf) {
23
- return appendSigned(log, key, leaf);
23
+ return appendSigned(log, key, { leaf });
24
24
  },
25
25
  };
26
26
  }
@@ -35,7 +35,7 @@ export function httpLog(url, opts = {}) {
35
35
  const res = await f(new URL("append", base), {
36
36
  method: "POST",
37
37
  headers: { "content-type": "application/json", ...(opts.token ? { authorization: `Bearer ${opts.token}` } : {}) },
38
- body: JSON.stringify({ leaf }),
38
+ body: JSON.stringify(opts.hashOnly ? { leafHash: leafHash(leaf).toString("hex") } : { leaf }),
39
39
  });
40
40
  if (!res.ok)
41
41
  throw new Error(`log ${url} refused the append: ${res.status} ${(await res.text()).slice(0, 200)}`);
@@ -52,7 +52,7 @@ export function openLog(cfg, key) {
52
52
  const token = cfg.log.tokenEnv ? process.env[cfg.log.tokenEnv] : undefined;
53
53
  if (cfg.log.tokenEnv && !token)
54
54
  throw new Error(`log token: environment variable ${cfg.log.tokenEnv} is not set`);
55
- return httpLog(cfg.log.url, token === undefined ? {} : { token });
55
+ return httpLog(cfg.log.url, { ...(token === undefined ? {} : { token }), ...(cfg.log.hashOnly ? { hashOnly: true } : {}) });
56
56
  }
57
57
  if (!cfg.logFile)
58
58
  throw new Error("config needs logFile or log.url");
@@ -66,9 +66,11 @@ export function openLog(cfg, key) {
66
66
  * GET /head -> {treeHead}, the current tree head signed with the log's key
67
67
  */
68
68
  export function logHandler(file, key, opts = {}) {
69
- const log = new MerkleLog(file);
70
- const tokens = opts.tokens ?? [];
71
- const authorized = (req) => {
69
+ const root = { log: new MerkleLog(file), tokens: opts.tokens ?? [], logId: opts.logId };
70
+ const tenants = new Map();
71
+ for (const [name, t] of Object.entries(opts.tenants ?? {}))
72
+ tenants.set(name, { log: new MerkleLog(t.file), tokens: t.tokens ?? [], logId: t.logId ?? name });
73
+ const authorized = (req, tokens) => {
72
74
  if (tokens.length === 0)
73
75
  return true;
74
76
  const h = req.headers.authorization ?? "";
@@ -84,22 +86,33 @@ export function logHandler(file, key, opts = {}) {
84
86
  res.end(JSON.stringify(body));
85
87
  };
86
88
  const url = new URL(req.url ?? "/", "http://localhost");
89
+ // /t/<tenant>/<op> reaches that tenant's log; anything else is the default log.
90
+ const m = /^\/t\/([A-Za-z0-9_.-]+)\/(append|root|consistency|head)$/.exec(url.pathname);
91
+ const which = m ? tenants.get(m[1]) : root;
92
+ if (!which)
93
+ return json(404, { error: "unknown log" });
94
+ const { log, tokens, logId } = which;
87
95
  if (req.method === "POST" && url.pathname.endsWith("/append")) {
88
- if (!authorized(req))
96
+ if (!authorized(req, tokens))
89
97
  return json(401, { error: "unauthorized" });
90
98
  let body = "";
91
99
  for await (const chunk of req)
92
100
  body += chunk;
93
- let leaf;
101
+ let parsed;
94
102
  try {
95
- leaf = JSON.parse(body).leaf;
103
+ parsed = JSON.parse(body);
96
104
  }
97
105
  catch {
98
- return json(400, { error: "body must be JSON {leaf}" });
106
+ return json(400, { error: "body must be JSON {leaf} or {leafHash}" });
107
+ }
108
+ if (typeof parsed.leafHash === "string") {
109
+ if (!/^[0-9a-f]{64}$/.test(parsed.leafHash))
110
+ return json(400, { error: "leafHash must be 64 lowercase hex characters" });
111
+ return json(200, appendSigned(log, key, { leafHash: parsed.leafHash }, logId));
99
112
  }
100
- if (typeof leaf !== "string" || leaf.length === 0)
101
- return json(400, { error: "leaf must be a non-empty string" });
102
- return json(200, appendSigned(log, key, leaf));
113
+ if (typeof parsed.leaf !== "string" || parsed.leaf.length === 0)
114
+ return json(400, { error: "leaf must be a non-empty string, or send leafHash" });
115
+ return json(200, appendSigned(log, key, { leaf: parsed.leaf }, logId));
103
116
  }
104
117
  if (req.method === "GET" && url.pathname.endsWith("/root")) {
105
118
  const size = url.searchParams.has("size") ? Number(url.searchParams.get("size")) : log.size;
@@ -115,7 +128,7 @@ export function logHandler(file, key, opts = {}) {
115
128
  return json(200, { oldSize, newSize, hashes: log.consistencyProof(oldSize, newSize) });
116
129
  }
117
130
  if (req.method === "GET" && url.pathname.endsWith("/head")) {
118
- const head = { treeSize: log.size, rootHash: log.root(), timestamp: new Date().toISOString() };
131
+ const head = { treeSize: log.size, rootHash: log.root(), timestamp: new Date().toISOString(), ...(logId ? { log: logId } : {}) };
119
132
  return json(200, { treeHead: dsseSign(TREEHEAD_TYPE, head, key) });
120
133
  }
121
134
  return json(404, { error: "not found" });
package/dist/log.d.ts CHANGED
@@ -22,6 +22,14 @@ export declare class MerkleLog {
22
22
  append(leaf: string): InclusionProof & {
23
23
  rootHash: string;
24
24
  };
25
+ /**
26
+ * Appends a leaf by its RFC 6962 leaf hash, sha256(0x00 || leaf), as hex. The log commits to the leaf without ever
27
+ * holding it, which is what a log run for someone else should do: the receipt stays with its issuer.
28
+ */
29
+ appendHash(leafHashHex: string): InclusionProof & {
30
+ rootHash: string;
31
+ };
32
+ private pushHash;
25
33
  root(size?: number): string;
26
34
  /** Proof that this log at newSize extends its own earlier state at oldSize. */
27
35
  consistencyProof(oldSize: number, newSize?: number): string[];
package/dist/log.js CHANGED
@@ -165,8 +165,9 @@ export class MerkleLog {
165
165
  if (!line.trim())
166
166
  continue;
167
167
  const parsed = JSON.parse(line);
168
- // A pruned leaf keeps only its hash: the tree, its roots, and every proof are unchanged; the content is gone.
169
- this.hashes.push(typeof parsed === "string" ? leafHash(parsed) : Buffer.from(parsed.pruned, "hex"));
168
+ // A pruned leaf keeps only its hash, and a leaf appended by hash never had content here: the tree, its
169
+ // roots, and every proof are the same either way.
170
+ this.hashes.push(typeof parsed === "string" ? leafHash(parsed) : Buffer.from((parsed.pruned ?? parsed.hash), "hex"));
170
171
  }
171
172
  }
172
173
  else {
@@ -179,7 +180,20 @@ export class MerkleLog {
179
180
  /** Appends a leaf (an opaque string, typically a canonical JSON envelope). Returns its proof against the new root. */
180
181
  append(leaf) {
181
182
  appendFileSync(this.file, JSON.stringify(leaf) + "\n");
182
- this.hashes.push(leafHash(leaf));
183
+ return this.pushHash(leafHash(leaf));
184
+ }
185
+ /**
186
+ * Appends a leaf by its RFC 6962 leaf hash, sha256(0x00 || leaf), as hex. The log commits to the leaf without ever
187
+ * holding it, which is what a log run for someone else should do: the receipt stays with its issuer.
188
+ */
189
+ appendHash(leafHashHex) {
190
+ if (!/^[0-9a-f]{64}$/.test(leafHashHex))
191
+ throw new Error("leafHash must be 64 lowercase hex characters");
192
+ appendFileSync(this.file, JSON.stringify({ hash: leafHashHex }) + "\n");
193
+ return this.pushHash(Buffer.from(leafHashHex, "hex"));
194
+ }
195
+ pushHash(hash) {
196
+ this.hashes.push(hash);
183
197
  const treeSize = this.hashes.length;
184
198
  return { leafIndex: treeSize - 1, treeSize, hashes: this.tree.path(treeSize - 1, 0, treeSize).map((b) => b.toString("hex")), rootHash: this.tree.mth(0, treeSize).toString("hex") };
185
199
  }
package/dist/receipt.d.ts CHANGED
@@ -153,6 +153,8 @@ export interface TreeHead {
153
153
  treeSize: number;
154
154
  rootHash: string;
155
155
  timestamp: string;
156
+ /** which log this head belongs to, when the log serves more than one; a verifier told which log to expect checks it */
157
+ log?: string;
156
158
  }
157
159
  /** What gets written to disk and handed to a verifier. Self-contained apart from public keys. */
158
160
  export interface ReceiptBundle {
package/dist/verify.d.ts CHANGED
@@ -18,6 +18,8 @@ export interface VerifyOptions {
18
18
  providerSecrets?: ProviderSecrets;
19
19
  /** If given, the root is recomputed from this log file at the receipt's tree size and compared. */
20
20
  logFile?: string;
21
+ /** If given, every tree head in the bundle must name this log, so a head from another tenant's log cannot be presented as this one's. */
22
+ logId?: string;
21
23
  }
22
24
  export interface VerifyResult {
23
25
  ok: boolean;
@@ -35,6 +37,6 @@ export interface AuditResult {
35
37
  * Does the newer tree head extend the older one? Both must be signed by a trusted log or issuer key, and the proof
36
38
  * must be the log's consistency proof between the two sizes. A pass means nothing in the older log was rewritten.
37
39
  */
38
- export declare function auditExtends(older: Envelope, newer: Envelope, proof: string[], keys: PublicKeyRef[]): AuditResult;
40
+ export declare function auditExtends(older: Envelope, newer: Envelope, proof: string[], keys: PublicKeyRef[], logId?: string): AuditResult;
39
41
  /** Human-readable report: checks, then every field with its provenance so the reader knows what was proven vs. claimed. */
40
42
  export declare function formatReport(r: VerifyResult): string;
package/dist/verify.js CHANGED
@@ -92,6 +92,8 @@ export function verifyBundle(bundle, opts) {
92
92
  add("tree head signature", th.ok && bundle.treeHead.payloadType === TREEHEAD_TYPE, th.ok ? `${byLog ? "log key" : "issuer key"} ${short(th.keyid)}` : th.error);
93
93
  if (th.ok) {
94
94
  const head = th.payload;
95
+ if (opts.logId !== undefined)
96
+ add("tree head names the expected log", head.log === opts.logId, head.log ? `log ${head.log}` : "tree head names no log");
95
97
  add("tree head matches inclusion proof size", head.treeSize === bundle.inclusion.treeSize);
96
98
  const included = verifyInclusion(leafHash(canonicalize(bundle.envelope)), bundle.inclusion, head.rootHash);
97
99
  add("log inclusion proof", included, `leaf ${bundle.inclusion.leafIndex} of ${bundle.inclusion.treeSize}, root ${short(head.rootHash)}`);
@@ -113,7 +115,7 @@ export function verifyBundle(bundle, opts) {
113
115
  * Does the newer tree head extend the older one? Both must be signed by a trusted log or issuer key, and the proof
114
116
  * must be the log's consistency proof between the two sizes. A pass means nothing in the older log was rewritten.
115
117
  */
116
- export function auditExtends(older, newer, proof, keys) {
118
+ export function auditExtends(older, newer, proof, keys, logId) {
117
119
  const checks = [];
118
120
  const add = (name, ok, detail) => {
119
121
  checks.push(detail === undefined ? { name, ok } : { name, ok, detail });
@@ -128,6 +130,10 @@ export function auditExtends(older, newer, proof, keys) {
128
130
  const b = decode("newer", newer);
129
131
  if (!a || !b)
130
132
  return { ok: false, checks, older: a, newer: b };
133
+ if (logId !== undefined)
134
+ add("both tree heads name the expected log", a.log === logId && b.log === logId, `expected ${logId}, got ${a.log ?? "none"} and ${b.log ?? "none"}`);
135
+ else if (a.log !== b.log)
136
+ add("both tree heads name the same log", false, `${a.log ?? "none"} and ${b.log ?? "none"}`);
131
137
  if (!add("older is not larger than newer", a.treeSize <= b.treeSize, `${a.treeSize} -> ${b.treeSize}`))
132
138
  return { ok: false, checks, older: a, newer: b };
133
139
  const consistent = verifyConsistency(a.treeSize, a.rootHash, b.treeSize, b.rootHash, proof);
package/docs/usage.md CHANGED
@@ -105,7 +105,7 @@ An upstream need not be an MCP server. A plain HTTP API is described as tools:
105
105
  "log": { "url": "https://log.example.com/", "tokenEnv": "AGENT_CUSTODY_LOG_TOKEN" }
106
106
  ```
107
107
 
108
- Exactly one of the two. The bearer token comes from the named environment variable, never from the file, and a missing variable fails at startup. With a remote log the tree head in each receipt is signed by the log's key, and a verifier must be given that key with `--log-key`. If the log refuses a leaf, the receipt is not issued and the call returns an error to the agent. For an ordinary call the upstream action has already happened by then, and the error says so; a receipt that was never logged must not be handed out. For a tool named in `precommit` the order is reversed, below, and the action never happens. The reference log server is `node src/cli.ts log --file log.jsonl --key keys/log.key --port 8787 --token-env AGENT_CUSTODY_LOG_TOKEN`. It serves `POST /append` (token required when one is configured), `GET /root?size=N`, `GET /consistency?old=M&new=N`, and `GET /head`; [verification.md](verification.md) says what each proves.
108
+ Exactly one of the two. The bearer token comes from the named environment variable, never from the file, and a missing variable fails at startup. Add `"hashOnly": true` for any log run by someone else: the gateway then sends only the leaf hash, sha256 of the receipt envelope with the RFC 6962 prefix, so the log commits to the receipt without ever holding it, and the receipts with their arguments and results stay in `receiptsDir`. The verifier does not change; it hashes the envelope itself. A log that serves several tenants is reached at `<url>/t/<tenant>/`, and each of its tree heads names its log, which a verifier checks with `--log-id`. With a remote log the tree head in each receipt is signed by the log's key, and a verifier must be given that key with `--log-key`. If the log refuses a leaf, the receipt is not issued and the call returns an error to the agent. For an ordinary call the upstream action has already happened by then, and the error says so; a receipt that was never logged must not be handed out. For a tool named in `precommit` the order is reversed, below, and the action never happens. The reference log server is `node src/cli.ts log --file log.jsonl --key keys/log.key --port 8787 --token-env AGENT_CUSTODY_LOG_TOKEN [--log-id <id>] [--tenants tenants.json]`. It serves `POST /append` with `{leaf}` or `{leafHash}` (token required when one is configured), `GET /root?size=N`, `GET /consistency?old=M&new=N`, and `GET /head`; [verification.md](verification.md) says what each proves. `--log-id` writes that id into every tree head. `--tenants` names a JSON file, `{ "acme": { "file": "acme.jsonl", "tokenEnv": "ACME_TOKEN", "logId": "acme-eu" } }`, and each tenant is its own log at `/t/acme/…` with its own token and id; the default log stays at the root paths. [deploy/](../../deploy/README.md) runs the server as a container.
109
109
 
110
110
  `otel`, optional in both the gateway and SDK configs, sends every receipt to the collector you already run as one span over OTLP/HTTP, after the receipt is issued: `"otel": { "url": "http://localhost:4318", "headersEnv": { "x-api-key": "OTEL_KEY" }, "serviceName": "support-agents" }`. The span's trace id is the receipt id, its attributes carry the tool, agent, principal, execution status, policy decision, and log position, and its status is an error only when the upstream failed or errored, since a denial is the policy working. Export is best effort: a collector that is down or refuses costs a line on stderr, never a receipt. Tutorial 18 shows it against a stand-in collector.
111
111
 
@@ -83,6 +83,7 @@ execution observed executed
83
83
  | authorization log inclusion proof | the authorization statement is a leaf of that tree | an authorization that was never logged |
84
84
  | authorization logged before execution | the authorization's leaf precedes the receipt's leaf, at a tree size no larger than the receipt's | evidence written after the side effect, dressed up as before |
85
85
  | tree head signature | the tree head was signed by a trusted issuer key | forged log position |
86
+ | tree head names the expected log | with `--log-id`: the tree head carries that log id, so a head from another tenant's log on the same server cannot be presented as this one's | a receipt logged to a different log than claimed |
86
87
  | tree head matches inclusion proof size | the proof and the tree head describe the same tree | mismatched bundle parts |
87
88
  | log inclusion proof | this exact envelope is a leaf of the tree with that root | receipt never logged, or logged then changed |
88
89
  | log file root matches tree head | recomputing the root from your copy of the log at that size gives the same value | your log copy and the issuer's history diverge: deletion, reordering, or edit |
@@ -160,7 +161,7 @@ An inclusion proof says a receipt was in the log at one moment. It does not say
160
161
 
161
162
  ```bash
162
163
  node src/cli.ts audit --older receipts/<earlier>.json --newer receipts/<later>.json --log log.jsonl --issuer-key keys/gateway.pub
163
- node src/cli.ts audit --older receipts/<earlier>.json --newer receipts/<later>.json --log-url https://log.example.com/ --log-key keys/log.pub
164
+ node src/cli.ts audit --older receipts/<earlier>.json --newer receipts/<later>.json --log-url https://log.example.com/t/acme/ --log-key keys/log.pub --log-id acme
164
165
  ```
165
166
 
166
167
  Both tree heads must be signed by a trusted key. With `--log` the proof is computed from a copy of the log; with `--log-url` it is fetched from the log's `GET /consistency?old=M&new=N`. Exit code 0 means the newer log extends the older one. A failure means either history was rewritten between the two heads or the proof belongs to other tree heads; example 14 shows a rewritten log failing this way while every individual receipt still verifies.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-custody/receipts",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Chain of custody for AI agents: signed, independently verifiable receipts for tool calls. MCP gateway + Cedar policy + Merkle transparency log",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -7,26 +7,26 @@
7
7
  "description": "The tree head from the first remote receipt and the log's later head, with the proof the log served.",
8
8
  "older": {
9
9
  "payloadType": "application/vnd.agent-custody.treehead+json",
10
- "payload": "eyJyb290SGFzaCI6IjY5NTU0NGFkNTA4YTEzYTk4NTUxM2YzYjJhMjU0MzhlZjk1MWIyMzk2MGQ1ODU0YjczZjFjYWRkMzJmOTI1YmQiLCJ0aW1lc3RhbXAiOiIyMDI2LTA5LTA4VDAxOjUwOjE1Ljc1NVoiLCJ0cmVlU2l6ZSI6MX0=",
10
+ "payload": "eyJsb2ciOiJ2ZWN0b3JzLWxvZyIsInJvb3RIYXNoIjoiODViYjZhMjk1YzYxYmIzMzg5MWYyNTFiNWNiZGQ4NzNjNGVkYTU5OTBkMWE4MzVjNDRjYjZiMDgwNTBhY2U2MiIsInRpbWVzdGFtcCI6IjIwMjYtMDktMDhUMDY6MzA6MDQuNjkzWiIsInRyZWVTaXplIjoxfQ==",
11
11
  "signatures": [
12
12
  {
13
- "keyid": "c150ba37105c4e66f85239796e4f07b97c0fc431a9c1118d2bd1554ba35dab54",
14
- "sig": "Dy7+sDttHIIQCUzGlIL7Yro+UFGLSPjnLuBJYxFdq2wiqkkAdA3ipo09aSoS0h5BwJMOOEqOTmrGb/ot7HZTCQ=="
13
+ "keyid": "74356bcffe957c1c3a3bde6702505f536d4e8bab2aac3b0f36419e846da1a8d5",
14
+ "sig": "xhkG/l7l3ElnvGnxrFfK2nYUCkRo0ktj0+6ctv6V1qKiXihMYexcOu/qepdqWWN0mWFq2u8/Aqo6kbdoWhiFDQ=="
15
15
  }
16
16
  ]
17
17
  },
18
18
  "newer": {
19
19
  "payloadType": "application/vnd.agent-custody.treehead+json",
20
- "payload": "eyJyb290SGFzaCI6IjU0YzBkNDY1NDhjYzdlYzcyMTQ1MDNlMzFjNzhhYTY5MDZhMmRkY2VhZTI3OTI0N2M5NzRjMjljYmY3Y2Q4NGMiLCJ0aW1lc3RhbXAiOiIyMDI2LTA5LTA4VDAxOjUwOjE1Ljc1OFoiLCJ0cmVlU2l6ZSI6Mn0=",
20
+ "payload": "eyJsb2ciOiJ2ZWN0b3JzLWxvZyIsInJvb3RIYXNoIjoiODY0N2E0MTczNTIxYzE5Mzg4NTg2Y2U1MzYyNGU3MGZiYWEyMzc0NGRiYjIzNTQ0ZDU2N2Q5NDY0NmE0ZTM3MiIsInRpbWVzdGFtcCI6IjIwMjYtMDktMDhUMDY6MzA6MDQuNjk3WiIsInRyZWVTaXplIjoyfQ==",
21
21
  "signatures": [
22
22
  {
23
- "keyid": "c150ba37105c4e66f85239796e4f07b97c0fc431a9c1118d2bd1554ba35dab54",
24
- "sig": "4belmD02Vv4dufyaCY2N8J48+bxQmMtMfg+jNyD3wadZftcYgQPkZJ2mUx7gCfoh5lOwEu+Wzd5Id1yZlHYvAQ=="
23
+ "keyid": "74356bcffe957c1c3a3bde6702505f536d4e8bab2aac3b0f36419e846da1a8d5",
24
+ "sig": "iaweKtCddKNA0R+dkq+0qyzwe8RhZdKgK6y9o21adUCjhMAGlpl+YDe1chzn4NvT7jIZvn+HDdll136TmqcmCQ=="
25
25
  }
26
26
  ]
27
27
  },
28
28
  "proof": [
29
- "32a570791cc4546f2b7cf560a2fe3459eb504e0cae52f0ce521fea311f9f15ba"
29
+ "f8e988293efa93dd5e5ea2af96b88588f4a1fba5c786848e997aa985c95e2f81"
30
30
  ],
31
31
  "keys": [
32
32
  "log"
@@ -41,26 +41,26 @@
41
41
  "description": "The same heads the wrong way round.",
42
42
  "older": {
43
43
  "payloadType": "application/vnd.agent-custody.treehead+json",
44
- "payload": "eyJyb290SGFzaCI6IjU0YzBkNDY1NDhjYzdlYzcyMTQ1MDNlMzFjNzhhYTY5MDZhMmRkY2VhZTI3OTI0N2M5NzRjMjljYmY3Y2Q4NGMiLCJ0aW1lc3RhbXAiOiIyMDI2LTA5LTA4VDAxOjUwOjE1Ljc1OFoiLCJ0cmVlU2l6ZSI6Mn0=",
44
+ "payload": "eyJsb2ciOiJ2ZWN0b3JzLWxvZyIsInJvb3RIYXNoIjoiODY0N2E0MTczNTIxYzE5Mzg4NTg2Y2U1MzYyNGU3MGZiYWEyMzc0NGRiYjIzNTQ0ZDU2N2Q5NDY0NmE0ZTM3MiIsInRpbWVzdGFtcCI6IjIwMjYtMDktMDhUMDY6MzA6MDQuNjk3WiIsInRyZWVTaXplIjoyfQ==",
45
45
  "signatures": [
46
46
  {
47
- "keyid": "c150ba37105c4e66f85239796e4f07b97c0fc431a9c1118d2bd1554ba35dab54",
48
- "sig": "4belmD02Vv4dufyaCY2N8J48+bxQmMtMfg+jNyD3wadZftcYgQPkZJ2mUx7gCfoh5lOwEu+Wzd5Id1yZlHYvAQ=="
47
+ "keyid": "74356bcffe957c1c3a3bde6702505f536d4e8bab2aac3b0f36419e846da1a8d5",
48
+ "sig": "iaweKtCddKNA0R+dkq+0qyzwe8RhZdKgK6y9o21adUCjhMAGlpl+YDe1chzn4NvT7jIZvn+HDdll136TmqcmCQ=="
49
49
  }
50
50
  ]
51
51
  },
52
52
  "newer": {
53
53
  "payloadType": "application/vnd.agent-custody.treehead+json",
54
- "payload": "eyJyb290SGFzaCI6IjY5NTU0NGFkNTA4YTEzYTk4NTUxM2YzYjJhMjU0MzhlZjk1MWIyMzk2MGQ1ODU0YjczZjFjYWRkMzJmOTI1YmQiLCJ0aW1lc3RhbXAiOiIyMDI2LTA5LTA4VDAxOjUwOjE1Ljc1NVoiLCJ0cmVlU2l6ZSI6MX0=",
54
+ "payload": "eyJsb2ciOiJ2ZWN0b3JzLWxvZyIsInJvb3RIYXNoIjoiODViYjZhMjk1YzYxYmIzMzg5MWYyNTFiNWNiZGQ4NzNjNGVkYTU5OTBkMWE4MzVjNDRjYjZiMDgwNTBhY2U2MiIsInRpbWVzdGFtcCI6IjIwMjYtMDktMDhUMDY6MzA6MDQuNjkzWiIsInRyZWVTaXplIjoxfQ==",
55
55
  "signatures": [
56
56
  {
57
- "keyid": "c150ba37105c4e66f85239796e4f07b97c0fc431a9c1118d2bd1554ba35dab54",
58
- "sig": "Dy7+sDttHIIQCUzGlIL7Yro+UFGLSPjnLuBJYxFdq2wiqkkAdA3ipo09aSoS0h5BwJMOOEqOTmrGb/ot7HZTCQ=="
57
+ "keyid": "74356bcffe957c1c3a3bde6702505f536d4e8bab2aac3b0f36419e846da1a8d5",
58
+ "sig": "xhkG/l7l3ElnvGnxrFfK2nYUCkRo0ktj0+6ctv6V1qKiXihMYexcOu/qepdqWWN0mWFq2u8/Aqo6kbdoWhiFDQ=="
59
59
  }
60
60
  ]
61
61
  },
62
62
  "proof": [
63
- "32a570791cc4546f2b7cf560a2fe3459eb504e0cae52f0ce521fea311f9f15ba"
63
+ "f8e988293efa93dd5e5ea2af96b88588f4a1fba5c786848e997aa985c95e2f81"
64
64
  ],
65
65
  "keys": [
66
66
  "log"
@@ -77,26 +77,26 @@
77
77
  "description": "Tree heads checked against the app key, which did not sign them.",
78
78
  "older": {
79
79
  "payloadType": "application/vnd.agent-custody.treehead+json",
80
- "payload": "eyJyb290SGFzaCI6IjY5NTU0NGFkNTA4YTEzYTk4NTUxM2YzYjJhMjU0MzhlZjk1MWIyMzk2MGQ1ODU0YjczZjFjYWRkMzJmOTI1YmQiLCJ0aW1lc3RhbXAiOiIyMDI2LTA5LTA4VDAxOjUwOjE1Ljc1NVoiLCJ0cmVlU2l6ZSI6MX0=",
80
+ "payload": "eyJsb2ciOiJ2ZWN0b3JzLWxvZyIsInJvb3RIYXNoIjoiODViYjZhMjk1YzYxYmIzMzg5MWYyNTFiNWNiZGQ4NzNjNGVkYTU5OTBkMWE4MzVjNDRjYjZiMDgwNTBhY2U2MiIsInRpbWVzdGFtcCI6IjIwMjYtMDktMDhUMDY6MzA6MDQuNjkzWiIsInRyZWVTaXplIjoxfQ==",
81
81
  "signatures": [
82
82
  {
83
- "keyid": "c150ba37105c4e66f85239796e4f07b97c0fc431a9c1118d2bd1554ba35dab54",
84
- "sig": "Dy7+sDttHIIQCUzGlIL7Yro+UFGLSPjnLuBJYxFdq2wiqkkAdA3ipo09aSoS0h5BwJMOOEqOTmrGb/ot7HZTCQ=="
83
+ "keyid": "74356bcffe957c1c3a3bde6702505f536d4e8bab2aac3b0f36419e846da1a8d5",
84
+ "sig": "xhkG/l7l3ElnvGnxrFfK2nYUCkRo0ktj0+6ctv6V1qKiXihMYexcOu/qepdqWWN0mWFq2u8/Aqo6kbdoWhiFDQ=="
85
85
  }
86
86
  ]
87
87
  },
88
88
  "newer": {
89
89
  "payloadType": "application/vnd.agent-custody.treehead+json",
90
- "payload": "eyJyb290SGFzaCI6IjU0YzBkNDY1NDhjYzdlYzcyMTQ1MDNlMzFjNzhhYTY5MDZhMmRkY2VhZTI3OTI0N2M5NzRjMjljYmY3Y2Q4NGMiLCJ0aW1lc3RhbXAiOiIyMDI2LTA5LTA4VDAxOjUwOjE1Ljc1OFoiLCJ0cmVlU2l6ZSI6Mn0=",
90
+ "payload": "eyJsb2ciOiJ2ZWN0b3JzLWxvZyIsInJvb3RIYXNoIjoiODY0N2E0MTczNTIxYzE5Mzg4NTg2Y2U1MzYyNGU3MGZiYWEyMzc0NGRiYjIzNTQ0ZDU2N2Q5NDY0NmE0ZTM3MiIsInRpbWVzdGFtcCI6IjIwMjYtMDktMDhUMDY6MzA6MDQuNjk3WiIsInRyZWVTaXplIjoyfQ==",
91
91
  "signatures": [
92
92
  {
93
- "keyid": "c150ba37105c4e66f85239796e4f07b97c0fc431a9c1118d2bd1554ba35dab54",
94
- "sig": "4belmD02Vv4dufyaCY2N8J48+bxQmMtMfg+jNyD3wadZftcYgQPkZJ2mUx7gCfoh5lOwEu+Wzd5Id1yZlHYvAQ=="
93
+ "keyid": "74356bcffe957c1c3a3bde6702505f536d4e8bab2aac3b0f36419e846da1a8d5",
94
+ "sig": "iaweKtCddKNA0R+dkq+0qyzwe8RhZdKgK6y9o21adUCjhMAGlpl+YDe1chzn4NvT7jIZvn+HDdll136TmqcmCQ=="
95
95
  }
96
96
  ]
97
97
  },
98
98
  "proof": [
99
- "32a570791cc4546f2b7cf560a2fe3459eb504e0cae52f0ce521fea311f9f15ba"
99
+ "f8e988293efa93dd5e5ea2af96b88588f4a1fba5c786848e997aa985c95e2f81"
100
100
  ],
101
101
  "keys": [
102
102
  "app"
@@ -114,21 +114,21 @@
114
114
  "description": "A proof with a hash removed.",
115
115
  "older": {
116
116
  "payloadType": "application/vnd.agent-custody.treehead+json",
117
- "payload": "eyJyb290SGFzaCI6IjY5NTU0NGFkNTA4YTEzYTk4NTUxM2YzYjJhMjU0MzhlZjk1MWIyMzk2MGQ1ODU0YjczZjFjYWRkMzJmOTI1YmQiLCJ0aW1lc3RhbXAiOiIyMDI2LTA5LTA4VDAxOjUwOjE1Ljc1NVoiLCJ0cmVlU2l6ZSI6MX0=",
117
+ "payload": "eyJsb2ciOiJ2ZWN0b3JzLWxvZyIsInJvb3RIYXNoIjoiODViYjZhMjk1YzYxYmIzMzg5MWYyNTFiNWNiZGQ4NzNjNGVkYTU5OTBkMWE4MzVjNDRjYjZiMDgwNTBhY2U2MiIsInRpbWVzdGFtcCI6IjIwMjYtMDktMDhUMDY6MzA6MDQuNjkzWiIsInRyZWVTaXplIjoxfQ==",
118
118
  "signatures": [
119
119
  {
120
- "keyid": "c150ba37105c4e66f85239796e4f07b97c0fc431a9c1118d2bd1554ba35dab54",
121
- "sig": "Dy7+sDttHIIQCUzGlIL7Yro+UFGLSPjnLuBJYxFdq2wiqkkAdA3ipo09aSoS0h5BwJMOOEqOTmrGb/ot7HZTCQ=="
120
+ "keyid": "74356bcffe957c1c3a3bde6702505f536d4e8bab2aac3b0f36419e846da1a8d5",
121
+ "sig": "xhkG/l7l3ElnvGnxrFfK2nYUCkRo0ktj0+6ctv6V1qKiXihMYexcOu/qepdqWWN0mWFq2u8/Aqo6kbdoWhiFDQ=="
122
122
  }
123
123
  ]
124
124
  },
125
125
  "newer": {
126
126
  "payloadType": "application/vnd.agent-custody.treehead+json",
127
- "payload": "eyJyb290SGFzaCI6IjU0YzBkNDY1NDhjYzdlYzcyMTQ1MDNlMzFjNzhhYTY5MDZhMmRkY2VhZTI3OTI0N2M5NzRjMjljYmY3Y2Q4NGMiLCJ0aW1lc3RhbXAiOiIyMDI2LTA5LTA4VDAxOjUwOjE1Ljc1OFoiLCJ0cmVlU2l6ZSI6Mn0=",
127
+ "payload": "eyJsb2ciOiJ2ZWN0b3JzLWxvZyIsInJvb3RIYXNoIjoiODY0N2E0MTczNTIxYzE5Mzg4NTg2Y2U1MzYyNGU3MGZiYWEyMzc0NGRiYjIzNTQ0ZDU2N2Q5NDY0NmE0ZTM3MiIsInRpbWVzdGFtcCI6IjIwMjYtMDktMDhUMDY6MzA6MDQuNjk3WiIsInRyZWVTaXplIjoyfQ==",
128
128
  "signatures": [
129
129
  {
130
- "keyid": "c150ba37105c4e66f85239796e4f07b97c0fc431a9c1118d2bd1554ba35dab54",
131
- "sig": "4belmD02Vv4dufyaCY2N8J48+bxQmMtMfg+jNyD3wadZftcYgQPkZJ2mUx7gCfoh5lOwEu+Wzd5Id1yZlHYvAQ=="
130
+ "keyid": "74356bcffe957c1c3a3bde6702505f536d4e8bab2aac3b0f36419e846da1a8d5",
131
+ "sig": "iaweKtCddKNA0R+dkq+0qyzwe8RhZdKgK6y9o21adUCjhMAGlpl+YDe1chzn4NvT7jIZvn+HDdll136TmqcmCQ=="
132
132
  }
133
133
  ]
134
134
  },
@@ -53,18 +53,18 @@
53
53
  }
54
54
  ],
55
55
  "keyid": {
56
- "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEABAiIObrUZgS8uRY1zr5NCUnkr2HqN3K/b+P75pkPX1I=\n-----END PUBLIC KEY-----\n",
57
- "keyid": "f660c2bc47886be244642e60bbe1235946e88b8ca864c1c55ef7a8c7a424e184"
56
+ "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA0ZCk/vXbGqi5xjdFAok3XGPWbCT9yO2OeGbMF1nvH5w=\n-----END PUBLIC KEY-----\n",
57
+ "keyid": "3df0af0ec3abda622a30f7a3273fc6ca4eda229404122f02e2cae12923a861ad"
58
58
  },
59
59
  "dsse": {
60
- "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEABAiIObrUZgS8uRY1zr5NCUnkr2HqN3K/b+P75pkPX1I=\n-----END PUBLIC KEY-----\n",
60
+ "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA0ZCk/vXbGqi5xjdFAok3XGPWbCT9yO2OeGbMF1nvH5w=\n-----END PUBLIC KEY-----\n",
61
61
  "envelope": {
62
62
  "payloadType": "application/vnd.example+json",
63
63
  "payload": "eyJoZWxsbyI6IndvcmxkIn0=",
64
64
  "signatures": [
65
65
  {
66
- "keyid": "f660c2bc47886be244642e60bbe1235946e88b8ca864c1c55ef7a8c7a424e184",
67
- "sig": "DY3PDf89eYVsOJeQJYxT4W2c4NsNeWup+P+Oi/yvp9Fr34uSct3SfueWkmdaAtrmjrenoAdNt8BWV0N+6f0JDA=="
66
+ "keyid": "3df0af0ec3abda622a30f7a3273fc6ca4eda229404122f02e2cae12923a861ad",
67
+ "sig": "aGI6lAPRJs17z+UUezv/7iGyZ1rHbjKRWBELgeC4YjLGytQLUZsH+2jwFKhmTasJIyssry1ob+HMt4V20WCzBQ=="
68
68
  }
69
69
  ]
70
70
  },