@agent-custody/receipts 0.1.4 → 0.1.5

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
@@ -231,6 +231,7 @@ src/sdk/index.ts the interceptor: policy decision, record, wrap(tool fn)
231
231
  src/sdk/claude.ts Claude Code command hook and Claude Agent SDK in-process hooks
232
232
  src/sdk/openai-agents.ts, vercel-ai.ts, langchain.ts framework adapters, tested against the real packages
233
233
  src/sidecar.ts the SDK issuer behind a local HTTP API, for agents in other languages
234
+ src/upstream.ts attested execution: an upstream signs its result for the receipt; the verifier checks it with the upstream key
234
235
  vectors/ conformance vectors: receipts, keys, logs, proofs, and expected verdicts; `bun run vectors` regenerates them
235
236
  src/verify.ts offline verification, the human-readable report, and the audit that a later log extends an earlier one
236
237
  src/cli.ts keygen, grant, gateway, hook, serve, log, verify, audit
@@ -254,6 +255,10 @@ The design is two producers feeding one verifier. The SDK is the top of the funn
254
255
  - SDK core: policy decision, record, and a generic `wrap(tool, fn)` for any framework whose tools are functions.
255
256
  - Claude Code command hook for PreToolUse, PostToolUse, and PostToolUseFailure, with blocking on deny.
256
257
  - Claude Agent SDK in-process hooks over the same handler.
258
+ - Attested execution: an upstream that holds a key signs its result for the receipt, the gateway embeds it, and a verifier given the upstream key reports the execution as attested rather than observed. The memory server and the demo upstream sign.
259
+ - HTTP upstreams: the gateway reaches an already-running MCP server over Streamable HTTP with a bearer token from the environment, as well as spawning one over stdio.
260
+ - Optional fact lookups: a lookup that references a call argument the call does not carry is skipped rather than denying, so policy can see the fact a write is about to supersede without refusing writes that supersede nothing.
261
+ - Consumed facts: an upstream declares the facts it served in its result `_meta`, and every later gateway receipt in the session carries those ids as `consumed`, observed, so what the agent had been shown before each call is on the record.
257
262
  - The forwarded call carries the receipt id and the attested agent and principal in `_meta`, so a stateful upstream can cite the receipt; the memory server in `@agent-custody/state` runs this way.
258
263
  - Conformance vectors, generated by the test suite and published with the spec, and a browser verifier on agent-custody.dev that passes all of them.
259
264
  - Sidecar: the SDK issuer behind a local HTTP API (`serve`), with a Python package on PyPI-ready footing and Go, Java, and Rust clients, so agents in any language get the same receipts from one signing implementation.
@@ -264,7 +269,7 @@ The design is two producers feeding one verifier. The SDK is the top of the funn
264
269
  **Next, in the order it pays off**
265
270
 
266
271
  1. OpenTelemetry export: emit each receipt as a span with the receipt id and issuer kind as attributes, so existing collectors and dashboards carry them without a new pipeline.
267
- 2. Embed upstream signed responses (Stripe webhook signatures, GitHub delivery signatures) so gateway execution can move from `observed` to `attested`.
272
+ 2. Provider-native upstream signatures (Stripe webhook signatures, GitHub delivery signatures) as adapters onto the upstream attestation field.
268
273
  3. An HTTP transport for the gateway, with the grant presented per connection, for a shared deployment rather than one process per agent session.
269
274
  4. Delegation chains for sub-agents.
270
275
  5. Receiver-attested receipts for agent-to-agent calls.
package/dist/cli.js CHANGED
@@ -19,7 +19,7 @@ const USAGE = `agent-custody <command>
19
19
  hook [--config <sdk.json>] Claude Code hook command; reads the event on stdin (or AGENT_CUSTODY_CONFIG)
20
20
  serve --config <sdk.json> [--port 8788] [--host 127.0.0.1] the SDK as a local HTTP API for agents in other languages
21
21
  log --file <log.jsonl> --key <log.key> [--port 8787] [--host 127.0.0.1] [--token-env <NAME>] reference log server
22
- verify <bundle.json> --issuer-key <pub> [--principal-key <pub>] [--log-key <pub>] [--log <log.jsonl>] [--json]
22
+ verify <bundle.json> --issuer-key <pub> [--principal-key <pub>] [--log-key <pub>] [--upstream-key <pub>] [--log <log.jsonl>] [--json]
23
23
  audit --older <bundle.json> --newer <bundle.json> (--log <log.jsonl> | --log-url <url>) --issuer-key <pub> [--log-key <pub>] [--json]
24
24
  checks that the newer receipt's log extends the older one's: nothing between them was rewritten
25
25
  `;
@@ -119,6 +119,7 @@ async function main(argv) {
119
119
  "gateway-key": { type: "string", multiple: true },
120
120
  "principal-key": { type: "string", multiple: true },
121
121
  "log-key": { type: "string", multiple: true },
122
+ "upstream-key": { type: "string", multiple: true },
122
123
  log: { type: "string" },
123
124
  json: { type: "boolean", default: false },
124
125
  },
@@ -132,6 +133,7 @@ async function main(argv) {
132
133
  issuerKeys: issuerKeyFiles.map(loadPublicKey),
133
134
  principalKeys: (values["principal-key"] ?? []).map(loadPublicKey),
134
135
  ...(values["log-key"] ? { logKeys: values["log-key"].map(loadPublicKey) } : {}),
136
+ ...(values["upstream-key"] ? { upstreamKeys: values["upstream-key"].map(loadPublicKey) } : {}),
135
137
  ...(values.log ? { logFile: values.log } : {}),
136
138
  });
137
139
  console.log(values.json ? JSON.stringify(result, null, 2) : formatReport(result));
package/dist/config.d.ts CHANGED
@@ -4,16 +4,20 @@ declare const FactSchema: z.ZodObject<{
4
4
  tool: z.ZodString;
5
5
  args: z.ZodRecord<z.ZodString, z.ZodString>;
6
6
  forTools: z.ZodArray<z.ZodString>;
7
+ optional: z.ZodDefault<z.ZodBoolean>;
7
8
  }, z.core.$strip>;
8
9
  export declare const GatewayConfigSchema: z.ZodObject<{
9
10
  identity: z.ZodObject<{
10
11
  keyFile: z.ZodString;
11
12
  }, z.core.$strip>;
12
- upstream: z.ZodObject<{
13
+ upstream: z.ZodUnion<readonly [z.ZodObject<{
13
14
  command: z.ZodString;
14
15
  args: z.ZodDefault<z.ZodArray<z.ZodString>>;
15
16
  env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
16
- }, z.core.$strip>;
17
+ }, z.core.$strip>, z.ZodObject<{
18
+ url: z.ZodString;
19
+ tokenEnv: z.ZodOptional<z.ZodString>;
20
+ }, z.core.$strip>]>;
17
21
  grantFile: z.ZodString;
18
22
  trustedPrincipalKeys: z.ZodArray<z.ZodString>;
19
23
  policyFile: z.ZodString;
@@ -22,6 +26,7 @@ export declare const GatewayConfigSchema: z.ZodObject<{
22
26
  tool: z.ZodString;
23
27
  args: z.ZodRecord<z.ZodString, z.ZodString>;
24
28
  forTools: z.ZodArray<z.ZodString>;
29
+ optional: z.ZodDefault<z.ZodBoolean>;
25
30
  }, z.core.$strip>>>;
26
31
  receiptsDir: z.ZodString;
27
32
  logFile: z.ZodOptional<z.ZodString>;
package/dist/config.js CHANGED
@@ -14,14 +14,16 @@ const FactSchema = z.object({
14
14
  args: z.record(z.string(), z.string()),
15
15
  /** which intercepted tools trigger this lookup */
16
16
  forTools: z.array(z.string().min(1)).min(1),
17
+ /** when true and a "$args.<key>" the template needs is absent from the call, the lookup is skipped and the fact is simply not present */
18
+ optional: z.boolean().default(false),
17
19
  });
18
20
  export const GatewayConfigSchema = z.object({
19
21
  identity: z.object({ keyFile: z.string() }),
20
- upstream: z.object({
21
- command: z.string(),
22
- args: z.array(z.string()).default([]),
23
- env: z.record(z.string(), z.string()).optional(),
24
- }),
22
+ /** the upstream MCP server: a process to spawn over stdio, or a URL to reach over Streamable HTTP with an optional bearer token from the environment */
23
+ upstream: z.union([
24
+ z.object({ command: z.string(), args: z.array(z.string()).default([]), env: z.record(z.string(), z.string()).optional() }),
25
+ z.object({ url: z.string().url(), tokenEnv: z.string().min(1).optional() }),
26
+ ]),
25
27
  grantFile: z.string(),
26
28
  trustedPrincipalKeys: z.array(z.string()).min(1),
27
29
  policyFile: z.string(),
package/dist/gateway.d.ts CHANGED
@@ -7,6 +7,8 @@ export declare const MODEL_META_KEY = "agent-custody/model";
7
7
  /** Set by the gateway on the call it forwards upstream: the receipt id, and the agent and principal from the attested grant. */
8
8
  export declare const AGENT_META_KEY = "agent-custody/agent";
9
9
  export declare const PRINCIPAL_META_KEY = "agent-custody/principal";
10
+ /** Set by an upstream on its result: the ids of the facts it served in this call. The gateway remembers them for the session. */
11
+ export declare const FACTS_META_KEY = "agent-custody/facts";
10
12
  export interface CallParams {
11
13
  name: string;
12
14
  arguments?: Record<string, unknown>;
package/dist/gateway.js CHANGED
@@ -4,6 +4,7 @@ import { randomUUID } from "node:crypto";
4
4
  import { readFileSync } from "node:fs";
5
5
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
6
6
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
7
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
7
8
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
8
9
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
10
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
@@ -11,6 +12,7 @@ import { digestOf, loadPrivateKey, loadPublicKey } from "./crypto.js";
11
12
  import { delegationValidAt, verifyDelegation } from "./delegation.js";
12
13
  import { createIssuer } from "./issue.js";
13
14
  import { openLog } from "./log-sink.js";
15
+ import { upstreamSignatureOf } from "./upstream.js";
14
16
  import { evaluate, policyDigest } from "./policy.js";
15
17
  export const GATEWAY_VERSION = "0.1.0";
16
18
  export const RECEIPT_META_KEY = "agent-custody/receipt";
@@ -18,13 +20,19 @@ export const MODEL_META_KEY = "agent-custody/model";
18
20
  /** Set by the gateway on the call it forwards upstream: the receipt id, and the agent and principal from the attested grant. */
19
21
  export const AGENT_META_KEY = "agent-custody/agent";
20
22
  export const PRINCIPAL_META_KEY = "agent-custody/principal";
21
- function resolveFactArgs(template, args) {
23
+ /** Set by an upstream on its result: the ids of the facts it served in this call. The gateway remembers them for the session. */
24
+ export const FACTS_META_KEY = "agent-custody/facts";
25
+ /** Returns null when an optional lookup references a call argument that is absent. */
26
+ function resolveFactArgs(template, args, optional = false) {
22
27
  const out = {};
23
28
  for (const [k, v] of Object.entries(template)) {
24
29
  if (v.startsWith("$args.")) {
25
30
  const key = v.slice("$args.".length);
26
- if (!(key in args))
31
+ if (!(key in args) || args[key] === undefined) {
32
+ if (optional)
33
+ return null;
27
34
  throw new Error(`fact argument "${k}" needs call argument "${key}", which is missing`);
35
+ }
28
36
  out[k] = args[key];
29
37
  }
30
38
  else {
@@ -60,25 +68,49 @@ export async function createGateway(cfg) {
60
68
  const pDigest = policyDigest(policyText);
61
69
  const issuer = createIssuer(gatewayKey, cfg.receiptsDir, openLog(cfg, gatewayKey));
62
70
  const upstream = new Client({ name: "agent-custody-gateway", version: GATEWAY_VERSION });
63
- await upstream.connect(new StdioClientTransport({ command: cfg.upstream.command, args: cfg.upstream.args, env: cfg.upstream.env, stderr: "inherit" }));
71
+ if ("url" in cfg.upstream) {
72
+ const token = cfg.upstream.tokenEnv ? process.env[cfg.upstream.tokenEnv] : undefined;
73
+ if (cfg.upstream.tokenEnv && !token)
74
+ throw new Error(`upstream token: environment variable ${cfg.upstream.tokenEnv} is not set`);
75
+ await upstream.connect(new StreamableHTTPClientTransport(new URL(cfg.upstream.url), token ? { requestInit: { headers: { authorization: `Bearer ${token}` } } } : {}));
76
+ }
77
+ else {
78
+ await upstream.connect(new StdioClientTransport({ command: cfg.upstream.command, args: cfg.upstream.args, env: cfg.upstream.env, stderr: "inherit" }));
79
+ }
64
80
  const callUpstream = async (name, args, meta) => (await upstream.callTool({ name, arguments: args, ...(meta ? { _meta: meta } : {}) }));
65
- async function gatherFacts(tool, args) {
81
+ async function gatherFacts(tool, args, meta) {
66
82
  const facts = {};
67
83
  for (const f of cfg.facts.filter((f) => f.forTools.includes(tool))) {
68
- const fargs = resolveFactArgs(f.args, args);
69
- const result = await callUpstream(f.tool, fargs);
84
+ const fargs = resolveFactArgs(f.args, args, f.optional);
85
+ if (fargs === null)
86
+ continue;
87
+ // Lookups carry the same metadata as the forwarded call: they are the gateway acting for this receipt.
88
+ const result = await callUpstream(f.tool, fargs, meta);
70
89
  if (result.isError)
71
90
  throw new Error(`fact "${f.name}" lookup via ${f.tool} failed: ${JSON.stringify(extractValue(result))}`);
72
91
  facts[f.name] = { tool: f.tool, args: fargs, value: extractValue(result), resultDigest: digestOf(result), provenance: "observed" };
73
92
  }
74
93
  return facts;
75
94
  }
95
+ /** Every fact id an upstream has declared it served, in order of first sight. One gateway process is one agent session. */
96
+ const consumed = [];
97
+ const noteServedFacts = (result) => {
98
+ const ids = result._meta?.[FACTS_META_KEY];
99
+ if (!Array.isArray(ids))
100
+ return;
101
+ for (const id of ids)
102
+ if (typeof id === "string" && !consumed.includes(id))
103
+ consumed.push(id);
104
+ };
76
105
  async function handleCall(params) {
77
106
  const tool = params.name;
78
107
  const args = params.arguments ?? {};
79
108
  const receiptId = randomUUID();
80
109
  const timestamp = new Date().toISOString();
81
110
  const modelClaim = params._meta?.[MODEL_META_KEY];
111
+ // What the agent had been shown before this call; recorded before this call's own result is seen.
112
+ const consumedNow = [...consumed];
113
+ const upstreamMeta = { [RECEIPT_META_KEY]: receiptId, [AGENT_META_KEY]: delegation.agent, [PRINCIPAL_META_KEY]: delegation.principal };
82
114
  let facts = {};
83
115
  let policy;
84
116
  let execution;
@@ -87,7 +119,7 @@ export async function createGateway(cfg) {
87
119
  }
88
120
  else {
89
121
  try {
90
- facts = await gatherFacts(tool, args);
122
+ facts = await gatherFacts(tool, args, upstreamMeta);
91
123
  const factValues = Object.fromEntries(Object.entries(facts).map(([k, f]) => [k, f.value]));
92
124
  policy = evaluate(policyText, {
93
125
  agentId: delegation.agent,
@@ -103,8 +135,10 @@ export async function createGateway(cfg) {
103
135
  try {
104
136
  // The upstream learns which receipt this call is, and who the grant says is calling. An upstream that keeps
105
137
  // state, such as the memory server, cites the receipt as the source of what it stores.
106
- const result = await callUpstream(tool, args, { [RECEIPT_META_KEY]: receiptId, [AGENT_META_KEY]: delegation.agent, [PRINCIPAL_META_KEY]: delegation.principal });
107
- execution = { status: result.isError ? "failed" : "executed", result, resultDigest: digestOf(result), provenance: "observed" };
138
+ const result = await callUpstream(tool, args, upstreamMeta);
139
+ const upstreamSig = upstreamSignatureOf(result);
140
+ execution = { status: result.isError ? "failed" : "executed", result, resultDigest: digestOf(result), provenance: "observed", ...(upstreamSig ? { upstream: { envelope: upstreamSig } } : {}) };
141
+ noteServedFacts(result);
108
142
  }
109
143
  catch (e) {
110
144
  execution = { status: "error", error: String(e instanceof Error ? e.message : e), provenance: "observed" };
@@ -125,6 +159,7 @@ export async function createGateway(cfg) {
125
159
  tool: { name: tool, provenance: "observed" },
126
160
  request: { args, argsDigest: digestOf(args), provenance: "claimed" },
127
161
  facts,
162
+ consumed: { factIds: consumedNow, provenance: "observed" },
128
163
  policy: { ...policy, provenance: "observed" },
129
164
  execution,
130
165
  });
package/dist/index.d.ts CHANGED
@@ -10,3 +10,4 @@ export * from "./receipt.ts";
10
10
  export * from "./verify.ts";
11
11
  export * from "./sdk/index.ts";
12
12
  export * from "./sidecar.ts";
13
+ export * from "./upstream.ts";
package/dist/index.js CHANGED
@@ -11,3 +11,4 @@ export * from "./receipt.js";
11
11
  export * from "./verify.js";
12
12
  export * from "./sdk/index.js";
13
13
  export * from "./sidecar.js";
14
+ export * from "./upstream.js";
package/dist/receipt.d.ts CHANGED
@@ -70,6 +70,15 @@ export interface ReceiptPredicate {
70
70
  provenance: "claimed";
71
71
  };
72
72
  facts: Record<string, FactRecord>;
73
+ /**
74
+ * Fact ids the agent had been shown, through this gateway, by the time of this call: every id an upstream declared
75
+ * in its result _meta under "agent-custody/facts" on an earlier call in the session. Observed, because the gateway
76
+ * saw those results itself. Absent on SDK receipts. This is what the agent relied on, as an upper bound.
77
+ */
78
+ consumed?: {
79
+ factIds: string[];
80
+ provenance: "observed";
81
+ };
73
82
  /** null when the issuer evaluated no policy. */
74
83
  policy: (PolicyDecision & {
75
84
  provenance: Provenance;
@@ -79,6 +88,10 @@ export interface ReceiptPredicate {
79
88
  result: unknown;
80
89
  resultDigest: string;
81
90
  provenance: Provenance;
91
+ /** an upstream's own signature over what it returned, bound to this receipt; checked by a verifier holding the upstream's key */
92
+ upstream?: {
93
+ envelope: Envelope;
94
+ };
82
95
  } | {
83
96
  status: "denied";
84
97
  reason: string;
@@ -0,0 +1,27 @@
1
+ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2
+ import { type Envelope, type KeyPair, type PublicKeyRef } from "./crypto.ts";
3
+ export declare const UPSTREAM_SIG_META_KEY = "agent-custody/upstream-signature";
4
+ export declare const UPSTREAM_TYPE = "application/vnd.agent-custody.upstream+json";
5
+ export interface UpstreamAttestation {
6
+ receiptId: string;
7
+ tool: string;
8
+ /** digest of { content, isError } of the result: what the agent received, without the _meta the signature lives in */
9
+ contentDigest: string;
10
+ }
11
+ export declare function contentDigest(result: {
12
+ content: unknown;
13
+ isError?: boolean | undefined;
14
+ }): string;
15
+ /** For upstreams: signs the result for this receipt and returns it with the envelope attached. */
16
+ export declare function signResult<R extends CallToolResult>(result: R, key: KeyPair, receiptId: string, tool: string): R;
17
+ /** For the gateway: the envelope an upstream attached, if any. */
18
+ export declare function upstreamSignatureOf(result: CallToolResult): Envelope | null;
19
+ export type UpstreamCheck = {
20
+ ok: true;
21
+ keyid: string;
22
+ } | {
23
+ ok: false;
24
+ error: string;
25
+ };
26
+ /** For verifiers: the envelope must verify against a trusted upstream key and bind to this receipt, tool, and content. */
27
+ export declare function checkUpstream(envelope: Envelope, keys: PublicKeyRef[], expected: UpstreamAttestation): UpstreamCheck;
@@ -0,0 +1,32 @@
1
+ import { digestOf, dsseSign, dsseVerify } from "./crypto.js";
2
+ export const UPSTREAM_SIG_META_KEY = "agent-custody/upstream-signature";
3
+ export const UPSTREAM_TYPE = "application/vnd.agent-custody.upstream+json";
4
+ export function contentDigest(result) {
5
+ return digestOf({ content: result.content, isError: !!result.isError });
6
+ }
7
+ /** For upstreams: signs the result for this receipt and returns it with the envelope attached. */
8
+ export function signResult(result, key, receiptId, tool) {
9
+ const payload = { receiptId, tool, contentDigest: contentDigest(result) };
10
+ return { ...result, _meta: { ...result._meta, [UPSTREAM_SIG_META_KEY]: dsseSign(UPSTREAM_TYPE, payload, key) } };
11
+ }
12
+ /** For the gateway: the envelope an upstream attached, if any. */
13
+ export function upstreamSignatureOf(result) {
14
+ const env = result._meta?.[UPSTREAM_SIG_META_KEY];
15
+ return env && typeof env.payload === "string" && Array.isArray(env.signatures) ? env : null;
16
+ }
17
+ /** For verifiers: the envelope must verify against a trusted upstream key and bind to this receipt, tool, and content. */
18
+ export function checkUpstream(envelope, keys, expected) {
19
+ const v = dsseVerify(envelope, keys);
20
+ if (!v.ok)
21
+ return { ok: false, error: v.error };
22
+ if (envelope.payloadType !== UPSTREAM_TYPE)
23
+ return { ok: false, error: `payload type ${envelope.payloadType}` };
24
+ const p = v.payload;
25
+ if (p.receiptId !== expected.receiptId)
26
+ return { ok: false, error: "signed for a different receipt" };
27
+ if (p.tool !== expected.tool)
28
+ return { ok: false, error: `signed for tool ${String(p.tool)}` };
29
+ if (p.contentDigest !== expected.contentDigest)
30
+ return { ok: false, error: "signed content differs from the result in the receipt" };
31
+ return { ok: true, keyid: v.keyid };
32
+ }
package/dist/verify.d.ts CHANGED
@@ -11,6 +11,8 @@ export interface VerifyOptions {
11
11
  principalKeys: PublicKeyRef[];
12
12
  /** keys of logs run by someone other than the issuer; tree heads are checked against these and the issuer keys */
13
13
  logKeys?: PublicKeyRef[];
14
+ /** keys of upstreams that sign their results; with one given, an execution carrying an upstream signature is checked and becomes attested */
15
+ upstreamKeys?: PublicKeyRef[];
14
16
  /** If given, the root is recomputed from this log file at the receipt's tree size and compared. */
15
17
  logFile?: string;
16
18
  }
package/dist/verify.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { canonicalize, digestOf, dsseVerify } from "./crypto.js";
3
3
  import { delegationValidAt, verifyDelegation } from "./delegation.js";
4
4
  import { leafHash, MerkleLog, verifyConsistency, verifyInclusion } from "./log.js";
5
+ import { checkUpstream, contentDigest } from "./upstream.js";
5
6
  import { RECEIPT_PREDICATE_TYPE, RECEIPT_TYPE, TREEHEAD_TYPE } from "./receipt.js";
6
7
  const short = (s) => s.slice(0, 12);
7
8
  export function verifyBundle(bundle, opts) {
@@ -44,6 +45,11 @@ export function verifyBundle(bundle, opts) {
44
45
  add("principal is claimed, not attested", p.principal.provenance === "claimed", "no signed delegation in this receipt");
45
46
  }
46
47
  add("request args digest", digestOf(p.request.args) === p.request.argsDigest && st.subject[0]?.digest.sha256 === p.request.argsDigest);
48
+ if ((p.execution.status === "executed" || p.execution.status === "failed") && p.execution.upstream && (opts.upstreamKeys?.length ?? 0) > 0) {
49
+ const result = p.execution.result;
50
+ const u = checkUpstream(p.execution.upstream.envelope, opts.upstreamKeys, { receiptId: p.receiptId, tool: p.tool.name, contentDigest: contentDigest(result) });
51
+ add("upstream signature (upstream key)", u.ok, u.ok ? `keyid ${short(u.keyid)}` : u.error);
52
+ }
47
53
  if (p.policy) {
48
54
  const consistent = p.policy.decision === "allow" ? p.execution.status !== "denied" : p.execution.status === "denied";
49
55
  add("policy decision consistent with execution", consistent, `${p.policy.decision} -> ${p.execution.status}`);
@@ -129,6 +135,10 @@ export function formatReport(r) {
129
135
  row("policy", p.policy.provenance, `${p.policy.decision} [${p.policy.reasons.join(",")}] policy ${short(p.policy.policyDigest)}`);
130
136
  else
131
137
  row("policy", "-", "(none evaluated)");
132
- row("execution", p.execution.provenance, p.execution.status);
138
+ if (p.consumed)
139
+ row("consumed", p.consumed.provenance, p.consumed.factIds.length === 0 ? "(no facts shown before this call)" : p.consumed.factIds);
140
+ const upstreamCheck = r.checks.find((c) => c.name === "upstream signature (upstream key)");
141
+ const hasUpstream = (p.execution.status === "executed" || p.execution.status === "failed") && !!p.execution.upstream;
142
+ row("execution", upstreamCheck?.ok ? "attested" : p.execution.provenance, `${p.execution.status}${hasUpstream ? (upstreamCheck ? (upstreamCheck.ok ? ` (signed by upstream ${upstreamCheck.detail})` : " (upstream signature FAILED)") : " (carries an upstream signature; pass --upstream-key to check it)") : ""}`);
133
143
  return lines.join("\n");
134
144
  }
package/docs/usage.md CHANGED
@@ -77,7 +77,7 @@ when {
77
77
  }
78
78
  ```
79
79
 
80
- `upstream` is spawned by the gateway exactly as an MCP host would spawn it. `env` is passed through, which is where upstream credentials go. The agent never sees them.
80
+ `upstream` is spawned by the gateway exactly as an MCP host would spawn it. `env` is passed through, which is where upstream credentials go. The agent never sees them. An upstream that is already running is reached instead with `"upstream": { "url": "https://memory.internal/mcp", "tokenEnv": "MEMORY_TOKEN" }`, over Streamable HTTP with a bearer token from the environment; the shared memory server in `@agent-custody/state` is the usual case.
81
81
 
82
82
  `logFile` is the local Merkle log, with tree heads signed by the gateway's own key. To log to a server the operator does not control, replace it with `log`:
83
83
 
@@ -87,7 +87,7 @@ when {
87
87
 
88
88
  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 executed call the upstream action has already happened by then, which is the honest outcome, since a receipt that was never logged must not be handed out. 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.
89
89
 
90
- `facts` tells the gateway which upstream tool to call before evaluating policy for a given tool. `$args.<key>` copies a value from the intercepted call. The result appears in Cedar as `context.facts.<name>` and in the receipt with its own digest, labelled `observed`. If a fact lookup fails, the call is denied and the receipt says why.
90
+ `facts` tells the gateway which upstream tool to call before evaluating policy for a given tool. `$args.<key>` copies a value from the intercepted call. The result appears in Cedar as `context.facts.<name>` and in the receipt with its own digest, labelled `observed`. If a fact lookup fails, the call is denied and the receipt says why. A lookup with `"optional": true` is skipped when a `$args.<key>` it needs is absent from the call, and the fact is then simply not present, which a policy tests with `context.facts has <name>`; this is how a policy sees the fact a `memory.write` is about to supersede without denying every write that supersedes nothing.
91
91
 
92
92
  **5. Run the gateway.** It speaks MCP on stdin/stdout and logs to stderr only.
93
93
 
@@ -192,7 +192,9 @@ The receipt id is the file name under `receiptsDir`.
192
192
 
193
193
  ## What the upstream gets
194
194
 
195
- The call the gateway forwards carries three `_meta` keys the agent cannot set: `agent-custody/receipt`, the id of the receipt being issued for this call; `agent-custody/agent` and `agent-custody/principal`, from the signed delegation grant. An upstream that keeps state can cite the receipt as the source of what it stores and record the attested caller rather than a claimed one. The memory server in `@agent-custody/state` does exactly that. Fact lookups do not carry them; only the forwarded call does.
195
+ The call the gateway forwards carries three `_meta` keys the agent cannot set: `agent-custody/receipt`, the id of the receipt being issued for this call; `agent-custody/agent` and `agent-custody/principal`, from the signed delegation grant. An upstream that keeps state can cite the receipt as the source of what it stores and record the attested caller rather than a claimed one. The memory server in `@agent-custody/state` does exactly that. Fact lookups carry the same keys, since they are the gateway acting for the same receipt.
196
+
197
+ The upstream can answer in kind. A result whose `_meta` carries `agent-custody/facts`, an array of fact ids, tells the gateway which facts it just served; the gateway remembers them for the rest of the session and every later receipt carries them as `consumed`, labelled `observed` because the gateway saw those results itself. That is what the agent had been shown by the time of each call, an upper bound on what it relied on, and it is what the state package's blast-radius query walks.
196
198
 
197
199
  ## Operational notes
198
200
 
@@ -17,6 +17,7 @@ node src/cli.ts verify receipts/<id>.json \
17
17
  --issuer-key keys/gateway.pub \
18
18
  --principal-key keys/principal.pub \
19
19
  --log-key keys/log.pub \ # only for receipts logged to a remote log
20
+ --upstream-key keys/upstream.pub \ # only when the upstream signed its result
20
21
  --log log.jsonl # optional
21
22
  ```
22
23
 
@@ -114,6 +115,7 @@ const result = verifyBundle(bundle, {
114
115
  issuerKeys: [loadPublicKey("keys/gateway.pub")], // gateway keys and SDK application keys
115
116
  principalKeys: [loadPublicKey("keys/principal.pub")],
116
117
  logKeys: [loadPublicKey("keys/log.pub")], // only for receipts logged to a remote log
118
+ upstreamKeys: [loadPublicKey("keys/upstream.pub")], // only when the upstream signed its result
117
119
  logFile: "log.jsonl", // optional
118
120
  });
119
121
 
@@ -165,3 +167,9 @@ The remote log also serves `GET /head`, its current tree head signed with the lo
165
167
 
166
168
  `vectors/` in the package holds fixed receipts, keys, logs, proofs, and the verdicts this verifier produces for them, generated by `bun run vectors` and checked by the test suite on every run. A verifier written elsewhere proves it agrees by reproducing every verdict. They are published at [agent-custody.dev/receipt/vectors](https://agent-custody.dev/receipt/vectors).
167
169
 
170
+ ## Attested execution
171
+
172
+ On a gateway receipt the execution is `observed`: the gateway saw what the upstream returned. An upstream that holds a key can do better and sign its result for the receipt being issued; the gateway embeds the signature, and a verifier given the upstream's public key checks it and reports the execution as `attested` by that key. The check binds the signature to the receipt id, the tool, and the digest of the result content, so a signature cannot be moved between receipts.
173
+
174
+ For upstream authors, `signResult(result, key, receiptId, tool)` from `@agent-custody/receipts` does the signing; the receipt id arrives in the call's `_meta["agent-custody/receipt"]`. The memory server in `@agent-custody/state` signs when started with `--key`, and the demo's fake upstream does too. Provider-native signatures, such as Stripe's webhook signatures, are adapters on top of the same field and are not implemented yet.
175
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-custody/receipts",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
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": "eyJyb290SGFzaCI6Ijg3MzM5M2E4ZTQ4ZmFkNzVlMmRkMzY1MzRjNmRjNzUzMTc5YmM0NmEzMTAxNDZjZTI2OGUxNTc2ODZiOGMwZTQiLCJ0aW1lc3RhbXAiOiIyMDI2LTA5LTA3VDA2OjUyOjUxLjExMFoiLCJ0cmVlU2l6ZSI6MX0=",
10
+ "payload": "eyJyb290SGFzaCI6IjY4ODlmODQyNmVkODg4YTVlNzI0Y2YzNThlNmI0Y2E3ZmI3YWVmMTkwODkxOWE5MGYxZjNlNzFjMWU3MmNkODUiLCJ0aW1lc3RhbXAiOiIyMDI2LTA5LTA3VDA4OjM3OjM0LjkyOVoiLCJ0cmVlU2l6ZSI6MX0=",
11
11
  "signatures": [
12
12
  {
13
- "keyid": "e1ed29255a4bd710006fe9ad333514e71c705608b769029088a818f57bcf080a",
14
- "sig": "vydFxb4Z753KUPOXLEFiYnE0nntaoU9xXtBe7IMW8eucYeef+XpGu+4+bXpo/zYZATcx4h+7bMmDd3frqizkBg=="
13
+ "keyid": "48967bd6051d66cebf961f226a6ae4e90dce91ead9ac10c0f9aaa427d40c4c16",
14
+ "sig": "FL7TL3y1JgXinAkxywmKfoULNe2V8D+JpIcsJWok54rIun70z7bmj78R1VnNe9aADLSGQb0JTYhdaODn9mOnCg=="
15
15
  }
16
16
  ]
17
17
  },
18
18
  "newer": {
19
19
  "payloadType": "application/vnd.agent-custody.treehead+json",
20
- "payload": "eyJyb290SGFzaCI6ImNlYmViNDAzYjNlZWYzYzgwNWVjYzg5OTI2NTM0ZGM0NjkxY2I4OWJlN2I0ZjFjNjE3Njg5MDFkMzIxNDkzYmEiLCJ0aW1lc3RhbXAiOiIyMDI2LTA5LTA3VDA2OjUyOjUxLjExNFoiLCJ0cmVlU2l6ZSI6Mn0=",
20
+ "payload": "eyJyb290SGFzaCI6IjUxY2MyNzIzMzM0NzhmMjYxNDU1MWFmMWM1NjExMzBjOTBlODEwM2JhYmI1NzNkZjY2YjM3ZTVhMDk1YjI0ZWYiLCJ0aW1lc3RhbXAiOiIyMDI2LTA5LTA3VDA4OjM3OjM0LjkzM1oiLCJ0cmVlU2l6ZSI6Mn0=",
21
21
  "signatures": [
22
22
  {
23
- "keyid": "e1ed29255a4bd710006fe9ad333514e71c705608b769029088a818f57bcf080a",
24
- "sig": "iyUBqiJcZd/RaEDXWhhxl34ekqy6tQ9qgsMQjAXlvuHS75aqPLeh0NRtUQB89LS2UMJYihmcSUdg/Z82uORqCQ=="
23
+ "keyid": "48967bd6051d66cebf961f226a6ae4e90dce91ead9ac10c0f9aaa427d40c4c16",
24
+ "sig": "o/zrDH6/I3VPabSVS4RozKm945Nd+hiwt6JTytpyMavxizlR8ZuGmG6lX56+A0M4wAk/H0qmmHXoLCGuobhdCw=="
25
25
  }
26
26
  ]
27
27
  },
28
28
  "proof": [
29
- "4869289b5853ed3431c235a1f4e97d1d4205723e622dc71bd836018c62572723"
29
+ "e74d1e2874003df276b93e2e764da876201bcc1a9efe8752118676eafc4ce5eb"
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": "eyJyb290SGFzaCI6ImNlYmViNDAzYjNlZWYzYzgwNWVjYzg5OTI2NTM0ZGM0NjkxY2I4OWJlN2I0ZjFjNjE3Njg5MDFkMzIxNDkzYmEiLCJ0aW1lc3RhbXAiOiIyMDI2LTA5LTA3VDA2OjUyOjUxLjExNFoiLCJ0cmVlU2l6ZSI6Mn0=",
44
+ "payload": "eyJyb290SGFzaCI6IjUxY2MyNzIzMzM0NzhmMjYxNDU1MWFmMWM1NjExMzBjOTBlODEwM2JhYmI1NzNkZjY2YjM3ZTVhMDk1YjI0ZWYiLCJ0aW1lc3RhbXAiOiIyMDI2LTA5LTA3VDA4OjM3OjM0LjkzM1oiLCJ0cmVlU2l6ZSI6Mn0=",
45
45
  "signatures": [
46
46
  {
47
- "keyid": "e1ed29255a4bd710006fe9ad333514e71c705608b769029088a818f57bcf080a",
48
- "sig": "iyUBqiJcZd/RaEDXWhhxl34ekqy6tQ9qgsMQjAXlvuHS75aqPLeh0NRtUQB89LS2UMJYihmcSUdg/Z82uORqCQ=="
47
+ "keyid": "48967bd6051d66cebf961f226a6ae4e90dce91ead9ac10c0f9aaa427d40c4c16",
48
+ "sig": "o/zrDH6/I3VPabSVS4RozKm945Nd+hiwt6JTytpyMavxizlR8ZuGmG6lX56+A0M4wAk/H0qmmHXoLCGuobhdCw=="
49
49
  }
50
50
  ]
51
51
  },
52
52
  "newer": {
53
53
  "payloadType": "application/vnd.agent-custody.treehead+json",
54
- "payload": "eyJyb290SGFzaCI6Ijg3MzM5M2E4ZTQ4ZmFkNzVlMmRkMzY1MzRjNmRjNzUzMTc5YmM0NmEzMTAxNDZjZTI2OGUxNTc2ODZiOGMwZTQiLCJ0aW1lc3RhbXAiOiIyMDI2LTA5LTA3VDA2OjUyOjUxLjExMFoiLCJ0cmVlU2l6ZSI6MX0=",
54
+ "payload": "eyJyb290SGFzaCI6IjY4ODlmODQyNmVkODg4YTVlNzI0Y2YzNThlNmI0Y2E3ZmI3YWVmMTkwODkxOWE5MGYxZjNlNzFjMWU3MmNkODUiLCJ0aW1lc3RhbXAiOiIyMDI2LTA5LTA3VDA4OjM3OjM0LjkyOVoiLCJ0cmVlU2l6ZSI6MX0=",
55
55
  "signatures": [
56
56
  {
57
- "keyid": "e1ed29255a4bd710006fe9ad333514e71c705608b769029088a818f57bcf080a",
58
- "sig": "vydFxb4Z753KUPOXLEFiYnE0nntaoU9xXtBe7IMW8eucYeef+XpGu+4+bXpo/zYZATcx4h+7bMmDd3frqizkBg=="
57
+ "keyid": "48967bd6051d66cebf961f226a6ae4e90dce91ead9ac10c0f9aaa427d40c4c16",
58
+ "sig": "FL7TL3y1JgXinAkxywmKfoULNe2V8D+JpIcsJWok54rIun70z7bmj78R1VnNe9aADLSGQb0JTYhdaODn9mOnCg=="
59
59
  }
60
60
  ]
61
61
  },
62
62
  "proof": [
63
- "4869289b5853ed3431c235a1f4e97d1d4205723e622dc71bd836018c62572723"
63
+ "e74d1e2874003df276b93e2e764da876201bcc1a9efe8752118676eafc4ce5eb"
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": "eyJyb290SGFzaCI6Ijg3MzM5M2E4ZTQ4ZmFkNzVlMmRkMzY1MzRjNmRjNzUzMTc5YmM0NmEzMTAxNDZjZTI2OGUxNTc2ODZiOGMwZTQiLCJ0aW1lc3RhbXAiOiIyMDI2LTA5LTA3VDA2OjUyOjUxLjExMFoiLCJ0cmVlU2l6ZSI6MX0=",
80
+ "payload": "eyJyb290SGFzaCI6IjY4ODlmODQyNmVkODg4YTVlNzI0Y2YzNThlNmI0Y2E3ZmI3YWVmMTkwODkxOWE5MGYxZjNlNzFjMWU3MmNkODUiLCJ0aW1lc3RhbXAiOiIyMDI2LTA5LTA3VDA4OjM3OjM0LjkyOVoiLCJ0cmVlU2l6ZSI6MX0=",
81
81
  "signatures": [
82
82
  {
83
- "keyid": "e1ed29255a4bd710006fe9ad333514e71c705608b769029088a818f57bcf080a",
84
- "sig": "vydFxb4Z753KUPOXLEFiYnE0nntaoU9xXtBe7IMW8eucYeef+XpGu+4+bXpo/zYZATcx4h+7bMmDd3frqizkBg=="
83
+ "keyid": "48967bd6051d66cebf961f226a6ae4e90dce91ead9ac10c0f9aaa427d40c4c16",
84
+ "sig": "FL7TL3y1JgXinAkxywmKfoULNe2V8D+JpIcsJWok54rIun70z7bmj78R1VnNe9aADLSGQb0JTYhdaODn9mOnCg=="
85
85
  }
86
86
  ]
87
87
  },
88
88
  "newer": {
89
89
  "payloadType": "application/vnd.agent-custody.treehead+json",
90
- "payload": "eyJyb290SGFzaCI6ImNlYmViNDAzYjNlZWYzYzgwNWVjYzg5OTI2NTM0ZGM0NjkxY2I4OWJlN2I0ZjFjNjE3Njg5MDFkMzIxNDkzYmEiLCJ0aW1lc3RhbXAiOiIyMDI2LTA5LTA3VDA2OjUyOjUxLjExNFoiLCJ0cmVlU2l6ZSI6Mn0=",
90
+ "payload": "eyJyb290SGFzaCI6IjUxY2MyNzIzMzM0NzhmMjYxNDU1MWFmMWM1NjExMzBjOTBlODEwM2JhYmI1NzNkZjY2YjM3ZTVhMDk1YjI0ZWYiLCJ0aW1lc3RhbXAiOiIyMDI2LTA5LTA3VDA4OjM3OjM0LjkzM1oiLCJ0cmVlU2l6ZSI6Mn0=",
91
91
  "signatures": [
92
92
  {
93
- "keyid": "e1ed29255a4bd710006fe9ad333514e71c705608b769029088a818f57bcf080a",
94
- "sig": "iyUBqiJcZd/RaEDXWhhxl34ekqy6tQ9qgsMQjAXlvuHS75aqPLeh0NRtUQB89LS2UMJYihmcSUdg/Z82uORqCQ=="
93
+ "keyid": "48967bd6051d66cebf961f226a6ae4e90dce91ead9ac10c0f9aaa427d40c4c16",
94
+ "sig": "o/zrDH6/I3VPabSVS4RozKm945Nd+hiwt6JTytpyMavxizlR8ZuGmG6lX56+A0M4wAk/H0qmmHXoLCGuobhdCw=="
95
95
  }
96
96
  ]
97
97
  },
98
98
  "proof": [
99
- "4869289b5853ed3431c235a1f4e97d1d4205723e622dc71bd836018c62572723"
99
+ "e74d1e2874003df276b93e2e764da876201bcc1a9efe8752118676eafc4ce5eb"
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": "eyJyb290SGFzaCI6Ijg3MzM5M2E4ZTQ4ZmFkNzVlMmRkMzY1MzRjNmRjNzUzMTc5YmM0NmEzMTAxNDZjZTI2OGUxNTc2ODZiOGMwZTQiLCJ0aW1lc3RhbXAiOiIyMDI2LTA5LTA3VDA2OjUyOjUxLjExMFoiLCJ0cmVlU2l6ZSI6MX0=",
117
+ "payload": "eyJyb290SGFzaCI6IjY4ODlmODQyNmVkODg4YTVlNzI0Y2YzNThlNmI0Y2E3ZmI3YWVmMTkwODkxOWE5MGYxZjNlNzFjMWU3MmNkODUiLCJ0aW1lc3RhbXAiOiIyMDI2LTA5LTA3VDA4OjM3OjM0LjkyOVoiLCJ0cmVlU2l6ZSI6MX0=",
118
118
  "signatures": [
119
119
  {
120
- "keyid": "e1ed29255a4bd710006fe9ad333514e71c705608b769029088a818f57bcf080a",
121
- "sig": "vydFxb4Z753KUPOXLEFiYnE0nntaoU9xXtBe7IMW8eucYeef+XpGu+4+bXpo/zYZATcx4h+7bMmDd3frqizkBg=="
120
+ "keyid": "48967bd6051d66cebf961f226a6ae4e90dce91ead9ac10c0f9aaa427d40c4c16",
121
+ "sig": "FL7TL3y1JgXinAkxywmKfoULNe2V8D+JpIcsJWok54rIun70z7bmj78R1VnNe9aADLSGQb0JTYhdaODn9mOnCg=="
122
122
  }
123
123
  ]
124
124
  },
125
125
  "newer": {
126
126
  "payloadType": "application/vnd.agent-custody.treehead+json",
127
- "payload": "eyJyb290SGFzaCI6ImNlYmViNDAzYjNlZWYzYzgwNWVjYzg5OTI2NTM0ZGM0NjkxY2I4OWJlN2I0ZjFjNjE3Njg5MDFkMzIxNDkzYmEiLCJ0aW1lc3RhbXAiOiIyMDI2LTA5LTA3VDA2OjUyOjUxLjExNFoiLCJ0cmVlU2l6ZSI6Mn0=",
127
+ "payload": "eyJyb290SGFzaCI6IjUxY2MyNzIzMzM0NzhmMjYxNDU1MWFmMWM1NjExMzBjOTBlODEwM2JhYmI1NzNkZjY2YjM3ZTVhMDk1YjI0ZWYiLCJ0aW1lc3RhbXAiOiIyMDI2LTA5LTA3VDA4OjM3OjM0LjkzM1oiLCJ0cmVlU2l6ZSI6Mn0=",
128
128
  "signatures": [
129
129
  {
130
- "keyid": "e1ed29255a4bd710006fe9ad333514e71c705608b769029088a818f57bcf080a",
131
- "sig": "iyUBqiJcZd/RaEDXWhhxl34ekqy6tQ9qgsMQjAXlvuHS75aqPLeh0NRtUQB89LS2UMJYihmcSUdg/Z82uORqCQ=="
130
+ "keyid": "48967bd6051d66cebf961f226a6ae4e90dce91ead9ac10c0f9aaa427d40c4c16",
131
+ "sig": "o/zrDH6/I3VPabSVS4RozKm945Nd+hiwt6JTytpyMavxizlR8ZuGmG6lX56+A0M4wAk/H0qmmHXoLCGuobhdCw=="
132
132
  }
133
133
  ]
134
134
  },
@@ -53,18 +53,18 @@
53
53
  }
54
54
  ],
55
55
  "keyid": {
56
- "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAEb81f6KaNmH1pNR7KbeIUo9DW65Y0Qsfdxqbtnhs7cg=\n-----END PUBLIC KEY-----\n",
57
- "keyid": "0ecde3bf114156b895ceb4ea63daa345271e8ee979f47bc69cbf0266bb93f6f5"
56
+ "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEADBTs73puK6sTeOys85s6w1yJCBPt+eiQheOIszKqBiU=\n-----END PUBLIC KEY-----\n",
57
+ "keyid": "9dd265bc328e448488e35ec8ca89cc3b4270bfc6c8a0ba01845bb8867f1c7c60"
58
58
  },
59
59
  "dsse": {
60
- "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAEb81f6KaNmH1pNR7KbeIUo9DW65Y0Qsfdxqbtnhs7cg=\n-----END PUBLIC KEY-----\n",
60
+ "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEADBTs73puK6sTeOys85s6w1yJCBPt+eiQheOIszKqBiU=\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": "0ecde3bf114156b895ceb4ea63daa345271e8ee979f47bc69cbf0266bb93f6f5",
67
- "sig": "wkH8ufuyC3BJldxmhFUCImAOo38KyQO/uQMITvTjgikPeE9GIv6OwYEFtQnjcBNzY0+MRTn++dGeMwvVKzIBDw=="
66
+ "keyid": "9dd265bc328e448488e35ec8ca89cc3b4270bfc6c8a0ba01845bb8867f1c7c60",
67
+ "sig": "6zlQBbpy7oGNtH2d5gJ5ZmHzrKIjF8gT0XDkjhzafjCWWphFCcyZaVzZumJOjl6hnhzvu73FgHenAjyImMA0Cg=="
68
68
  }
69
69
  ]
70
70
  },