@forestrie/forestrie-cli 0.9.1 → 0.11.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 +49 -3
- package/dist/cli.js +3515 -590
- package/dist/lib/decode-receipt-labels.d.ts +7 -6
- package/dist/lib/decode-receipt-labels.d.ts.map +1 -1
- package/dist/lib/decode-receipt-labels.js +11 -10
- package/dist/lib/decode-receipt-labels.js.map +1 -1
- package/package.json +5 -5
- package/src/command.ts +2 -0
- package/src/commands/fetch-ks256-attestation.ts +52 -0
- package/src/commands/verify-grant.ts +8 -2
- package/src/commands/verify.ts +8 -2
- package/src/lib/checkpoint-provider.ts +73 -30
- package/src/lib/complete-grant-derive.ts +1 -0
- package/src/lib/create-log-prepare.ts +3 -1
- package/src/lib/create-receipt-derive.ts +16 -0
- package/src/lib/decode-receipt-labels.ts +11 -10
- package/src/lib/delegate-flow.ts +4 -2
- package/src/lib/http-problem.ts +53 -0
- package/src/lib/known-log-key.ts +35 -0
- package/src/lib/resolve-receipt-freshen.ts +9 -11
- package/src/lib/verify-anchored.ts +1 -1
- package/src/lib/verify-checkpoint-chain.ts +2 -2
- package/src/lib/verify-eventscan.ts +2 -2
- package/src/lib/verify-inputs.ts +1 -1
- package/src/lib/verify-report.ts +78 -0
- package/src/main/fetch-ks256-attestation.ts +245 -0
- package/src/main/onboard-genesis.ts +3 -1
- package/src/main/onboard-request.ts +4 -2
- package/src/main/verify.ts +169 -60
- package/src/options/fetch-ks256-attestation.ts +60 -0
- package/src/options/verify.ts +34 -20
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
calldataCheckpointChain,
|
|
40
40
|
sthCheckpointChain,
|
|
41
41
|
type CheckpointLink,
|
|
42
|
+
singleProofChain,
|
|
42
43
|
} from "./checkpoint-provider.js";
|
|
43
44
|
import { univocityLeafHash } from "./verify-anchored.js";
|
|
44
45
|
import { bytesEqual } from "./bytes.js";
|
|
@@ -102,16 +103,11 @@ async function emitFreshened(opts: {
|
|
|
102
103
|
const result = await freshenReceipt({
|
|
103
104
|
oldReceiptBytes: opts.oldReceiptBytes,
|
|
104
105
|
leafValue: opts.leafValue,
|
|
105
|
-
// `freshenReceipt`
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
// signed size.
|
|
111
|
-
consistencyProofs: opts.links.map((l) => ({
|
|
112
|
-
...l.proof,
|
|
113
|
-
signedTreeSize2: l.proof.treeSize2,
|
|
114
|
-
})),
|
|
106
|
+
// `freshenReceipt` takes chains; each link is one step, wrapped as a
|
|
107
|
+
// one-proof chain — see `singleProofChain` in checkpoint-provider.ts.
|
|
108
|
+
consistencyProofs: opts.links.map((l) =>
|
|
109
|
+
singleProofChain(l.proof, l.signedTreeSize2),
|
|
110
|
+
),
|
|
115
111
|
latestCheckpointBytes: opts.latestCheckpointBytes,
|
|
116
112
|
});
|
|
117
113
|
|
|
@@ -177,7 +173,9 @@ export async function freshenFromSthChain(opts: {
|
|
|
177
173
|
leafValue,
|
|
178
174
|
links,
|
|
179
175
|
latestCheckpointBytes: opts.checkpoints[opts.checkpoints.length - 1]!,
|
|
180
|
-
|
|
176
|
+
// One entry per LINK, as the calldata path reports: a checkpoint that
|
|
177
|
+
// relays several proofs yields several links that all name it.
|
|
178
|
+
sourceRefs: links.map((l) => l.sourceRef ?? ""),
|
|
181
179
|
knownAccumulator: opts.knownAccumulator,
|
|
182
180
|
});
|
|
183
181
|
}
|
|
@@ -59,7 +59,7 @@ export type AnchorCheck = OnChainLogState & {
|
|
|
59
59
|
* - the leaf is at/after the anchored size — the entry simply is not
|
|
60
60
|
* anchored yet (`receipt_newer_than_anchored_state`).
|
|
61
61
|
* Tampered receipts surface earlier (signature/inclusion) or as a
|
|
62
|
-
* recomputed peak that matches no anchored state ever; the chain
|
|
62
|
+
* recomputed peak that matches no anchored state ever; the chain roots
|
|
63
63
|
* that PROVE the buried case land in later plan phases.
|
|
64
64
|
*/
|
|
65
65
|
export function classifyUnanchoredPeak(
|
|
@@ -15,7 +15,7 @@ import type { AnchorCheck } from "./verify-anchored.js";
|
|
|
15
15
|
* (ADR-0056: every checkpoint's embedded consistency proof spans its
|
|
16
16
|
* massif's entry boundary to its seal). `@forestrie/receipt-verify`'s
|
|
17
17
|
* `verifyCheckpointChain` folds that chain from size 0, authenticating the
|
|
18
|
-
* accumulator at EVERY retained seal — this
|
|
18
|
+
* accumulator at EVERY retained seal — this root depends only on the
|
|
19
19
|
* public log store: no tiles, no RPC, no holder cache. It is the
|
|
20
20
|
* complement of the `CheckpointPublished` event scan (public chain data
|
|
21
21
|
* only); the two paths are independently sufficient and cross-checkable.
|
|
@@ -109,7 +109,7 @@ export type CheckpointChainAnchorCheck = AnchorCheck & {
|
|
|
109
109
|
|
|
110
110
|
/**
|
|
111
111
|
* Find the recomputed receipt peak in the authenticated chain. Newest-first
|
|
112
|
-
* (mirrors the event-scan
|
|
112
|
+
* (mirrors the event-scan route): the freshest cover gives the most useful
|
|
113
113
|
* report, and a match at ANY link is proof — later links' signed
|
|
114
114
|
* consistency proofs commit it forward. Newer-than-chain fails CLOSED with
|
|
115
115
|
* a refresh remedy: retention limits coverage, never validity.
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* record of those accumulators (`CheckpointPublished` carries `size` and
|
|
12
12
|
* `accumulator`; the contract stores nothing retrievable historically).
|
|
13
13
|
*
|
|
14
|
-
* This
|
|
15
|
-
* independent of the log store (the retained-checkpoint chain
|
|
14
|
+
* This route depends only on public blockchain data — deliberately
|
|
15
|
+
* independent of the log store (the retained-checkpoint chain root is its
|
|
16
16
|
* complement with the opposite dependency; see plan-2607-29).
|
|
17
17
|
*/
|
|
18
18
|
import { bytesEqual } from "./bytes.js";
|
package/src/lib/verify-inputs.ts
CHANGED
|
@@ -64,7 +64,7 @@ export type LoadedVerifyArtifacts = Omit<
|
|
|
64
64
|
"genesisCbor"
|
|
65
65
|
> & { genesisCbor: Uint8Array | undefined };
|
|
66
66
|
|
|
67
|
-
function readBytes(path: string, flag: string): Uint8Array {
|
|
67
|
+
export function readBytes(path: string, flag: string): Uint8Array {
|
|
68
68
|
try {
|
|
69
69
|
return new Uint8Array(readFileSync(path));
|
|
70
70
|
} catch (err) {
|
package/src/lib/verify-report.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
ReceiptVerifyResult,
|
|
3
3
|
ReceiptVerifyStage,
|
|
4
4
|
} from "@forestrie/receipt-verify";
|
|
5
|
+
import type { Ks256Attestation } from "@forestrie/receipt-verify";
|
|
5
6
|
import type { AnchorCheck } from "./verify-anchored.js";
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -110,6 +111,46 @@ export function knownKeyStageRows(result: ReceiptVerifyResult): StageRow[] {
|
|
|
110
111
|
});
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
/**
|
|
115
|
+
* KS256 root facts for the report: whether the root is a KS256 address, how
|
|
116
|
+
* it was supplied, and any attestation given (with whether the certificate
|
|
117
|
+
* needed it, i.e. did not verify by ecrecover alone).
|
|
118
|
+
*/
|
|
119
|
+
export type Ks256ReportInfo = {
|
|
120
|
+
ks256Root: boolean;
|
|
121
|
+
knownKey: boolean;
|
|
122
|
+
attestation?: Ks256Attestation;
|
|
123
|
+
attestationUsed?: boolean;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
/** Reword the parse/signature rows for a KS256 root or an attestation. */
|
|
127
|
+
export function applyKs256Rows(
|
|
128
|
+
rows: StageRow[],
|
|
129
|
+
info: Ks256ReportInfo,
|
|
130
|
+
): StageRow[] {
|
|
131
|
+
const att = info.attestation;
|
|
132
|
+
return rows.map((row) => {
|
|
133
|
+
if (row.status !== "ok") return row;
|
|
134
|
+
if (row.stage === "parse" && info.ks256Root) {
|
|
135
|
+
return {
|
|
136
|
+
...row,
|
|
137
|
+
reason: `receipt COSE decodes; ${info.knownKey ? "caller-known log key" : "genesis trust root"} loads (KS256 address)`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
if (row.stage === "signature" && info.ks256Root) {
|
|
141
|
+
const anchorName = info.knownKey ? "caller-known log key" : "genesis root";
|
|
142
|
+
return {
|
|
143
|
+
...row,
|
|
144
|
+
reason:
|
|
145
|
+
att !== undefined && info.attestationUsed === true
|
|
146
|
+
? `delegation certificate valid under the contract's signers at block ${att.blockNumber} (ERC-1271 attestation; chain ${att.chainId}) — not a signature by one key`
|
|
147
|
+
: `delegation certificate recovers to the KS256 ${anchorName} (ecrecover)`,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return row;
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
113
154
|
export function stageRows(result: ReceiptVerifyResult): StageRow[] {
|
|
114
155
|
if (result.ok) {
|
|
115
156
|
return VERIFY_STAGES.map((stage) => ({ stage, status: "ok" }));
|
|
@@ -185,6 +226,19 @@ export type VerifyReport = {
|
|
|
185
226
|
reason?: string;
|
|
186
227
|
stages: StageRow[];
|
|
187
228
|
anchor?: AnchorReport;
|
|
229
|
+
/** KS256 root and ERC-1271 attestation facts (present for a KS256 root). */
|
|
230
|
+
ks256?: {
|
|
231
|
+
root: boolean;
|
|
232
|
+
/** An unused attestation reports `used: false` only: its fields were
|
|
233
|
+
* never checked against anything, so they are not echoed. */
|
|
234
|
+
attestation?: {
|
|
235
|
+
used: boolean;
|
|
236
|
+
chainId?: string;
|
|
237
|
+
address?: string;
|
|
238
|
+
blockNumber?: string;
|
|
239
|
+
blockHash?: string;
|
|
240
|
+
};
|
|
241
|
+
};
|
|
188
242
|
};
|
|
189
243
|
|
|
190
244
|
/** Extra anchor facts present only for known-accumulator checks. */
|
|
@@ -203,6 +257,7 @@ export function buildVerifyReport(opts: {
|
|
|
203
257
|
logId?: string | undefined;
|
|
204
258
|
/** Anchor-only mode: rows that reflect the skipped signature stage. */
|
|
205
259
|
stagesOverride?: StageRow[] | undefined;
|
|
260
|
+
ks256?: Ks256ReportInfo | undefined;
|
|
206
261
|
}): VerifyReport {
|
|
207
262
|
const report: VerifyReport = {
|
|
208
263
|
command: "verify",
|
|
@@ -214,6 +269,25 @@ export function buildVerifyReport(opts: {
|
|
|
214
269
|
if (opts.result.reason !== undefined) {
|
|
215
270
|
report.reason = opts.result.reason;
|
|
216
271
|
}
|
|
272
|
+
if (opts.ks256 !== undefined) {
|
|
273
|
+
const att = opts.ks256.attestation;
|
|
274
|
+
report.ks256 = {
|
|
275
|
+
root: opts.ks256.ks256Root,
|
|
276
|
+
...(att !== undefined && opts.ks256.attestationUsed !== true
|
|
277
|
+
? { attestation: { used: false } }
|
|
278
|
+
: att !== undefined
|
|
279
|
+
? {
|
|
280
|
+
attestation: {
|
|
281
|
+
used: true,
|
|
282
|
+
chainId: att.chainId.toString(),
|
|
283
|
+
address: toHex(att.address),
|
|
284
|
+
blockNumber: att.blockNumber.toString(),
|
|
285
|
+
blockHash: toHex(att.blockHash),
|
|
286
|
+
},
|
|
287
|
+
}
|
|
288
|
+
: {}),
|
|
289
|
+
};
|
|
290
|
+
}
|
|
217
291
|
if (opts.anchor !== undefined) {
|
|
218
292
|
const anchor: AnchorReport = {
|
|
219
293
|
univocity: opts.univocity ?? "",
|
|
@@ -263,3 +337,7 @@ export function buildVerifyReport(opts: {
|
|
|
263
337
|
}
|
|
264
338
|
return report;
|
|
265
339
|
}
|
|
340
|
+
|
|
341
|
+
function toHex(bytes: Uint8Array): string {
|
|
342
|
+
return `0x${Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("")}`;
|
|
343
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import type { Out } from "@forestrie/cli-kit/reporting";
|
|
3
|
+
import {
|
|
4
|
+
bytesToHex,
|
|
5
|
+
encodeIsValidSignatureCall,
|
|
6
|
+
ethRpc,
|
|
7
|
+
hexAddressToBytes,
|
|
8
|
+
normalizeHexAddress,
|
|
9
|
+
} from "@forestrie/chain-rpc";
|
|
10
|
+
import {
|
|
11
|
+
decodeChainBindingFromGenesis,
|
|
12
|
+
decodeTrustRootFromGenesis,
|
|
13
|
+
encodeKs256Attestation,
|
|
14
|
+
ERC1271_MAGIC_VALUE,
|
|
15
|
+
extractKs256DelegationDigest,
|
|
16
|
+
} from "@forestrie/receipt-verify";
|
|
17
|
+
import { keccak256 } from "viem";
|
|
18
|
+
import type { FetchKs256AttestationOptions } from "../options/fetch-ks256-attestation.js";
|
|
19
|
+
import { isKs256Root } from "../lib/known-log-key.js";
|
|
20
|
+
|
|
21
|
+
/** `--json` success shape on stdout — stable for demo scripting. */
|
|
22
|
+
export type FetchKs256AttestationReport = {
|
|
23
|
+
command: "fetch-ks256-attestation";
|
|
24
|
+
chainId: string;
|
|
25
|
+
address: string;
|
|
26
|
+
digest: string;
|
|
27
|
+
blockNumber: string;
|
|
28
|
+
blockHash: string;
|
|
29
|
+
out: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type FetchKs256AttestationErrorReport = {
|
|
33
|
+
error: "fetch_ks256_attestation_failed";
|
|
34
|
+
command: "fetch-ks256-attestation";
|
|
35
|
+
message: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function readFile(path: string, flag: string): Uint8Array {
|
|
39
|
+
try {
|
|
40
|
+
return new Uint8Array(readFileSync(path));
|
|
41
|
+
} catch (err) {
|
|
42
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
43
|
+
throw new Error(`cannot read ${flag} '${path}': ${message}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function hexToBigint(v: unknown, what: string): bigint {
|
|
48
|
+
if (typeof v !== "string" || !/^0x[0-9a-fA-F]+$/.test(v)) {
|
|
49
|
+
throw new Error(`${what}: expected a hex quantity, got ${String(v)}`);
|
|
50
|
+
}
|
|
51
|
+
return BigInt(v);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function hexToBytes32(v: unknown, what: string): Uint8Array {
|
|
55
|
+
if (typeof v !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(v)) {
|
|
56
|
+
throw new Error(`${what}: expected a 32-byte hex value, got ${String(v)}`);
|
|
57
|
+
}
|
|
58
|
+
return new Uint8Array(Buffer.from(v.slice(2), "hex"));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Pin to one block, then: refuse an EOA root (no code), and require the
|
|
63
|
+
* contract's `isValidSignature(digest, signature)` to return the ERC-1271
|
|
64
|
+
* magic value for the certificate's own digest and signature. Nothing is
|
|
65
|
+
* written unless every check passes.
|
|
66
|
+
*/
|
|
67
|
+
export async function runFetchKs256Attestation(
|
|
68
|
+
out: Out,
|
|
69
|
+
options: FetchKs256AttestationOptions,
|
|
70
|
+
): Promise<void> {
|
|
71
|
+
try {
|
|
72
|
+
const receiptCbor = readFile(options.receipt, "--receipt");
|
|
73
|
+
const genesisCbor =
|
|
74
|
+
options.genesis !== undefined
|
|
75
|
+
? readFile(options.genesis, "--genesis")
|
|
76
|
+
: undefined;
|
|
77
|
+
|
|
78
|
+
let root: Uint8Array;
|
|
79
|
+
if (options.root !== undefined) {
|
|
80
|
+
const normalized = normalizeHexAddress(options.root);
|
|
81
|
+
if (normalized === null) {
|
|
82
|
+
throw new Error(`--root is not a valid address: '${options.root}'`);
|
|
83
|
+
}
|
|
84
|
+
root = hexAddressToBytes(normalized);
|
|
85
|
+
} else {
|
|
86
|
+
const trust = await decodeTrustRootFromGenesis(genesisCbor!);
|
|
87
|
+
if (!isKs256Root(trust)) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
"the genesis root is not a KS256 address (an ES256 root needs no attestation)",
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
root = (trust as { address: Uint8Array }).address;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let genesisChainId: bigint | undefined;
|
|
96
|
+
if (genesisCbor !== undefined) {
|
|
97
|
+
genesisChainId = BigInt(decodeChainBindingFromGenesis(genesisCbor).chainId);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let parts: ReturnType<typeof extractKs256DelegationDigest>;
|
|
101
|
+
try {
|
|
102
|
+
parts = extractKs256DelegationDigest(receiptCbor);
|
|
103
|
+
} catch (err) {
|
|
104
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
105
|
+
throw new Error(`the receipt's delegation certificate is not KS256: ${message}`);
|
|
106
|
+
}
|
|
107
|
+
if (parts === null) {
|
|
108
|
+
throw new Error("receipt carries no delegation certificate");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const chainId = hexToBigint(
|
|
112
|
+
await ethRpc(options.rpcUrl, "eth_chainId", []),
|
|
113
|
+
"eth_chainId",
|
|
114
|
+
);
|
|
115
|
+
if (genesisChainId !== undefined && chainId !== genesisChainId) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
`RPC chain id ${chainId} differs from the genesis chain id ${genesisChainId}`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const blockTag =
|
|
122
|
+
options.block === "latest" ? "latest" : `0x${options.block.toString(16)}`;
|
|
123
|
+
const block = (await ethRpc(options.rpcUrl, "eth_getBlockByNumber", [
|
|
124
|
+
blockTag,
|
|
125
|
+
false,
|
|
126
|
+
])) as { number?: unknown; hash?: unknown } | null;
|
|
127
|
+
if (block === null || typeof block !== "object") {
|
|
128
|
+
throw new Error(`eth_getBlockByNumber returned no block for ${blockTag}`);
|
|
129
|
+
}
|
|
130
|
+
const blockNumber = hexToBigint(block.number, "block number");
|
|
131
|
+
if (options.block !== "latest" && blockNumber !== options.block) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`RPC returned block ${blockNumber} for --block ${options.block}`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
const blockHash = hexToBytes32(block.hash, "block hash");
|
|
137
|
+
// EIP-1898: read state at exactly the recorded block hash, so the
|
|
138
|
+
// attestation's blockHash is the state actually read (a reorg or a
|
|
139
|
+
// load-balanced RPC cannot answer from another block).
|
|
140
|
+
const pinned = {
|
|
141
|
+
blockHash: `0x${bytesToHex(blockHash)}`,
|
|
142
|
+
requireCanonical: true,
|
|
143
|
+
};
|
|
144
|
+
const rootHex = `0x${bytesToHex(root)}`;
|
|
145
|
+
|
|
146
|
+
const code = await ethRpc(options.rpcUrl, "eth_getCode", [rootHex, pinned]);
|
|
147
|
+
if (typeof code !== "string" || code === "0x" || code === "") {
|
|
148
|
+
throw new Error(
|
|
149
|
+
`${rootHex} has no contract code at block ${blockNumber}: an EOA root needs no attestation (verify recovers it by ecrecover)`,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let ret: unknown;
|
|
154
|
+
try {
|
|
155
|
+
ret = await ethRpc(options.rpcUrl, "eth_call", [
|
|
156
|
+
{
|
|
157
|
+
to: rootHex,
|
|
158
|
+
data: encodeIsValidSignatureCall(parts.digest, parts.signature),
|
|
159
|
+
},
|
|
160
|
+
pinned,
|
|
161
|
+
]);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
164
|
+
// ethRpc surfaces only the JSON-RPC error message (not its code), so a
|
|
165
|
+
// revert is recognised by message; anything else (e.g. header not
|
|
166
|
+
// found) is an RPC failure that says nothing about the contract.
|
|
167
|
+
if (/revert/i.test(message)) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
`${rootHex} did not validate the certificate signature (isValidSignature reverted: ${message})`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
throw new Error(
|
|
173
|
+
`RPC error on isValidSignature eth_call at block ${blockNumber}: ${message}`,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
// Exactly one 32-byte word: bytes4 magic, left-aligned, zero padded.
|
|
177
|
+
// Anything else (extra data, dirty padding, a calldata echo from a
|
|
178
|
+
// fallback — the magic is also the selector) is not an attestation.
|
|
179
|
+
const expectedWord = `0x${bytesToHex(ERC1271_MAGIC_VALUE)}${"00".repeat(28)}`;
|
|
180
|
+
if (typeof ret !== "string" || !/^0x[0-9a-fA-F]{64}$/.test(ret)) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
`isValidSignature did not return exactly one 32-byte word: ${String(ret)}`,
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
if (ret.toLowerCase() !== expectedWord) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
`${rootHex} did not validate the certificate signature: isValidSignature returned ${ret}, not the ERC-1271 magic value word ${expectedWord}`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
const magic = ERC1271_MAGIC_VALUE.slice();
|
|
191
|
+
|
|
192
|
+
const signatureHash = new Uint8Array(
|
|
193
|
+
Buffer.from(keccak256(parts.signature).slice(2), "hex"),
|
|
194
|
+
);
|
|
195
|
+
writeFileSync(
|
|
196
|
+
options.out,
|
|
197
|
+
encodeKs256Attestation({
|
|
198
|
+
version: 1,
|
|
199
|
+
chainId,
|
|
200
|
+
address: root,
|
|
201
|
+
digest: parts.digest,
|
|
202
|
+
signatureHash,
|
|
203
|
+
blockNumber,
|
|
204
|
+
blockHash,
|
|
205
|
+
magic,
|
|
206
|
+
}),
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
if (options.json) {
|
|
210
|
+
const report: FetchKs256AttestationReport = {
|
|
211
|
+
command: "fetch-ks256-attestation",
|
|
212
|
+
chainId: chainId.toString(),
|
|
213
|
+
address: rootHex,
|
|
214
|
+
digest: `0x${bytesToHex(parts.digest)}`,
|
|
215
|
+
blockNumber: blockNumber.toString(),
|
|
216
|
+
blockHash: `0x${bytesToHex(blockHash)}`,
|
|
217
|
+
out: options.out,
|
|
218
|
+
};
|
|
219
|
+
out.out(JSON.stringify(report, null, 2));
|
|
220
|
+
} else {
|
|
221
|
+
out.print(
|
|
222
|
+
"fetch-ks256-attestation: %s signs digest 0x%s at block %s (chain %s)",
|
|
223
|
+
rootHex,
|
|
224
|
+
bytesToHex(parts.digest),
|
|
225
|
+
blockNumber.toString(),
|
|
226
|
+
chainId.toString(),
|
|
227
|
+
);
|
|
228
|
+
out.out(`wrote ERC-1271 attestation: ${options.out}`);
|
|
229
|
+
}
|
|
230
|
+
process.exitCode = 0;
|
|
231
|
+
} catch (err) {
|
|
232
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
233
|
+
if (options.json) {
|
|
234
|
+
const report: FetchKs256AttestationErrorReport = {
|
|
235
|
+
error: "fetch_ks256_attestation_failed",
|
|
236
|
+
command: "fetch-ks256-attestation",
|
|
237
|
+
message,
|
|
238
|
+
};
|
|
239
|
+
out.out(JSON.stringify(report, null, 2));
|
|
240
|
+
} else {
|
|
241
|
+
out.warn("forestrie fetch-ks256-attestation: %s", message);
|
|
242
|
+
}
|
|
243
|
+
process.exitCode = 1;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -10,6 +10,8 @@ import {
|
|
|
10
10
|
} from "../lib/onboard-genesis-body.js";
|
|
11
11
|
import type { OnboardGenesisOptions } from "../options/onboard-genesis.js";
|
|
12
12
|
|
|
13
|
+
import { responseProblemDetail } from "../lib/http-problem.js";
|
|
14
|
+
|
|
13
15
|
/**
|
|
14
16
|
* FOR-406 (plan-2607-27 W2): operator genesis onboarding — turn a
|
|
15
17
|
* `forestrie deploy` into a usable forest. POSTs the five-label v2
|
|
@@ -138,7 +140,7 @@ export async function runOnboardGenesis(
|
|
|
138
140
|
command: "onboard-genesis",
|
|
139
141
|
message: `genesis POST rejected: HTTP ${res.status}`,
|
|
140
142
|
httpStatus: res.status,
|
|
141
|
-
detail:
|
|
143
|
+
detail: await responseProblemDetail(res),
|
|
142
144
|
});
|
|
143
145
|
return;
|
|
144
146
|
}
|
|
@@ -16,6 +16,8 @@ import { loadEs256SigningKey } from "../lib/sign-statement-key.js";
|
|
|
16
16
|
import { signX402Payment } from "../lib/x402-payment.js";
|
|
17
17
|
import type { OnboardRequestOptions } from "../options/onboard-request.js";
|
|
18
18
|
|
|
19
|
+
import { responseProblemDetail } from "../lib/http-problem.js";
|
|
20
|
+
|
|
19
21
|
/**
|
|
20
22
|
* Self-service onboarding (plan-2607-43 slice 06): create → (pay) → redeem.
|
|
21
23
|
*
|
|
@@ -183,7 +185,7 @@ export async function runOnboardRequest(
|
|
|
183
185
|
reportError(out, options, {
|
|
184
186
|
error: "create_failed",
|
|
185
187
|
command: "onboard-request",
|
|
186
|
-
message: `request rejected: HTTP ${res.status}: ${
|
|
188
|
+
message: `request rejected: HTTP ${res.status}: ${await responseProblemDetail(res, 300)}`,
|
|
187
189
|
httpStatus: res.status,
|
|
188
190
|
});
|
|
189
191
|
return;
|
|
@@ -271,7 +273,7 @@ export async function runOnboardRequest(
|
|
|
271
273
|
reportError(out, options, {
|
|
272
274
|
error: "redeem_failed",
|
|
273
275
|
command: "onboard-request",
|
|
274
|
-
message: `redeem rejected: HTTP ${res.status}: ${
|
|
276
|
+
message: `redeem rejected: HTTP ${res.status}: ${await responseProblemDetail(res, 300)}`,
|
|
275
277
|
httpStatus: res.status,
|
|
276
278
|
});
|
|
277
279
|
return;
|