@agent-receipts/crypto 0.3.3 → 0.5.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 +19 -14
- package/dist/index.d.mts +41 -1
- package/dist/index.d.ts +41 -1
- package/dist/index.js +100 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +91 -1
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @agent-receipts/crypto
|
|
2
2
|
|
|
3
|
-
Ed25519 signing and verification for the
|
|
3
|
+
Ed25519 signing and verification for the Agent Receipts protocol. Signs receipts, verifies signatures, manages key pairs. Pure JavaScript, zero native dependencies, audited cryptography via @noble/ed25519.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -13,24 +13,24 @@ npm install @agent-receipts/crypto
|
|
|
13
13
|
```typescript
|
|
14
14
|
import {
|
|
15
15
|
generateKeyPair,
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
signReceiptV2,
|
|
17
|
+
verifyAnyReceipt,
|
|
18
|
+
computeKeyId,
|
|
19
|
+
hashReceipt,
|
|
20
20
|
} from '@agent-receipts/crypto'
|
|
21
21
|
|
|
22
22
|
// Generate a new Ed25519 key pair
|
|
23
23
|
const { privateKey, publicKey } = generateKeyPair()
|
|
24
24
|
|
|
25
|
-
// Sign a receipt
|
|
26
|
-
const
|
|
27
|
-
|
|
25
|
+
// Sign a receipt (format v2: every field except signature + verify_url)
|
|
26
|
+
const receipt = { ...fields, sig_version: 2, key_id: computeKeyId(publicKey) }
|
|
27
|
+
receipt.signature = signReceiptV2(receipt, privateKey)
|
|
28
28
|
|
|
29
|
-
// Verify
|
|
30
|
-
const valid =
|
|
29
|
+
// Verify any receipt (v1 or v2). Use requireV2 when you show summary, cost or files to people.
|
|
30
|
+
const { valid, sig_version, covers_all_fields, key_id_matches } = verifyAnyReceipt(receipt, publicKey, { requireV2: true })
|
|
31
31
|
|
|
32
|
-
//
|
|
33
|
-
const
|
|
32
|
+
// Link the next receipt in a chain to this one
|
|
33
|
+
const prev_receipt_hash = hashReceipt(receipt)
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
## API
|
|
@@ -38,8 +38,13 @@ const pub = getPublicKeyFromPrivate(privateKey)
|
|
|
38
38
|
| Function | Description |
|
|
39
39
|
|----------|-------------|
|
|
40
40
|
| `generateKeyPair()` | Generate an Ed25519 key pair (returns `{ privateKey, publicKey }` as hex strings) |
|
|
41
|
-
| `
|
|
42
|
-
| `
|
|
41
|
+
| `signReceiptV2(receipt, privateKey)` | Sign every field of a v2 receipt (deep canonical JSON) |
|
|
42
|
+
| `verifyAnyReceipt(receipt, publicKey, { requireV2? })` | Verify v1 or v2; reports `sig_version`, `covers_all_fields`, `key_id_matches` |
|
|
43
|
+
| `computeKeyId(publicKey)` | Short key name: `key_` + 16 hex of sha256(key) |
|
|
44
|
+
| `hashReceipt(receipt)` | sha256 of a whole stored receipt (for `prev_receipt_hash`) |
|
|
45
|
+
| `canonicalizeDeep(value)` | Keys sorted at every depth; the bytes v2 signs |
|
|
46
|
+
| `signReceipt(payload, privateKey)` | Legacy v1: sign the 12-field payload |
|
|
47
|
+
| `verifyReceipt(payload, signature, publicKey)` | Legacy v1: verify a 12-field payload (does not verify v2 receipts) |
|
|
43
48
|
| `getSignablePayload(receipt)` | Extract the deterministic signable fields from a receipt |
|
|
44
49
|
| `getPublicKeyFromPrivate(privateKey)` | Derive the public key from a private key |
|
|
45
50
|
| `canonicalize(obj)` | Deterministic JSON serialization for signing |
|
package/dist/index.d.mts
CHANGED
|
@@ -69,4 +69,44 @@ declare function generateKeyPair(): {
|
|
|
69
69
|
*/
|
|
70
70
|
declare function getPublicKeyFromPrivate(privateKeyHex: string): string;
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
/** Fields never covered by a v2 signature. */
|
|
73
|
+
declare const V2_UNSIGNED_FIELDS: readonly ["signature", "verify_url"];
|
|
74
|
+
/**
|
|
75
|
+
* Deep canonical JSON: keys sorted at every depth, arrays kept in order,
|
|
76
|
+
* `undefined` object values dropped (as JSON.stringify does).
|
|
77
|
+
*
|
|
78
|
+
* The browser verifier (receipt.html, verify page) re-implements exactly this
|
|
79
|
+
* function, so keep the two in sync.
|
|
80
|
+
*/
|
|
81
|
+
declare function canonicalizeDeep(value: unknown): string;
|
|
82
|
+
/** Everything in the receipt except the signature and verify_url. */
|
|
83
|
+
declare function getSignablePayloadV2(receipt: Record<string, unknown>): Record<string, unknown>;
|
|
84
|
+
/** Sign a v2 receipt. The receipt must already carry `sig_version: 2`. */
|
|
85
|
+
declare function signReceiptV2(receipt: Record<string, unknown>, privateKey: string): string;
|
|
86
|
+
/** Verify a v2 signature. Returns false for anything that isn't a valid v2 receipt. */
|
|
87
|
+
declare function verifyReceiptV2(receipt: Record<string, unknown>, publicKey: string): boolean;
|
|
88
|
+
interface VerifyResult {
|
|
89
|
+
valid: boolean;
|
|
90
|
+
sig_version: 1 | 2;
|
|
91
|
+
/** True when the signature covers every field (v2). v1 covers 12 fields only. */
|
|
92
|
+
covers_all_fields: boolean;
|
|
93
|
+
key_id_matches: boolean | null;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Verify any receipt, v1 or v2.
|
|
97
|
+
*
|
|
98
|
+
* A v1 receipt only signs 12 core fields: its summary, cost, files and
|
|
99
|
+
* metadata can be edited without breaking the signature. Verifiers that show
|
|
100
|
+
* those fields to people must pass `{ requireV2: true }` (stripping
|
|
101
|
+
* `sig_version` from a v2 receipt does not help an attacker: v1 signs
|
|
102
|
+
* different bytes, so it fails either way).
|
|
103
|
+
*/
|
|
104
|
+
declare function verifyAnyReceipt(receipt: Record<string, unknown>, publicKey: string, options?: {
|
|
105
|
+
requireV2?: boolean;
|
|
106
|
+
}): VerifyResult;
|
|
107
|
+
/** Short, stable name for a public key: `key_` + first 16 hex of sha256(raw key bytes). */
|
|
108
|
+
declare function computeKeyId(publicKeyHex: string): string;
|
|
109
|
+
/** Hash of a whole stored receipt (signature included) — what the next receipt in a chain links to. */
|
|
110
|
+
declare function hashReceipt(receipt: Record<string, unknown>): string;
|
|
111
|
+
|
|
112
|
+
export { V2_UNSIGNED_FIELDS, type VerifyResult, canonicalize, canonicalizeDeep, computeKeyId, generateKeyPair, getPublicKeyFromPrivate, getSignablePayload, getSignablePayloadV2, hashReceipt, signReceipt, signReceiptV2, verifyAnyReceipt, verifyReceipt, verifyReceiptV2 };
|
package/dist/index.d.ts
CHANGED
|
@@ -69,4 +69,44 @@ declare function generateKeyPair(): {
|
|
|
69
69
|
*/
|
|
70
70
|
declare function getPublicKeyFromPrivate(privateKeyHex: string): string;
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
/** Fields never covered by a v2 signature. */
|
|
73
|
+
declare const V2_UNSIGNED_FIELDS: readonly ["signature", "verify_url"];
|
|
74
|
+
/**
|
|
75
|
+
* Deep canonical JSON: keys sorted at every depth, arrays kept in order,
|
|
76
|
+
* `undefined` object values dropped (as JSON.stringify does).
|
|
77
|
+
*
|
|
78
|
+
* The browser verifier (receipt.html, verify page) re-implements exactly this
|
|
79
|
+
* function, so keep the two in sync.
|
|
80
|
+
*/
|
|
81
|
+
declare function canonicalizeDeep(value: unknown): string;
|
|
82
|
+
/** Everything in the receipt except the signature and verify_url. */
|
|
83
|
+
declare function getSignablePayloadV2(receipt: Record<string, unknown>): Record<string, unknown>;
|
|
84
|
+
/** Sign a v2 receipt. The receipt must already carry `sig_version: 2`. */
|
|
85
|
+
declare function signReceiptV2(receipt: Record<string, unknown>, privateKey: string): string;
|
|
86
|
+
/** Verify a v2 signature. Returns false for anything that isn't a valid v2 receipt. */
|
|
87
|
+
declare function verifyReceiptV2(receipt: Record<string, unknown>, publicKey: string): boolean;
|
|
88
|
+
interface VerifyResult {
|
|
89
|
+
valid: boolean;
|
|
90
|
+
sig_version: 1 | 2;
|
|
91
|
+
/** True when the signature covers every field (v2). v1 covers 12 fields only. */
|
|
92
|
+
covers_all_fields: boolean;
|
|
93
|
+
key_id_matches: boolean | null;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Verify any receipt, v1 or v2.
|
|
97
|
+
*
|
|
98
|
+
* A v1 receipt only signs 12 core fields: its summary, cost, files and
|
|
99
|
+
* metadata can be edited without breaking the signature. Verifiers that show
|
|
100
|
+
* those fields to people must pass `{ requireV2: true }` (stripping
|
|
101
|
+
* `sig_version` from a v2 receipt does not help an attacker: v1 signs
|
|
102
|
+
* different bytes, so it fails either way).
|
|
103
|
+
*/
|
|
104
|
+
declare function verifyAnyReceipt(receipt: Record<string, unknown>, publicKey: string, options?: {
|
|
105
|
+
requireV2?: boolean;
|
|
106
|
+
}): VerifyResult;
|
|
107
|
+
/** Short, stable name for a public key: `key_` + first 16 hex of sha256(raw key bytes). */
|
|
108
|
+
declare function computeKeyId(publicKeyHex: string): string;
|
|
109
|
+
/** Hash of a whole stored receipt (signature included) — what the next receipt in a chain links to. */
|
|
110
|
+
declare function hashReceipt(receipt: Record<string, unknown>): string;
|
|
111
|
+
|
|
112
|
+
export { V2_UNSIGNED_FIELDS, type VerifyResult, canonicalize, canonicalizeDeep, computeKeyId, generateKeyPair, getPublicKeyFromPrivate, getSignablePayload, getSignablePayloadV2, hashReceipt, signReceipt, signReceiptV2, verifyAnyReceipt, verifyReceipt, verifyReceiptV2 };
|
package/dist/index.js
CHANGED
|
@@ -20,12 +20,20 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
V2_UNSIGNED_FIELDS: () => V2_UNSIGNED_FIELDS,
|
|
23
24
|
canonicalize: () => canonicalize,
|
|
25
|
+
canonicalizeDeep: () => canonicalizeDeep,
|
|
26
|
+
computeKeyId: () => computeKeyId,
|
|
24
27
|
generateKeyPair: () => generateKeyPair,
|
|
25
28
|
getPublicKeyFromPrivate: () => getPublicKeyFromPrivate,
|
|
26
29
|
getSignablePayload: () => getSignablePayload,
|
|
30
|
+
getSignablePayloadV2: () => getSignablePayloadV2,
|
|
31
|
+
hashReceipt: () => hashReceipt,
|
|
27
32
|
signReceipt: () => signReceipt,
|
|
28
|
-
|
|
33
|
+
signReceiptV2: () => signReceiptV2,
|
|
34
|
+
verifyAnyReceipt: () => verifyAnyReceipt,
|
|
35
|
+
verifyReceipt: () => verifyReceipt,
|
|
36
|
+
verifyReceiptV2: () => verifyReceiptV2
|
|
29
37
|
});
|
|
30
38
|
module.exports = __toCommonJS(index_exports);
|
|
31
39
|
|
|
@@ -98,13 +106,103 @@ function getPublicKeyFromPrivate(privateKeyHex) {
|
|
|
98
106
|
const pubKeyBytes = (0, import_ed255192.getPublicKey)(privKeyBytes);
|
|
99
107
|
return import_ed255192.etc.bytesToHex(pubKeyBytes);
|
|
100
108
|
}
|
|
109
|
+
|
|
110
|
+
// src/v2.ts
|
|
111
|
+
var import_ed255193 = require("@noble/ed25519");
|
|
112
|
+
var import_sha256 = require("@noble/hashes/sha256");
|
|
113
|
+
var import_sha5122 = require("@noble/hashes/sha512");
|
|
114
|
+
import_ed255193.etc.sha512Sync = (...messages) => (0, import_sha5122.sha512)(import_ed255193.etc.concatBytes(...messages));
|
|
115
|
+
var V2_UNSIGNED_FIELDS = ["signature", "verify_url"];
|
|
116
|
+
function canonicalizeDeep(value) {
|
|
117
|
+
return JSON.stringify(sortDeep(value)) ?? "null";
|
|
118
|
+
}
|
|
119
|
+
function sortDeep(value) {
|
|
120
|
+
if (value === null || typeof value !== "object") return value;
|
|
121
|
+
if (Array.isArray(value)) return value.map(sortDeep);
|
|
122
|
+
const out = /* @__PURE__ */ Object.create(null);
|
|
123
|
+
for (const key of Object.keys(value).sort()) {
|
|
124
|
+
const v = value[key];
|
|
125
|
+
if (v !== void 0) out[key] = sortDeep(v);
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
function getSignablePayloadV2(receipt) {
|
|
130
|
+
const out = /* @__PURE__ */ Object.create(null);
|
|
131
|
+
for (const [key, value] of Object.entries(receipt)) {
|
|
132
|
+
if (V2_UNSIGNED_FIELDS.includes(key)) continue;
|
|
133
|
+
if (value !== void 0) out[key] = value;
|
|
134
|
+
}
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
function signReceiptV2(receipt, privateKey) {
|
|
138
|
+
if (receipt["sig_version"] !== 2) {
|
|
139
|
+
throw new Error("signReceiptV2: receipt.sig_version must be 2");
|
|
140
|
+
}
|
|
141
|
+
const message = new TextEncoder().encode(canonicalizeDeep(getSignablePayloadV2(receipt)));
|
|
142
|
+
const sig = (0, import_ed255193.sign)(message, import_ed255193.etc.hexToBytes(privateKey));
|
|
143
|
+
return `ed25519:${Buffer.from(sig).toString("base64")}`;
|
|
144
|
+
}
|
|
145
|
+
function verifyReceiptV2(receipt, publicKey) {
|
|
146
|
+
try {
|
|
147
|
+
const signature = receipt["signature"];
|
|
148
|
+
if (receipt["sig_version"] !== 2 || typeof signature !== "string") return false;
|
|
149
|
+
if (!signature.startsWith("ed25519:")) return false;
|
|
150
|
+
const message = new TextEncoder().encode(canonicalizeDeep(getSignablePayloadV2(receipt)));
|
|
151
|
+
const sigBytes = new Uint8Array(Buffer.from(signature.slice("ed25519:".length), "base64"));
|
|
152
|
+
return (0, import_ed255193.verify)(sigBytes, message, import_ed255193.etc.hexToBytes(publicKey));
|
|
153
|
+
} catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function verifyAnyReceipt(receipt, publicKey, options = {}) {
|
|
158
|
+
if (receipt["sig_version"] === 2) {
|
|
159
|
+
const keyId = receipt["key_id"];
|
|
160
|
+
return {
|
|
161
|
+
valid: verifyReceiptV2(receipt, publicKey),
|
|
162
|
+
sig_version: 2,
|
|
163
|
+
covers_all_fields: true,
|
|
164
|
+
key_id_matches: typeof keyId === "string" ? keyId === computeKeyId(publicKey) : null
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (options.requireV2) {
|
|
168
|
+
return { valid: false, sig_version: 1, covers_all_fields: false, key_id_matches: null };
|
|
169
|
+
}
|
|
170
|
+
let valid = false;
|
|
171
|
+
try {
|
|
172
|
+
const signable = getSignablePayload(receipt);
|
|
173
|
+
const signature = String(receipt["signature"] ?? "");
|
|
174
|
+
if (signature.startsWith("ed25519:")) {
|
|
175
|
+
const message = new TextEncoder().encode(canonicalize(signable));
|
|
176
|
+
const sigBytes = new Uint8Array(Buffer.from(signature.slice("ed25519:".length), "base64"));
|
|
177
|
+
valid = (0, import_ed255193.verify)(sigBytes, message, import_ed255193.etc.hexToBytes(publicKey));
|
|
178
|
+
}
|
|
179
|
+
} catch {
|
|
180
|
+
valid = false;
|
|
181
|
+
}
|
|
182
|
+
return { valid, sig_version: 1, covers_all_fields: false, key_id_matches: null };
|
|
183
|
+
}
|
|
184
|
+
function computeKeyId(publicKeyHex) {
|
|
185
|
+
return `key_${import_ed255193.etc.bytesToHex((0, import_sha256.sha256)(import_ed255193.etc.hexToBytes(publicKeyHex))).slice(0, 16)}`;
|
|
186
|
+
}
|
|
187
|
+
function hashReceipt(receipt) {
|
|
188
|
+
const bytes = new TextEncoder().encode(canonicalizeDeep(receipt));
|
|
189
|
+
return `sha256:${import_ed255193.etc.bytesToHex((0, import_sha256.sha256)(bytes))}`;
|
|
190
|
+
}
|
|
101
191
|
// Annotate the CommonJS export names for ESM import in node:
|
|
102
192
|
0 && (module.exports = {
|
|
193
|
+
V2_UNSIGNED_FIELDS,
|
|
103
194
|
canonicalize,
|
|
195
|
+
canonicalizeDeep,
|
|
196
|
+
computeKeyId,
|
|
104
197
|
generateKeyPair,
|
|
105
198
|
getPublicKeyFromPrivate,
|
|
106
199
|
getSignablePayload,
|
|
200
|
+
getSignablePayloadV2,
|
|
201
|
+
hashReceipt,
|
|
107
202
|
signReceipt,
|
|
108
|
-
|
|
203
|
+
signReceiptV2,
|
|
204
|
+
verifyAnyReceipt,
|
|
205
|
+
verifyReceipt,
|
|
206
|
+
verifyReceiptV2
|
|
109
207
|
});
|
|
110
208
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/canonical.ts","../src/sign.ts","../src/keys.ts"],"sourcesContent":["export { canonicalize } from './canonical'\nexport { signReceipt, verifyReceipt, getSignablePayload } from './sign'\nexport { generateKeyPair, getPublicKeyFromPrivate } from './keys'\n","import type { SignablePayload } from '@agent-receipts/schema'\n\n/**\n * Produce a canonical JSON string from a SignablePayload.\n * Keys are sorted alphabetically. Output is deterministic.\n *\n * IMPORTANT: SignablePayload contains only flat primitives (strings, nulls).\n * No nested objects or arrays. Simple key sorting is sufficient here.\n *\n * For hashing arbitrary user input/output (in the SDK), use deep canonical\n * sorting — that's a different function in the SDK package.\n */\nexport function canonicalize(payload: SignablePayload): string {\n const sorted = Object.keys(payload).sort()\n return JSON.stringify(payload, sorted)\n}\n","import { sign, verify, etc, getPublicKey } from '@noble/ed25519'\nimport { sha512 } from '@noble/hashes/sha512'\nimport type { SignablePayload } from '@agent-receipts/schema'\nimport { canonicalize } from './canonical'\n\n// Configure noble/ed25519 to use sha512 for sync operations.\n// This is required by @noble/ed25519 v2 — without it, sync sign/verify throw.\netc.sha512Sync = (...messages: Uint8Array[]): Uint8Array =>\n sha512(etc.concatBytes(...messages))\n\n/**\n * Extract the signable fields from a full receipt object.\n * Maps receipt fields to the SignablePayload structure.\n *\n * NOTE: The API uses `timestamp` while the DB uses `created_at`.\n * This function expects the API format (timestamp).\n */\nexport function getSignablePayload(receipt: {\n receipt_id: string\n chain_id: string\n receipt_type: string\n agent_id: string\n org_id: string\n action: string\n input_hash: string\n output_hash: string | null\n status: string\n timestamp: string\n completed_at: string | null\n environment: string\n [key: string]: unknown\n}): SignablePayload {\n return {\n receipt_id: receipt.receipt_id,\n chain_id: receipt.chain_id,\n receipt_type: receipt.receipt_type as SignablePayload['receipt_type'],\n agent_id: receipt.agent_id,\n org_id: receipt.org_id,\n action: receipt.action,\n input_hash: receipt.input_hash,\n output_hash: receipt.output_hash,\n status: receipt.status as SignablePayload['status'],\n timestamp: receipt.timestamp,\n completed_at: receipt.completed_at,\n environment: receipt.environment as SignablePayload['environment'],\n }\n}\n\n/**\n * Sign a receipt payload with Ed25519.\n *\n * @param payload - The signable fields (12 deterministic fields)\n * @param privateKey - Ed25519 private key as hex string (64 chars = 32 bytes)\n * @returns Signature string in format \"ed25519:<base64>\"\n */\nexport function signReceipt(\n payload: SignablePayload,\n privateKey: string\n): string {\n const canonical = canonicalize(payload)\n const message = new TextEncoder().encode(canonical)\n const privKeyBytes = etc.hexToBytes(privateKey)\n const signature = sign(message, privKeyBytes)\n return `ed25519:${bytesToBase64(signature)}`\n}\n\n/**\n * Verify a receipt signature.\n *\n * @param payload - The signable fields (must match what was signed)\n * @param signature - Signature string in format \"ed25519:<base64>\"\n * @param publicKey - Ed25519 public key as hex string (64 chars = 32 bytes)\n * @returns true if signature is valid\n */\nexport function verifyReceipt(\n payload: SignablePayload,\n signature: string,\n publicKey: string\n): boolean {\n try {\n if (!signature.startsWith('ed25519:')) {\n return false\n }\n const canonical = canonicalize(payload)\n const message = new TextEncoder().encode(canonical)\n const sigBytes = base64ToBytes(signature.slice('ed25519:'.length))\n const pubKeyBytes = etc.hexToBytes(publicKey)\n return verify(sigBytes, message, pubKeyBytes)\n } catch {\n return false\n }\n}\n\nfunction bytesToBase64(bytes: Uint8Array): string {\n return Buffer.from(bytes).toString('base64')\n}\n\nfunction base64ToBytes(b64: string): Uint8Array {\n return new Uint8Array(Buffer.from(b64, 'base64'))\n}\n","import { getPublicKey, etc, utils } from '@noble/ed25519'\n\n/**\n * Generate a new Ed25519 key pair.\n *\n * @returns { privateKey: string, publicKey: string } — both hex-encoded (64 chars each)\n */\nexport function generateKeyPair(): {\n privateKey: string\n publicKey: string\n} {\n const privateKeyBytes = utils.randomPrivateKey()\n const publicKeyBytes = getPublicKey(privateKeyBytes)\n return {\n privateKey: etc.bytesToHex(privateKeyBytes),\n publicKey: etc.bytesToHex(publicKeyBytes),\n }\n}\n\n/**\n * Derive public key from private key.\n *\n * @param privateKeyHex - Ed25519 private key as hex string (64 chars)\n * @returns Public key as hex string (64 chars)\n */\nexport function getPublicKeyFromPrivate(privateKeyHex: string): string {\n const privKeyBytes = etc.hexToBytes(privateKeyHex)\n const pubKeyBytes = getPublicKey(privKeyBytes)\n return etc.bytesToHex(pubKeyBytes)\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACYO,SAAS,aAAa,SAAkC;AAC7D,QAAM,SAAS,OAAO,KAAK,OAAO,EAAE,KAAK;AACzC,SAAO,KAAK,UAAU,SAAS,MAAM;AACvC;;;ACfA,qBAAgD;AAChD,oBAAuB;AAMvB,mBAAI,aAAa,IAAI,iBACnB,sBAAO,mBAAI,YAAY,GAAG,QAAQ,CAAC;AAS9B,SAAS,mBAAmB,SAcf;AAClB,SAAO;AAAA,IACL,YAAY,QAAQ;AAAA,IACpB,UAAU,QAAQ;AAAA,IAClB,cAAc,QAAQ;AAAA,IACtB,UAAU,QAAQ;AAAA,IAClB,QAAQ,QAAQ;AAAA,IAChB,QAAQ,QAAQ;AAAA,IAChB,YAAY,QAAQ;AAAA,IACpB,aAAa,QAAQ;AAAA,IACrB,QAAQ,QAAQ;AAAA,IAChB,WAAW,QAAQ;AAAA,IACnB,cAAc,QAAQ;AAAA,IACtB,aAAa,QAAQ;AAAA,EACvB;AACF;AASO,SAAS,YACd,SACA,YACQ;AACR,QAAM,YAAY,aAAa,OAAO;AACtC,QAAM,UAAU,IAAI,YAAY,EAAE,OAAO,SAAS;AAClD,QAAM,eAAe,mBAAI,WAAW,UAAU;AAC9C,QAAM,gBAAY,qBAAK,SAAS,YAAY;AAC5C,SAAO,WAAW,cAAc,SAAS,CAAC;AAC5C;AAUO,SAAS,cACd,SACA,WACA,WACS;AACT,MAAI;AACF,QAAI,CAAC,UAAU,WAAW,UAAU,GAAG;AACrC,aAAO;AAAA,IACT;AACA,UAAM,YAAY,aAAa,OAAO;AACtC,UAAM,UAAU,IAAI,YAAY,EAAE,OAAO,SAAS;AAClD,UAAM,WAAW,cAAc,UAAU,MAAM,WAAW,MAAM,CAAC;AACjE,UAAM,cAAc,mBAAI,WAAW,SAAS;AAC5C,eAAO,uBAAO,UAAU,SAAS,WAAW;AAAA,EAC9C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,cAAc,OAA2B;AAChD,SAAO,OAAO,KAAK,KAAK,EAAE,SAAS,QAAQ;AAC7C;AAEA,SAAS,cAAc,KAAyB;AAC9C,SAAO,IAAI,WAAW,OAAO,KAAK,KAAK,QAAQ,CAAC;AAClD;;;ACnGA,IAAAA,kBAAyC;AAOlC,SAAS,kBAGd;AACA,QAAM,kBAAkB,sBAAM,iBAAiB;AAC/C,QAAM,qBAAiB,8BAAa,eAAe;AACnD,SAAO;AAAA,IACL,YAAY,oBAAI,WAAW,eAAe;AAAA,IAC1C,WAAW,oBAAI,WAAW,cAAc;AAAA,EAC1C;AACF;AAQO,SAAS,wBAAwB,eAA+B;AACrE,QAAM,eAAe,oBAAI,WAAW,aAAa;AACjD,QAAM,kBAAc,8BAAa,YAAY;AAC7C,SAAO,oBAAI,WAAW,WAAW;AACnC;","names":["import_ed25519"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/canonical.ts","../src/sign.ts","../src/keys.ts","../src/v2.ts"],"sourcesContent":["export { canonicalize } from './canonical'\nexport { signReceipt, verifyReceipt, getSignablePayload } from './sign'\nexport { generateKeyPair, getPublicKeyFromPrivate } from './keys'\nexport {\n canonicalizeDeep,\n getSignablePayloadV2,\n signReceiptV2,\n verifyReceiptV2,\n verifyAnyReceipt,\n computeKeyId,\n hashReceipt,\n V2_UNSIGNED_FIELDS,\n} from './v2'\nexport type { VerifyResult } from './v2'\n","import type { SignablePayload } from '@agent-receipts/schema'\n\n/**\n * Produce a canonical JSON string from a SignablePayload.\n * Keys are sorted alphabetically. Output is deterministic.\n *\n * IMPORTANT: SignablePayload contains only flat primitives (strings, nulls).\n * No nested objects or arrays. Simple key sorting is sufficient here.\n *\n * For hashing arbitrary user input/output (in the SDK), use deep canonical\n * sorting — that's a different function in the SDK package.\n */\nexport function canonicalize(payload: SignablePayload): string {\n const sorted = Object.keys(payload).sort()\n return JSON.stringify(payload, sorted)\n}\n","import { sign, verify, etc, getPublicKey } from '@noble/ed25519'\nimport { sha512 } from '@noble/hashes/sha512'\nimport type { SignablePayload } from '@agent-receipts/schema'\nimport { canonicalize } from './canonical'\n\n// Configure noble/ed25519 to use sha512 for sync operations.\n// This is required by @noble/ed25519 v2 — without it, sync sign/verify throw.\netc.sha512Sync = (...messages: Uint8Array[]): Uint8Array =>\n sha512(etc.concatBytes(...messages))\n\n/**\n * Extract the signable fields from a full receipt object.\n * Maps receipt fields to the SignablePayload structure.\n *\n * NOTE: The API uses `timestamp` while the DB uses `created_at`.\n * This function expects the API format (timestamp).\n */\nexport function getSignablePayload(receipt: {\n receipt_id: string\n chain_id: string\n receipt_type: string\n agent_id: string\n org_id: string\n action: string\n input_hash: string\n output_hash: string | null\n status: string\n timestamp: string\n completed_at: string | null\n environment: string\n [key: string]: unknown\n}): SignablePayload {\n return {\n receipt_id: receipt.receipt_id,\n chain_id: receipt.chain_id,\n receipt_type: receipt.receipt_type as SignablePayload['receipt_type'],\n agent_id: receipt.agent_id,\n org_id: receipt.org_id,\n action: receipt.action,\n input_hash: receipt.input_hash,\n output_hash: receipt.output_hash,\n status: receipt.status as SignablePayload['status'],\n timestamp: receipt.timestamp,\n completed_at: receipt.completed_at,\n environment: receipt.environment as SignablePayload['environment'],\n }\n}\n\n/**\n * Sign a receipt payload with Ed25519.\n *\n * @param payload - The signable fields (12 deterministic fields)\n * @param privateKey - Ed25519 private key as hex string (64 chars = 32 bytes)\n * @returns Signature string in format \"ed25519:<base64>\"\n */\nexport function signReceipt(\n payload: SignablePayload,\n privateKey: string\n): string {\n const canonical = canonicalize(payload)\n const message = new TextEncoder().encode(canonical)\n const privKeyBytes = etc.hexToBytes(privateKey)\n const signature = sign(message, privKeyBytes)\n return `ed25519:${bytesToBase64(signature)}`\n}\n\n/**\n * Verify a receipt signature.\n *\n * @param payload - The signable fields (must match what was signed)\n * @param signature - Signature string in format \"ed25519:<base64>\"\n * @param publicKey - Ed25519 public key as hex string (64 chars = 32 bytes)\n * @returns true if signature is valid\n */\nexport function verifyReceipt(\n payload: SignablePayload,\n signature: string,\n publicKey: string\n): boolean {\n try {\n if (!signature.startsWith('ed25519:')) {\n return false\n }\n const canonical = canonicalize(payload)\n const message = new TextEncoder().encode(canonical)\n const sigBytes = base64ToBytes(signature.slice('ed25519:'.length))\n const pubKeyBytes = etc.hexToBytes(publicKey)\n return verify(sigBytes, message, pubKeyBytes)\n } catch {\n return false\n }\n}\n\nfunction bytesToBase64(bytes: Uint8Array): string {\n return Buffer.from(bytes).toString('base64')\n}\n\nfunction base64ToBytes(b64: string): Uint8Array {\n return new Uint8Array(Buffer.from(b64, 'base64'))\n}\n","import { getPublicKey, etc, utils } from '@noble/ed25519'\n\n/**\n * Generate a new Ed25519 key pair.\n *\n * @returns { privateKey: string, publicKey: string } — both hex-encoded (64 chars each)\n */\nexport function generateKeyPair(): {\n privateKey: string\n publicKey: string\n} {\n const privateKeyBytes = utils.randomPrivateKey()\n const publicKeyBytes = getPublicKey(privateKeyBytes)\n return {\n privateKey: etc.bytesToHex(privateKeyBytes),\n publicKey: etc.bytesToHex(publicKeyBytes),\n }\n}\n\n/**\n * Derive public key from private key.\n *\n * @param privateKeyHex - Ed25519 private key as hex string (64 chars)\n * @returns Public key as hex string (64 chars)\n */\nexport function getPublicKeyFromPrivate(privateKeyHex: string): string {\n const privKeyBytes = etc.hexToBytes(privateKeyHex)\n const pubKeyBytes = getPublicKey(privKeyBytes)\n return etc.bytesToHex(pubKeyBytes)\n}\n","import { sign, verify, etc } from '@noble/ed25519'\nimport { sha256 } from '@noble/hashes/sha256'\nimport { sha512 } from '@noble/hashes/sha512'\nimport { canonicalize } from './canonical'\nimport { getSignablePayload } from './sign'\n\n// noble/ed25519 v2 needs a sync sha512 for sign/verify (also set in sign.ts;\n// repeated here so this module works when imported on its own).\netc.sha512Sync = (...messages: Uint8Array[]): Uint8Array =>\n sha512(etc.concatBytes(...messages))\n\n/** Fields never covered by a v2 signature. */\nexport const V2_UNSIGNED_FIELDS = ['signature', 'verify_url'] as const\n\n/**\n * Deep canonical JSON: keys sorted at every depth, arrays kept in order,\n * `undefined` object values dropped (as JSON.stringify does).\n *\n * The browser verifier (receipt.html, verify page) re-implements exactly this\n * function, so keep the two in sync.\n */\nexport function canonicalizeDeep(value: unknown): string {\n return JSON.stringify(sortDeep(value)) ?? 'null'\n}\n\nfunction sortDeep(value: unknown): unknown {\n if (value === null || typeof value !== 'object') return value\n if (Array.isArray(value)) return value.map(sortDeep)\n // Null-prototype object: a \"__proto__\" key stays an ordinary key (and so is\n // signed) instead of silently becoming the prototype and vanishing.\n const out: Record<string, unknown> = Object.create(null)\n for (const key of Object.keys(value as Record<string, unknown>).sort()) {\n const v = (value as Record<string, unknown>)[key]\n if (v !== undefined) out[key] = sortDeep(v)\n }\n return out\n}\n\n/** Everything in the receipt except the signature and verify_url. */\nexport function getSignablePayloadV2(receipt: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = Object.create(null)\n for (const [key, value] of Object.entries(receipt)) {\n if ((V2_UNSIGNED_FIELDS as readonly string[]).includes(key)) continue\n if (value !== undefined) out[key] = value\n }\n return out\n}\n\n/** Sign a v2 receipt. The receipt must already carry `sig_version: 2`. */\nexport function signReceiptV2(receipt: Record<string, unknown>, privateKey: string): string {\n if (receipt['sig_version'] !== 2) {\n throw new Error('signReceiptV2: receipt.sig_version must be 2')\n }\n const message = new TextEncoder().encode(canonicalizeDeep(getSignablePayloadV2(receipt)))\n const sig = sign(message, etc.hexToBytes(privateKey))\n return `ed25519:${Buffer.from(sig).toString('base64')}`\n}\n\n/** Verify a v2 signature. Returns false for anything that isn't a valid v2 receipt. */\nexport function verifyReceiptV2(receipt: Record<string, unknown>, publicKey: string): boolean {\n try {\n const signature = receipt['signature']\n if (receipt['sig_version'] !== 2 || typeof signature !== 'string') return false\n if (!signature.startsWith('ed25519:')) return false\n const message = new TextEncoder().encode(canonicalizeDeep(getSignablePayloadV2(receipt)))\n const sigBytes = new Uint8Array(Buffer.from(signature.slice('ed25519:'.length), 'base64'))\n return verify(sigBytes, message, etc.hexToBytes(publicKey))\n } catch {\n return false\n }\n}\n\nexport interface VerifyResult {\n valid: boolean\n sig_version: 1 | 2\n /** True when the signature covers every field (v2). v1 covers 12 fields only. */\n covers_all_fields: boolean\n key_id_matches: boolean | null\n}\n\n/**\n * Verify any receipt, v1 or v2.\n *\n * A v1 receipt only signs 12 core fields: its summary, cost, files and\n * metadata can be edited without breaking the signature. Verifiers that show\n * those fields to people must pass `{ requireV2: true }` (stripping\n * `sig_version` from a v2 receipt does not help an attacker: v1 signs\n * different bytes, so it fails either way).\n */\nexport function verifyAnyReceipt(\n receipt: Record<string, unknown>,\n publicKey: string,\n options: { requireV2?: boolean } = {},\n): VerifyResult {\n if (receipt['sig_version'] === 2) {\n const keyId = receipt['key_id']\n return {\n valid: verifyReceiptV2(receipt, publicKey),\n sig_version: 2,\n covers_all_fields: true,\n key_id_matches: typeof keyId === 'string' ? keyId === computeKeyId(publicKey) : null,\n }\n }\n if (options.requireV2) {\n return { valid: false, sig_version: 1, covers_all_fields: false, key_id_matches: null }\n }\n let valid = false\n try {\n const signable = getSignablePayload(receipt as Parameters<typeof getSignablePayload>[0])\n const signature = String(receipt['signature'] ?? '')\n if (signature.startsWith('ed25519:')) {\n const message = new TextEncoder().encode(canonicalize(signable))\n const sigBytes = new Uint8Array(Buffer.from(signature.slice('ed25519:'.length), 'base64'))\n valid = verify(sigBytes, message, etc.hexToBytes(publicKey))\n }\n } catch {\n valid = false\n }\n return { valid, sig_version: 1, covers_all_fields: false, key_id_matches: null }\n}\n\n/** Short, stable name for a public key: `key_` + first 16 hex of sha256(raw key bytes). */\nexport function computeKeyId(publicKeyHex: string): string {\n return `key_${etc.bytesToHex(sha256(etc.hexToBytes(publicKeyHex))).slice(0, 16)}`\n}\n\n/** Hash of a whole stored receipt (signature included) — what the next receipt in a chain links to. */\nexport function hashReceipt(receipt: Record<string, unknown>): string {\n const bytes = new TextEncoder().encode(canonicalizeDeep(receipt))\n return `sha256:${etc.bytesToHex(sha256(bytes))}`\n}\n"],"mappings":"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","names":["import_ed25519","import_ed25519","import_sha512"]}
|
package/dist/index.mjs
CHANGED
|
@@ -67,12 +67,102 @@ function getPublicKeyFromPrivate(privateKeyHex) {
|
|
|
67
67
|
const pubKeyBytes = getPublicKey2(privKeyBytes);
|
|
68
68
|
return etc2.bytesToHex(pubKeyBytes);
|
|
69
69
|
}
|
|
70
|
+
|
|
71
|
+
// src/v2.ts
|
|
72
|
+
import { sign as sign2, verify as verify2, etc as etc3 } from "@noble/ed25519";
|
|
73
|
+
import { sha256 } from "@noble/hashes/sha256";
|
|
74
|
+
import { sha512 as sha5122 } from "@noble/hashes/sha512";
|
|
75
|
+
etc3.sha512Sync = (...messages) => sha5122(etc3.concatBytes(...messages));
|
|
76
|
+
var V2_UNSIGNED_FIELDS = ["signature", "verify_url"];
|
|
77
|
+
function canonicalizeDeep(value) {
|
|
78
|
+
return JSON.stringify(sortDeep(value)) ?? "null";
|
|
79
|
+
}
|
|
80
|
+
function sortDeep(value) {
|
|
81
|
+
if (value === null || typeof value !== "object") return value;
|
|
82
|
+
if (Array.isArray(value)) return value.map(sortDeep);
|
|
83
|
+
const out = /* @__PURE__ */ Object.create(null);
|
|
84
|
+
for (const key of Object.keys(value).sort()) {
|
|
85
|
+
const v = value[key];
|
|
86
|
+
if (v !== void 0) out[key] = sortDeep(v);
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
function getSignablePayloadV2(receipt) {
|
|
91
|
+
const out = /* @__PURE__ */ Object.create(null);
|
|
92
|
+
for (const [key, value] of Object.entries(receipt)) {
|
|
93
|
+
if (V2_UNSIGNED_FIELDS.includes(key)) continue;
|
|
94
|
+
if (value !== void 0) out[key] = value;
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
function signReceiptV2(receipt, privateKey) {
|
|
99
|
+
if (receipt["sig_version"] !== 2) {
|
|
100
|
+
throw new Error("signReceiptV2: receipt.sig_version must be 2");
|
|
101
|
+
}
|
|
102
|
+
const message = new TextEncoder().encode(canonicalizeDeep(getSignablePayloadV2(receipt)));
|
|
103
|
+
const sig = sign2(message, etc3.hexToBytes(privateKey));
|
|
104
|
+
return `ed25519:${Buffer.from(sig).toString("base64")}`;
|
|
105
|
+
}
|
|
106
|
+
function verifyReceiptV2(receipt, publicKey) {
|
|
107
|
+
try {
|
|
108
|
+
const signature = receipt["signature"];
|
|
109
|
+
if (receipt["sig_version"] !== 2 || typeof signature !== "string") return false;
|
|
110
|
+
if (!signature.startsWith("ed25519:")) return false;
|
|
111
|
+
const message = new TextEncoder().encode(canonicalizeDeep(getSignablePayloadV2(receipt)));
|
|
112
|
+
const sigBytes = new Uint8Array(Buffer.from(signature.slice("ed25519:".length), "base64"));
|
|
113
|
+
return verify2(sigBytes, message, etc3.hexToBytes(publicKey));
|
|
114
|
+
} catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function verifyAnyReceipt(receipt, publicKey, options = {}) {
|
|
119
|
+
if (receipt["sig_version"] === 2) {
|
|
120
|
+
const keyId = receipt["key_id"];
|
|
121
|
+
return {
|
|
122
|
+
valid: verifyReceiptV2(receipt, publicKey),
|
|
123
|
+
sig_version: 2,
|
|
124
|
+
covers_all_fields: true,
|
|
125
|
+
key_id_matches: typeof keyId === "string" ? keyId === computeKeyId(publicKey) : null
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (options.requireV2) {
|
|
129
|
+
return { valid: false, sig_version: 1, covers_all_fields: false, key_id_matches: null };
|
|
130
|
+
}
|
|
131
|
+
let valid = false;
|
|
132
|
+
try {
|
|
133
|
+
const signable = getSignablePayload(receipt);
|
|
134
|
+
const signature = String(receipt["signature"] ?? "");
|
|
135
|
+
if (signature.startsWith("ed25519:")) {
|
|
136
|
+
const message = new TextEncoder().encode(canonicalize(signable));
|
|
137
|
+
const sigBytes = new Uint8Array(Buffer.from(signature.slice("ed25519:".length), "base64"));
|
|
138
|
+
valid = verify2(sigBytes, message, etc3.hexToBytes(publicKey));
|
|
139
|
+
}
|
|
140
|
+
} catch {
|
|
141
|
+
valid = false;
|
|
142
|
+
}
|
|
143
|
+
return { valid, sig_version: 1, covers_all_fields: false, key_id_matches: null };
|
|
144
|
+
}
|
|
145
|
+
function computeKeyId(publicKeyHex) {
|
|
146
|
+
return `key_${etc3.bytesToHex(sha256(etc3.hexToBytes(publicKeyHex))).slice(0, 16)}`;
|
|
147
|
+
}
|
|
148
|
+
function hashReceipt(receipt) {
|
|
149
|
+
const bytes = new TextEncoder().encode(canonicalizeDeep(receipt));
|
|
150
|
+
return `sha256:${etc3.bytesToHex(sha256(bytes))}`;
|
|
151
|
+
}
|
|
70
152
|
export {
|
|
153
|
+
V2_UNSIGNED_FIELDS,
|
|
71
154
|
canonicalize,
|
|
155
|
+
canonicalizeDeep,
|
|
156
|
+
computeKeyId,
|
|
72
157
|
generateKeyPair,
|
|
73
158
|
getPublicKeyFromPrivate,
|
|
74
159
|
getSignablePayload,
|
|
160
|
+
getSignablePayloadV2,
|
|
161
|
+
hashReceipt,
|
|
75
162
|
signReceipt,
|
|
76
|
-
|
|
163
|
+
signReceiptV2,
|
|
164
|
+
verifyAnyReceipt,
|
|
165
|
+
verifyReceipt,
|
|
166
|
+
verifyReceiptV2
|
|
77
167
|
};
|
|
78
168
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/canonical.ts","../src/sign.ts","../src/keys.ts"],"sourcesContent":["import type { SignablePayload } from '@agent-receipts/schema'\n\n/**\n * Produce a canonical JSON string from a SignablePayload.\n * Keys are sorted alphabetically. Output is deterministic.\n *\n * IMPORTANT: SignablePayload contains only flat primitives (strings, nulls).\n * No nested objects or arrays. Simple key sorting is sufficient here.\n *\n * For hashing arbitrary user input/output (in the SDK), use deep canonical\n * sorting — that's a different function in the SDK package.\n */\nexport function canonicalize(payload: SignablePayload): string {\n const sorted = Object.keys(payload).sort()\n return JSON.stringify(payload, sorted)\n}\n","import { sign, verify, etc, getPublicKey } from '@noble/ed25519'\nimport { sha512 } from '@noble/hashes/sha512'\nimport type { SignablePayload } from '@agent-receipts/schema'\nimport { canonicalize } from './canonical'\n\n// Configure noble/ed25519 to use sha512 for sync operations.\n// This is required by @noble/ed25519 v2 — without it, sync sign/verify throw.\netc.sha512Sync = (...messages: Uint8Array[]): Uint8Array =>\n sha512(etc.concatBytes(...messages))\n\n/**\n * Extract the signable fields from a full receipt object.\n * Maps receipt fields to the SignablePayload structure.\n *\n * NOTE: The API uses `timestamp` while the DB uses `created_at`.\n * This function expects the API format (timestamp).\n */\nexport function getSignablePayload(receipt: {\n receipt_id: string\n chain_id: string\n receipt_type: string\n agent_id: string\n org_id: string\n action: string\n input_hash: string\n output_hash: string | null\n status: string\n timestamp: string\n completed_at: string | null\n environment: string\n [key: string]: unknown\n}): SignablePayload {\n return {\n receipt_id: receipt.receipt_id,\n chain_id: receipt.chain_id,\n receipt_type: receipt.receipt_type as SignablePayload['receipt_type'],\n agent_id: receipt.agent_id,\n org_id: receipt.org_id,\n action: receipt.action,\n input_hash: receipt.input_hash,\n output_hash: receipt.output_hash,\n status: receipt.status as SignablePayload['status'],\n timestamp: receipt.timestamp,\n completed_at: receipt.completed_at,\n environment: receipt.environment as SignablePayload['environment'],\n }\n}\n\n/**\n * Sign a receipt payload with Ed25519.\n *\n * @param payload - The signable fields (12 deterministic fields)\n * @param privateKey - Ed25519 private key as hex string (64 chars = 32 bytes)\n * @returns Signature string in format \"ed25519:<base64>\"\n */\nexport function signReceipt(\n payload: SignablePayload,\n privateKey: string\n): string {\n const canonical = canonicalize(payload)\n const message = new TextEncoder().encode(canonical)\n const privKeyBytes = etc.hexToBytes(privateKey)\n const signature = sign(message, privKeyBytes)\n return `ed25519:${bytesToBase64(signature)}`\n}\n\n/**\n * Verify a receipt signature.\n *\n * @param payload - The signable fields (must match what was signed)\n * @param signature - Signature string in format \"ed25519:<base64>\"\n * @param publicKey - Ed25519 public key as hex string (64 chars = 32 bytes)\n * @returns true if signature is valid\n */\nexport function verifyReceipt(\n payload: SignablePayload,\n signature: string,\n publicKey: string\n): boolean {\n try {\n if (!signature.startsWith('ed25519:')) {\n return false\n }\n const canonical = canonicalize(payload)\n const message = new TextEncoder().encode(canonical)\n const sigBytes = base64ToBytes(signature.slice('ed25519:'.length))\n const pubKeyBytes = etc.hexToBytes(publicKey)\n return verify(sigBytes, message, pubKeyBytes)\n } catch {\n return false\n }\n}\n\nfunction bytesToBase64(bytes: Uint8Array): string {\n return Buffer.from(bytes).toString('base64')\n}\n\nfunction base64ToBytes(b64: string): Uint8Array {\n return new Uint8Array(Buffer.from(b64, 'base64'))\n}\n","import { getPublicKey, etc, utils } from '@noble/ed25519'\n\n/**\n * Generate a new Ed25519 key pair.\n *\n * @returns { privateKey: string, publicKey: string } — both hex-encoded (64 chars each)\n */\nexport function generateKeyPair(): {\n privateKey: string\n publicKey: string\n} {\n const privateKeyBytes = utils.randomPrivateKey()\n const publicKeyBytes = getPublicKey(privateKeyBytes)\n return {\n privateKey: etc.bytesToHex(privateKeyBytes),\n publicKey: etc.bytesToHex(publicKeyBytes),\n }\n}\n\n/**\n * Derive public key from private key.\n *\n * @param privateKeyHex - Ed25519 private key as hex string (64 chars)\n * @returns Public key as hex string (64 chars)\n */\nexport function getPublicKeyFromPrivate(privateKeyHex: string): string {\n const privKeyBytes = etc.hexToBytes(privateKeyHex)\n const pubKeyBytes = getPublicKey(privKeyBytes)\n return etc.bytesToHex(pubKeyBytes)\n}\n"],"mappings":";AAYO,SAAS,aAAa,SAAkC;AAC7D,QAAM,SAAS,OAAO,KAAK,OAAO,EAAE,KAAK;AACzC,SAAO,KAAK,UAAU,SAAS,MAAM;AACvC;;;ACfA,SAAS,MAAM,QAAQ,WAAyB;AAChD,SAAS,cAAc;AAMvB,IAAI,aAAa,IAAI,aACnB,OAAO,IAAI,YAAY,GAAG,QAAQ,CAAC;AAS9B,SAAS,mBAAmB,SAcf;AAClB,SAAO;AAAA,IACL,YAAY,QAAQ;AAAA,IACpB,UAAU,QAAQ;AAAA,IAClB,cAAc,QAAQ;AAAA,IACtB,UAAU,QAAQ;AAAA,IAClB,QAAQ,QAAQ;AAAA,IAChB,QAAQ,QAAQ;AAAA,IAChB,YAAY,QAAQ;AAAA,IACpB,aAAa,QAAQ;AAAA,IACrB,QAAQ,QAAQ;AAAA,IAChB,WAAW,QAAQ;AAAA,IACnB,cAAc,QAAQ;AAAA,IACtB,aAAa,QAAQ;AAAA,EACvB;AACF;AASO,SAAS,YACd,SACA,YACQ;AACR,QAAM,YAAY,aAAa,OAAO;AACtC,QAAM,UAAU,IAAI,YAAY,EAAE,OAAO,SAAS;AAClD,QAAM,eAAe,IAAI,WAAW,UAAU;AAC9C,QAAM,YAAY,KAAK,SAAS,YAAY;AAC5C,SAAO,WAAW,cAAc,SAAS,CAAC;AAC5C;AAUO,SAAS,cACd,SACA,WACA,WACS;AACT,MAAI;AACF,QAAI,CAAC,UAAU,WAAW,UAAU,GAAG;AACrC,aAAO;AAAA,IACT;AACA,UAAM,YAAY,aAAa,OAAO;AACtC,UAAM,UAAU,IAAI,YAAY,EAAE,OAAO,SAAS;AAClD,UAAM,WAAW,cAAc,UAAU,MAAM,WAAW,MAAM,CAAC;AACjE,UAAM,cAAc,IAAI,WAAW,SAAS;AAC5C,WAAO,OAAO,UAAU,SAAS,WAAW;AAAA,EAC9C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,cAAc,OAA2B;AAChD,SAAO,OAAO,KAAK,KAAK,EAAE,SAAS,QAAQ;AAC7C;AAEA,SAAS,cAAc,KAAyB;AAC9C,SAAO,IAAI,WAAW,OAAO,KAAK,KAAK,QAAQ,CAAC;AAClD;;;ACnGA,SAAS,gBAAAA,eAAc,OAAAC,MAAK,aAAa;AAOlC,SAAS,kBAGd;AACA,QAAM,kBAAkB,MAAM,iBAAiB;AAC/C,QAAM,iBAAiBD,cAAa,eAAe;AACnD,SAAO;AAAA,IACL,YAAYC,KAAI,WAAW,eAAe;AAAA,IAC1C,WAAWA,KAAI,WAAW,cAAc;AAAA,EAC1C;AACF;AAQO,SAAS,wBAAwB,eAA+B;AACrE,QAAM,eAAeA,KAAI,WAAW,aAAa;AACjD,QAAM,cAAcD,cAAa,YAAY;AAC7C,SAAOC,KAAI,WAAW,WAAW;AACnC;","names":["getPublicKey","etc"]}
|
|
1
|
+
{"version":3,"sources":["../src/canonical.ts","../src/sign.ts","../src/keys.ts","../src/v2.ts"],"sourcesContent":["import type { SignablePayload } from '@agent-receipts/schema'\n\n/**\n * Produce a canonical JSON string from a SignablePayload.\n * Keys are sorted alphabetically. Output is deterministic.\n *\n * IMPORTANT: SignablePayload contains only flat primitives (strings, nulls).\n * No nested objects or arrays. Simple key sorting is sufficient here.\n *\n * For hashing arbitrary user input/output (in the SDK), use deep canonical\n * sorting — that's a different function in the SDK package.\n */\nexport function canonicalize(payload: SignablePayload): string {\n const sorted = Object.keys(payload).sort()\n return JSON.stringify(payload, sorted)\n}\n","import { sign, verify, etc, getPublicKey } from '@noble/ed25519'\nimport { sha512 } from '@noble/hashes/sha512'\nimport type { SignablePayload } from '@agent-receipts/schema'\nimport { canonicalize } from './canonical'\n\n// Configure noble/ed25519 to use sha512 for sync operations.\n// This is required by @noble/ed25519 v2 — without it, sync sign/verify throw.\netc.sha512Sync = (...messages: Uint8Array[]): Uint8Array =>\n sha512(etc.concatBytes(...messages))\n\n/**\n * Extract the signable fields from a full receipt object.\n * Maps receipt fields to the SignablePayload structure.\n *\n * NOTE: The API uses `timestamp` while the DB uses `created_at`.\n * This function expects the API format (timestamp).\n */\nexport function getSignablePayload(receipt: {\n receipt_id: string\n chain_id: string\n receipt_type: string\n agent_id: string\n org_id: string\n action: string\n input_hash: string\n output_hash: string | null\n status: string\n timestamp: string\n completed_at: string | null\n environment: string\n [key: string]: unknown\n}): SignablePayload {\n return {\n receipt_id: receipt.receipt_id,\n chain_id: receipt.chain_id,\n receipt_type: receipt.receipt_type as SignablePayload['receipt_type'],\n agent_id: receipt.agent_id,\n org_id: receipt.org_id,\n action: receipt.action,\n input_hash: receipt.input_hash,\n output_hash: receipt.output_hash,\n status: receipt.status as SignablePayload['status'],\n timestamp: receipt.timestamp,\n completed_at: receipt.completed_at,\n environment: receipt.environment as SignablePayload['environment'],\n }\n}\n\n/**\n * Sign a receipt payload with Ed25519.\n *\n * @param payload - The signable fields (12 deterministic fields)\n * @param privateKey - Ed25519 private key as hex string (64 chars = 32 bytes)\n * @returns Signature string in format \"ed25519:<base64>\"\n */\nexport function signReceipt(\n payload: SignablePayload,\n privateKey: string\n): string {\n const canonical = canonicalize(payload)\n const message = new TextEncoder().encode(canonical)\n const privKeyBytes = etc.hexToBytes(privateKey)\n const signature = sign(message, privKeyBytes)\n return `ed25519:${bytesToBase64(signature)}`\n}\n\n/**\n * Verify a receipt signature.\n *\n * @param payload - The signable fields (must match what was signed)\n * @param signature - Signature string in format \"ed25519:<base64>\"\n * @param publicKey - Ed25519 public key as hex string (64 chars = 32 bytes)\n * @returns true if signature is valid\n */\nexport function verifyReceipt(\n payload: SignablePayload,\n signature: string,\n publicKey: string\n): boolean {\n try {\n if (!signature.startsWith('ed25519:')) {\n return false\n }\n const canonical = canonicalize(payload)\n const message = new TextEncoder().encode(canonical)\n const sigBytes = base64ToBytes(signature.slice('ed25519:'.length))\n const pubKeyBytes = etc.hexToBytes(publicKey)\n return verify(sigBytes, message, pubKeyBytes)\n } catch {\n return false\n }\n}\n\nfunction bytesToBase64(bytes: Uint8Array): string {\n return Buffer.from(bytes).toString('base64')\n}\n\nfunction base64ToBytes(b64: string): Uint8Array {\n return new Uint8Array(Buffer.from(b64, 'base64'))\n}\n","import { getPublicKey, etc, utils } from '@noble/ed25519'\n\n/**\n * Generate a new Ed25519 key pair.\n *\n * @returns { privateKey: string, publicKey: string } — both hex-encoded (64 chars each)\n */\nexport function generateKeyPair(): {\n privateKey: string\n publicKey: string\n} {\n const privateKeyBytes = utils.randomPrivateKey()\n const publicKeyBytes = getPublicKey(privateKeyBytes)\n return {\n privateKey: etc.bytesToHex(privateKeyBytes),\n publicKey: etc.bytesToHex(publicKeyBytes),\n }\n}\n\n/**\n * Derive public key from private key.\n *\n * @param privateKeyHex - Ed25519 private key as hex string (64 chars)\n * @returns Public key as hex string (64 chars)\n */\nexport function getPublicKeyFromPrivate(privateKeyHex: string): string {\n const privKeyBytes = etc.hexToBytes(privateKeyHex)\n const pubKeyBytes = getPublicKey(privKeyBytes)\n return etc.bytesToHex(pubKeyBytes)\n}\n","import { sign, verify, etc } from '@noble/ed25519'\nimport { sha256 } from '@noble/hashes/sha256'\nimport { sha512 } from '@noble/hashes/sha512'\nimport { canonicalize } from './canonical'\nimport { getSignablePayload } from './sign'\n\n// noble/ed25519 v2 needs a sync sha512 for sign/verify (also set in sign.ts;\n// repeated here so this module works when imported on its own).\netc.sha512Sync = (...messages: Uint8Array[]): Uint8Array =>\n sha512(etc.concatBytes(...messages))\n\n/** Fields never covered by a v2 signature. */\nexport const V2_UNSIGNED_FIELDS = ['signature', 'verify_url'] as const\n\n/**\n * Deep canonical JSON: keys sorted at every depth, arrays kept in order,\n * `undefined` object values dropped (as JSON.stringify does).\n *\n * The browser verifier (receipt.html, verify page) re-implements exactly this\n * function, so keep the two in sync.\n */\nexport function canonicalizeDeep(value: unknown): string {\n return JSON.stringify(sortDeep(value)) ?? 'null'\n}\n\nfunction sortDeep(value: unknown): unknown {\n if (value === null || typeof value !== 'object') return value\n if (Array.isArray(value)) return value.map(sortDeep)\n // Null-prototype object: a \"__proto__\" key stays an ordinary key (and so is\n // signed) instead of silently becoming the prototype and vanishing.\n const out: Record<string, unknown> = Object.create(null)\n for (const key of Object.keys(value as Record<string, unknown>).sort()) {\n const v = (value as Record<string, unknown>)[key]\n if (v !== undefined) out[key] = sortDeep(v)\n }\n return out\n}\n\n/** Everything in the receipt except the signature and verify_url. */\nexport function getSignablePayloadV2(receipt: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = Object.create(null)\n for (const [key, value] of Object.entries(receipt)) {\n if ((V2_UNSIGNED_FIELDS as readonly string[]).includes(key)) continue\n if (value !== undefined) out[key] = value\n }\n return out\n}\n\n/** Sign a v2 receipt. The receipt must already carry `sig_version: 2`. */\nexport function signReceiptV2(receipt: Record<string, unknown>, privateKey: string): string {\n if (receipt['sig_version'] !== 2) {\n throw new Error('signReceiptV2: receipt.sig_version must be 2')\n }\n const message = new TextEncoder().encode(canonicalizeDeep(getSignablePayloadV2(receipt)))\n const sig = sign(message, etc.hexToBytes(privateKey))\n return `ed25519:${Buffer.from(sig).toString('base64')}`\n}\n\n/** Verify a v2 signature. Returns false for anything that isn't a valid v2 receipt. */\nexport function verifyReceiptV2(receipt: Record<string, unknown>, publicKey: string): boolean {\n try {\n const signature = receipt['signature']\n if (receipt['sig_version'] !== 2 || typeof signature !== 'string') return false\n if (!signature.startsWith('ed25519:')) return false\n const message = new TextEncoder().encode(canonicalizeDeep(getSignablePayloadV2(receipt)))\n const sigBytes = new Uint8Array(Buffer.from(signature.slice('ed25519:'.length), 'base64'))\n return verify(sigBytes, message, etc.hexToBytes(publicKey))\n } catch {\n return false\n }\n}\n\nexport interface VerifyResult {\n valid: boolean\n sig_version: 1 | 2\n /** True when the signature covers every field (v2). v1 covers 12 fields only. */\n covers_all_fields: boolean\n key_id_matches: boolean | null\n}\n\n/**\n * Verify any receipt, v1 or v2.\n *\n * A v1 receipt only signs 12 core fields: its summary, cost, files and\n * metadata can be edited without breaking the signature. Verifiers that show\n * those fields to people must pass `{ requireV2: true }` (stripping\n * `sig_version` from a v2 receipt does not help an attacker: v1 signs\n * different bytes, so it fails either way).\n */\nexport function verifyAnyReceipt(\n receipt: Record<string, unknown>,\n publicKey: string,\n options: { requireV2?: boolean } = {},\n): VerifyResult {\n if (receipt['sig_version'] === 2) {\n const keyId = receipt['key_id']\n return {\n valid: verifyReceiptV2(receipt, publicKey),\n sig_version: 2,\n covers_all_fields: true,\n key_id_matches: typeof keyId === 'string' ? keyId === computeKeyId(publicKey) : null,\n }\n }\n if (options.requireV2) {\n return { valid: false, sig_version: 1, covers_all_fields: false, key_id_matches: null }\n }\n let valid = false\n try {\n const signable = getSignablePayload(receipt as Parameters<typeof getSignablePayload>[0])\n const signature = String(receipt['signature'] ?? '')\n if (signature.startsWith('ed25519:')) {\n const message = new TextEncoder().encode(canonicalize(signable))\n const sigBytes = new Uint8Array(Buffer.from(signature.slice('ed25519:'.length), 'base64'))\n valid = verify(sigBytes, message, etc.hexToBytes(publicKey))\n }\n } catch {\n valid = false\n }\n return { valid, sig_version: 1, covers_all_fields: false, key_id_matches: null }\n}\n\n/** Short, stable name for a public key: `key_` + first 16 hex of sha256(raw key bytes). */\nexport function computeKeyId(publicKeyHex: string): string {\n return `key_${etc.bytesToHex(sha256(etc.hexToBytes(publicKeyHex))).slice(0, 16)}`\n}\n\n/** Hash of a whole stored receipt (signature included) — what the next receipt in a chain links to. */\nexport function hashReceipt(receipt: Record<string, unknown>): string {\n const bytes = new TextEncoder().encode(canonicalizeDeep(receipt))\n return `sha256:${etc.bytesToHex(sha256(bytes))}`\n}\n"],"mappings":"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","names":["getPublicKey","etc","sign","verify","etc","sha512","etc","sha512","sign","verify"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-receipts/crypto",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Ed25519 signing and verification for the Action Receipt Protocol",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Amin <amin@webaes.co> (https://webaes.co)",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@noble/ed25519": "^2.1.0",
|
|
47
47
|
"@noble/hashes": "^1.7.0",
|
|
48
|
-
"@agent-receipts/schema": "0.
|
|
48
|
+
"@agent-receipts/schema": "0.5.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/node": "^20.14.0",
|