@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 +2 -1
- package/dist/cli.js +26 -6
- package/dist/config.d.ts +2 -0
- package/dist/config.js +1 -1
- package/dist/log-sink.d.ts +15 -0
- package/dist/log-sink.js +31 -18
- package/dist/log.d.ts +8 -0
- package/dist/log.js +17 -3
- package/dist/receipt.d.ts +2 -0
- package/dist/verify.d.ts +3 -1
- package/dist/verify.js +7 -1
- package/docs/usage.md +1 -1
- package/docs/verification.md +2 -1
- package/package.json +1 -1
- package/vectors/audit.json +27 -27
- package/vectors/canonical.json +5 -5
- package/vectors/receipts.json +312 -200
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.
|
|
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
|
-
|
|
128
|
-
|
|
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() });
|
package/dist/log-sink.d.ts
CHANGED
|
@@ -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
|
|
70
|
-
const
|
|
71
|
-
const
|
|
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
|
|
101
|
+
let parsed;
|
|
94
102
|
try {
|
|
95
|
-
|
|
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
|
|
169
|
-
|
|
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.
|
|
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
|
|
package/docs/verification.md
CHANGED
|
@@ -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
|
+
"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": {
|
package/vectors/audit.json
CHANGED
|
@@ -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": "
|
|
10
|
+
"payload": "eyJsb2ciOiJ2ZWN0b3JzLWxvZyIsInJvb3RIYXNoIjoiODViYjZhMjk1YzYxYmIzMzg5MWYyNTFiNWNiZGQ4NzNjNGVkYTU5OTBkMWE4MzVjNDRjYjZiMDgwNTBhY2U2MiIsInRpbWVzdGFtcCI6IjIwMjYtMDktMDhUMDY6MzA6MDQuNjkzWiIsInRyZWVTaXplIjoxfQ==",
|
|
11
11
|
"signatures": [
|
|
12
12
|
{
|
|
13
|
-
"keyid": "
|
|
14
|
-
"sig": "
|
|
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": "
|
|
20
|
+
"payload": "eyJsb2ciOiJ2ZWN0b3JzLWxvZyIsInJvb3RIYXNoIjoiODY0N2E0MTczNTIxYzE5Mzg4NTg2Y2U1MzYyNGU3MGZiYWEyMzc0NGRiYjIzNTQ0ZDU2N2Q5NDY0NmE0ZTM3MiIsInRpbWVzdGFtcCI6IjIwMjYtMDktMDhUMDY6MzA6MDQuNjk3WiIsInRyZWVTaXplIjoyfQ==",
|
|
21
21
|
"signatures": [
|
|
22
22
|
{
|
|
23
|
-
"keyid": "
|
|
24
|
-
"sig": "
|
|
23
|
+
"keyid": "74356bcffe957c1c3a3bde6702505f536d4e8bab2aac3b0f36419e846da1a8d5",
|
|
24
|
+
"sig": "iaweKtCddKNA0R+dkq+0qyzwe8RhZdKgK6y9o21adUCjhMAGlpl+YDe1chzn4NvT7jIZvn+HDdll136TmqcmCQ=="
|
|
25
25
|
}
|
|
26
26
|
]
|
|
27
27
|
},
|
|
28
28
|
"proof": [
|
|
29
|
-
"
|
|
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": "
|
|
44
|
+
"payload": "eyJsb2ciOiJ2ZWN0b3JzLWxvZyIsInJvb3RIYXNoIjoiODY0N2E0MTczNTIxYzE5Mzg4NTg2Y2U1MzYyNGU3MGZiYWEyMzc0NGRiYjIzNTQ0ZDU2N2Q5NDY0NmE0ZTM3MiIsInRpbWVzdGFtcCI6IjIwMjYtMDktMDhUMDY6MzA6MDQuNjk3WiIsInRyZWVTaXplIjoyfQ==",
|
|
45
45
|
"signatures": [
|
|
46
46
|
{
|
|
47
|
-
"keyid": "
|
|
48
|
-
"sig": "
|
|
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": "
|
|
54
|
+
"payload": "eyJsb2ciOiJ2ZWN0b3JzLWxvZyIsInJvb3RIYXNoIjoiODViYjZhMjk1YzYxYmIzMzg5MWYyNTFiNWNiZGQ4NzNjNGVkYTU5OTBkMWE4MzVjNDRjYjZiMDgwNTBhY2U2MiIsInRpbWVzdGFtcCI6IjIwMjYtMDktMDhUMDY6MzA6MDQuNjkzWiIsInRyZWVTaXplIjoxfQ==",
|
|
55
55
|
"signatures": [
|
|
56
56
|
{
|
|
57
|
-
"keyid": "
|
|
58
|
-
"sig": "
|
|
57
|
+
"keyid": "74356bcffe957c1c3a3bde6702505f536d4e8bab2aac3b0f36419e846da1a8d5",
|
|
58
|
+
"sig": "xhkG/l7l3ElnvGnxrFfK2nYUCkRo0ktj0+6ctv6V1qKiXihMYexcOu/qepdqWWN0mWFq2u8/Aqo6kbdoWhiFDQ=="
|
|
59
59
|
}
|
|
60
60
|
]
|
|
61
61
|
},
|
|
62
62
|
"proof": [
|
|
63
|
-
"
|
|
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": "
|
|
80
|
+
"payload": "eyJsb2ciOiJ2ZWN0b3JzLWxvZyIsInJvb3RIYXNoIjoiODViYjZhMjk1YzYxYmIzMzg5MWYyNTFiNWNiZGQ4NzNjNGVkYTU5OTBkMWE4MzVjNDRjYjZiMDgwNTBhY2U2MiIsInRpbWVzdGFtcCI6IjIwMjYtMDktMDhUMDY6MzA6MDQuNjkzWiIsInRyZWVTaXplIjoxfQ==",
|
|
81
81
|
"signatures": [
|
|
82
82
|
{
|
|
83
|
-
"keyid": "
|
|
84
|
-
"sig": "
|
|
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": "
|
|
90
|
+
"payload": "eyJsb2ciOiJ2ZWN0b3JzLWxvZyIsInJvb3RIYXNoIjoiODY0N2E0MTczNTIxYzE5Mzg4NTg2Y2U1MzYyNGU3MGZiYWEyMzc0NGRiYjIzNTQ0ZDU2N2Q5NDY0NmE0ZTM3MiIsInRpbWVzdGFtcCI6IjIwMjYtMDktMDhUMDY6MzA6MDQuNjk3WiIsInRyZWVTaXplIjoyfQ==",
|
|
91
91
|
"signatures": [
|
|
92
92
|
{
|
|
93
|
-
"keyid": "
|
|
94
|
-
"sig": "
|
|
93
|
+
"keyid": "74356bcffe957c1c3a3bde6702505f536d4e8bab2aac3b0f36419e846da1a8d5",
|
|
94
|
+
"sig": "iaweKtCddKNA0R+dkq+0qyzwe8RhZdKgK6y9o21adUCjhMAGlpl+YDe1chzn4NvT7jIZvn+HDdll136TmqcmCQ=="
|
|
95
95
|
}
|
|
96
96
|
]
|
|
97
97
|
},
|
|
98
98
|
"proof": [
|
|
99
|
-
"
|
|
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": "
|
|
117
|
+
"payload": "eyJsb2ciOiJ2ZWN0b3JzLWxvZyIsInJvb3RIYXNoIjoiODViYjZhMjk1YzYxYmIzMzg5MWYyNTFiNWNiZGQ4NzNjNGVkYTU5OTBkMWE4MzVjNDRjYjZiMDgwNTBhY2U2MiIsInRpbWVzdGFtcCI6IjIwMjYtMDktMDhUMDY6MzA6MDQuNjkzWiIsInRyZWVTaXplIjoxfQ==",
|
|
118
118
|
"signatures": [
|
|
119
119
|
{
|
|
120
|
-
"keyid": "
|
|
121
|
-
"sig": "
|
|
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": "
|
|
127
|
+
"payload": "eyJsb2ciOiJ2ZWN0b3JzLWxvZyIsInJvb3RIYXNoIjoiODY0N2E0MTczNTIxYzE5Mzg4NTg2Y2U1MzYyNGU3MGZiYWEyMzc0NGRiYjIzNTQ0ZDU2N2Q5NDY0NmE0ZTM3MiIsInRpbWVzdGFtcCI6IjIwMjYtMDktMDhUMDY6MzA6MDQuNjk3WiIsInRyZWVTaXplIjoyfQ==",
|
|
128
128
|
"signatures": [
|
|
129
129
|
{
|
|
130
|
-
"keyid": "
|
|
131
|
-
"sig": "
|
|
130
|
+
"keyid": "74356bcffe957c1c3a3bde6702505f536d4e8bab2aac3b0f36419e846da1a8d5",
|
|
131
|
+
"sig": "iaweKtCddKNA0R+dkq+0qyzwe8RhZdKgK6y9o21adUCjhMAGlpl+YDe1chzn4NvT7jIZvn+HDdll136TmqcmCQ=="
|
|
132
132
|
}
|
|
133
133
|
]
|
|
134
134
|
},
|
package/vectors/canonical.json
CHANGED
|
@@ -53,18 +53,18 @@
|
|
|
53
53
|
}
|
|
54
54
|
],
|
|
55
55
|
"keyid": {
|
|
56
|
-
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\
|
|
57
|
-
"keyid": "
|
|
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-----\
|
|
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": "
|
|
67
|
-
"sig": "
|
|
66
|
+
"keyid": "3df0af0ec3abda622a30f7a3273fc6ca4eda229404122f02e2cae12923a861ad",
|
|
67
|
+
"sig": "aGI6lAPRJs17z+UUezv/7iGyZ1rHbjKRWBELgeC4YjLGytQLUZsH+2jwFKhmTasJIyssry1ob+HMt4V20WCzBQ=="
|
|
68
68
|
}
|
|
69
69
|
]
|
|
70
70
|
},
|