@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
|
@@ -46,12 +46,13 @@ export declare const HEADER_LABELS: ReadonlyMap<number, LabelInfo>;
|
|
|
46
46
|
/** COSE algorithm names (RFC 9053 + forestrie private-use). */
|
|
47
47
|
export declare const ALG_NAMES: ReadonlyMap<number, string>;
|
|
48
48
|
/**
|
|
49
|
-
* Verifiable data structure ids (
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* never as registry fact
|
|
49
|
+
* Verifiable data structure ids (the COSE receipts registry, RFC 9942).
|
|
50
|
+
* 3 is NOT a registered codepoint: draft-bryce-cose-receipts-mmr-profile
|
|
51
|
+
* requests TBD_1 (registration pending). The Go sealer emits `395: 3` on
|
|
52
|
+
* every checkpoint and peak receipt; the TypeScript exporters do not, so a
|
|
53
|
+
* verifier must not require it. Render it as the draft's unregistered
|
|
54
|
+
* codepoint, never as registry fact. The display strings are the protocol
|
|
55
|
+
* registry's (spec/label-registry.md).
|
|
55
56
|
*/
|
|
56
57
|
export declare const VDS_NAMES: ReadonlyMap<number, string>;
|
|
57
58
|
/** CWT claim keys (RFC 8392 §3.1, cnf per RFC 8747). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"decode-receipt-labels.d.ts","sourceRoot":"","sources":["../../src/lib/decode-receipt-labels.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,6CAA6C;AAC7C,eAAO,MAAM,cAAc,KAAK,CAAC;AAEjC,iFAAiF;AACjF,eAAO,MAAM,uBAAuB,MAAM,CAAC;AAC3C,uFAAuF;AACvF,eAAO,MAAM,SAAS,MAAM,CAAC;AAC7B,8CAA8C;AAC9C,eAAO,MAAM,gBAAgB,KAAK,CAAC;AACnC,gFAAgF;AAChF,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAC1C,oFAAoF;AACpF,eAAO,MAAM,wBAAwB,SAAS,CAAC;AAC/C;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,SAAS,CAAC;AAExC,8CAA8C;AAC9C,eAAO,MAAM,oBAAoB,KAAK,CAAC;AACvC,gDAAgD;AAChD,eAAO,MAAM,sBAAsB,KAAK,CAAC;AAEzC,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,wDAAwD;IACxD,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,6EAA6E;AAC7E,eAAO,MAAM,aAAa,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,CAuEvD,CAAC;AAEH,+DAA+D;AAC/D,eAAO,MAAM,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAWhD,CAAC;AAEH
|
|
1
|
+
{"version":3,"file":"decode-receipt-labels.d.ts","sourceRoot":"","sources":["../../src/lib/decode-receipt-labels.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,6CAA6C;AAC7C,eAAO,MAAM,cAAc,KAAK,CAAC;AAEjC,iFAAiF;AACjF,eAAO,MAAM,uBAAuB,MAAM,CAAC;AAC3C,uFAAuF;AACvF,eAAO,MAAM,SAAS,MAAM,CAAC;AAC7B,8CAA8C;AAC9C,eAAO,MAAM,gBAAgB,KAAK,CAAC;AACnC,gFAAgF;AAChF,eAAO,MAAM,qBAAqB,OAAO,CAAC;AAC1C,oFAAoF;AACpF,eAAO,MAAM,wBAAwB,SAAS,CAAC;AAC/C;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,SAAS,CAAC;AAExC,8CAA8C;AAC9C,eAAO,MAAM,oBAAoB,KAAK,CAAC;AACvC,gDAAgD;AAChD,eAAO,MAAM,sBAAsB,KAAK,CAAC;AAEzC,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,wDAAwD;IACxD,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,6EAA6E;AAC7E,eAAO,MAAM,aAAa,EAAE,WAAW,CAAC,MAAM,EAAE,SAAS,CAuEvD,CAAC;AAEH,+DAA+D;AAC/D,eAAO,MAAM,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAWhD,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,SAAS,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAIhD,CAAC;AAEH,wDAAwD;AACxD,eAAO,MAAM,eAAe,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAStD,CAAC;AAEH,yEAAyE;AACzE,eAAO,MAAM,oBAAoB,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAO3D,CAAC;AAEH,mEAAmE;AACnE,eAAO,MAAM,gBAAgB,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAGvD,CAAC;AAEH,4EAA4E;AAC5E,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAE/D"}
|
|
@@ -47,11 +47,11 @@ export const HEADER_LABELS = new Map([
|
|
|
47
47
|
[CWT_CLAIMS_LABEL, { name: "CWT claims", note: "RFC 9597" }],
|
|
48
48
|
[
|
|
49
49
|
VDS_LABEL,
|
|
50
|
-
{ name: "verifiable data structure", note: "COSE receipts (
|
|
50
|
+
{ name: "verifiable data structure", note: "COSE receipts (RFC 9942)" },
|
|
51
51
|
],
|
|
52
52
|
[
|
|
53
53
|
VERIFIABLE_PROOFS_LABEL,
|
|
54
|
-
{ name: "verifiable proofs", note: "COSE receipts (
|
|
54
|
+
{ name: "verifiable proofs", note: "COSE receipts (RFC 9942)" },
|
|
55
55
|
],
|
|
56
56
|
[
|
|
57
57
|
DELEGATION_CERT_LABEL,
|
|
@@ -115,17 +115,18 @@ export const ALG_NAMES = new Map([
|
|
|
115
115
|
[-65799, "KS256 (secp256k1 + Keccak-256, forestrie private-use)"],
|
|
116
116
|
[
|
|
117
117
|
-65800,
|
|
118
|
-
"ES256-WebAuthn (forestrie private-use; delegation proofs
|
|
119
|
-
"
|
|
118
|
+
"ES256-WebAuthn (forestrie private-use; delegation proofs, certificates " +
|
|
119
|
+
"and session-key endorsements, never checkpoint-signing)",
|
|
120
120
|
],
|
|
121
121
|
]);
|
|
122
122
|
/**
|
|
123
|
-
* Verifiable data structure ids (
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
* never as registry fact
|
|
123
|
+
* Verifiable data structure ids (the COSE receipts registry, RFC 9942).
|
|
124
|
+
* 3 is NOT a registered codepoint: draft-bryce-cose-receipts-mmr-profile
|
|
125
|
+
* requests TBD_1 (registration pending). The Go sealer emits `395: 3` on
|
|
126
|
+
* every checkpoint and peak receipt; the TypeScript exporters do not, so a
|
|
127
|
+
* verifier must not require it. Render it as the draft's unregistered
|
|
128
|
+
* codepoint, never as registry fact. The display strings are the protocol
|
|
129
|
+
* registry's (spec/label-registry.md).
|
|
129
130
|
*/
|
|
130
131
|
export const VDS_NAMES = new Map([
|
|
131
132
|
[1, "RFC9162_SHA256 (Certificate Transparency)"],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"decode-receipt-labels.js","sourceRoot":"","sources":["../../src/lib/decode-receipt-labels.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,6CAA6C;AAC7C,MAAM,CAAC,MAAM,cAAc,GAAG,EAAE,CAAC;AAEjC,iFAAiF;AACjF,MAAM,CAAC,MAAM,uBAAuB,GAAG,GAAG,CAAC;AAC3C,uFAAuF;AACvF,MAAM,CAAC,MAAM,SAAS,GAAG,GAAG,CAAC;AAC7B,8CAA8C;AAC9C,MAAM,CAAC,MAAM,gBAAgB,GAAG,EAAE,CAAC;AACnC,gFAAgF;AAChF,MAAM,CAAC,MAAM,qBAAqB,GAAG,IAAI,CAAC;AAC1C,oFAAoF;AACpF,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,KAAK,CAAC;AAC/C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,KAAK,CAAC;AAExC,8CAA8C;AAC9C,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC,CAAC;AACvC,gDAAgD;AAChD,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC,CAAC;AAQzC,6EAA6E;AAC7E,MAAM,CAAC,MAAM,aAAa,GAAmC,IAAI,GAAG,CAAC;IACnE,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;IACpB,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IACrB,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE,CAAC;IAC7B,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;IACpB,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IACnB,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC;IAC3B,CAAC,gBAAgB,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;IAC5D;QACE,SAAS;QACT,EAAE,IAAI,EAAE,2BAA2B,EAAE,IAAI,EAAE,
|
|
1
|
+
{"version":3,"file":"decode-receipt-labels.js","sourceRoot":"","sources":["../../src/lib/decode-receipt-labels.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,6CAA6C;AAC7C,MAAM,CAAC,MAAM,cAAc,GAAG,EAAE,CAAC;AAEjC,iFAAiF;AACjF,MAAM,CAAC,MAAM,uBAAuB,GAAG,GAAG,CAAC;AAC3C,uFAAuF;AACvF,MAAM,CAAC,MAAM,SAAS,GAAG,GAAG,CAAC;AAC7B,8CAA8C;AAC9C,MAAM,CAAC,MAAM,gBAAgB,GAAG,EAAE,CAAC;AACnC,gFAAgF;AAChF,MAAM,CAAC,MAAM,qBAAqB,GAAG,IAAI,CAAC;AAC1C,oFAAoF;AACpF,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,KAAK,CAAC;AAC/C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,KAAK,CAAC;AAExC,8CAA8C;AAC9C,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC,CAAC;AACvC,gDAAgD;AAChD,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC,CAAC;AAQzC,6EAA6E;AAC7E,MAAM,CAAC,MAAM,aAAa,GAAmC,IAAI,GAAG,CAAC;IACnE,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;IACpB,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IACrB,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,cAAc,EAAE,CAAC;IAC7B,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;IACpB,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IACnB,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC;IAC3B,CAAC,gBAAgB,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;IAC5D;QACE,SAAS;QACT,EAAE,IAAI,EAAE,2BAA2B,EAAE,IAAI,EAAE,0BAA0B,EAAE;KACxE;IACD;QACE,uBAAuB;QACvB,EAAE,IAAI,EAAE,mBAAmB,EAAE,IAAI,EAAE,0BAA0B,EAAE;KAChE;IACD;QACE,qBAAqB;QACrB;YACE,IAAI,EAAE,wBAAwB;YAC9B,IAAI,EAAE,6DAA6D;SACpE;KACF;IACD,CAAC,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,uBAAuB,EAAE,CAAC;IAChE,CAAC,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,oBAAoB,EAAE,IAAI,EAAE,uBAAuB,EAAE,CAAC;IACvE;QACE,CAAC,KAAK;QACN;YACE,IAAI,EAAE,6BAA6B;YACnC,IAAI,EACF,oEAAoE;gBACpE,8DAA8D;SACjE;KACF;IACD;QACE,CAAC,KAAK;QACN;YACE,IAAI,EAAE,yBAAyB;YAC/B,IAAI,EACF,iEAAiE;gBACjE,0DAA0D;SAC7D;KACF;IACD;QACE,wBAAwB;QACxB;YACE,IAAI,EAAE,0BAA0B;YAChC,IAAI,EAAE,qDAAqD;SAC5D;KACF;IACD;QACE,iBAAiB;QACjB;YACE,IAAI,EAAE,aAAa;YACnB,IAAI,EACF,oEAAoE;gBACpE,mBAAmB;SACtB;KACF;IACD;QACE,CAAC,KAAK;QACN;YACE,IAAI,EAAE,2BAA2B;YACjC,IAAI,EAAE,wDAAwD;SAC/D;KACF;IACD,CAAC,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,wBAAwB,EAAE,IAAI,EAAE,uBAAuB,EAAE,CAAC;IAC3E,CAAC,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,mBAAmB,EAAE,IAAI,EAAE,uBAAuB,EAAE,CAAC;IACtE,CAAC,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,uBAAuB,EAAE,CAAC;IAC7D,CAAC,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,oBAAoB,EAAE,IAAI,EAAE,uBAAuB,EAAE,CAAC;IACvE,CAAC,CAAC,KAAK,EAAE,EAAE,IAAI,EAAE,eAAe,EAAE,IAAI,EAAE,uBAAuB,EAAE,CAAC;CACnE,CAAC,CAAC;AAEH,+DAA+D;AAC/D,MAAM,CAAC,MAAM,SAAS,GAAgC,IAAI,GAAG,CAAC;IAC5D,CAAC,CAAC,CAAC,EAAE,+BAA+B,CAAC;IACrC,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC;IACb,CAAC,CAAC,EAAE,EAAE,OAAO,CAAC;IACd,CAAC,CAAC,EAAE,EAAE,OAAO,CAAC;IACd,CAAC,CAAC,KAAK,EAAE,uDAAuD,CAAC;IACjE;QACE,CAAC,KAAK;QACN,yEAAyE;YACvE,yDAAyD;KAC5D;CACF,CAAC,CAAC;AAEH;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,SAAS,GAAgC,IAAI,GAAG,CAAC;IAC5D,CAAC,CAAC,EAAE,2CAA2C,CAAC;IAChD,CAAC,CAAC,EAAE,mBAAmB,CAAC;IACxB,CAAC,CAAC,EAAE,0CAA0C,CAAC;CAChD,CAAC,CAAC;AAEH,wDAAwD;AACxD,MAAM,CAAC,MAAM,eAAe,GAAgC,IAAI,GAAG,CAAC;IAClE,CAAC,CAAC,EAAE,KAAK,CAAC;IACV,CAAC,CAAC,EAAE,KAAK,CAAC;IACV,CAAC,CAAC,EAAE,KAAK,CAAC;IACV,CAAC,CAAC,EAAE,KAAK,CAAC;IACV,CAAC,CAAC,EAAE,KAAK,CAAC;IACV,CAAC,CAAC,EAAE,KAAK,CAAC;IACV,CAAC,CAAC,EAAE,KAAK,CAAC;IACV,CAAC,CAAC,EAAE,oCAAoC,CAAC;CAC1C,CAAC,CAAC;AAEH,yEAAyE;AACzE,MAAM,CAAC,MAAM,oBAAoB,GAAgC,IAAI,GAAG,CAAC;IACvE,CAAC,CAAC,EAAE,KAAK,CAAC;IACV,CAAC,CAAC,EAAE,KAAK,CAAC;IACV,CAAC,CAAC,EAAE,KAAK,CAAC;IACV,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC;IACX,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC;IACT,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC;CACV,CAAC,CAAC;AAEH,mEAAmE;AACnE,MAAM,CAAC,MAAM,gBAAgB,GAAgC,IAAI,GAAG,CAAC;IACnE,CAAC,oBAAoB,EAAE,kBAAkB,CAAC;IAC1C,CAAC,sBAAsB,EAAE,oBAAoB,CAAC;CAC/C,CAAC,CAAC;AAEH,4EAA4E;AAC5E,MAAM,UAAU,eAAe,CAAC,KAAa;IAC3C,OAAO,aAAa,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,IAAI,CAAC;AAC1C,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forestrie/forestrie-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "forestrie — participant CLI for forestrie transparency logs (SCITT / COSE receipts)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"forestrie",
|
|
@@ -66,12 +66,12 @@
|
|
|
66
66
|
"dependencies": {
|
|
67
67
|
"@forestrie/chain-rpc": "^0.3.0",
|
|
68
68
|
"@forestrie/cli-kit": "^0.1.1",
|
|
69
|
-
"@forestrie/delegation-cose": "
|
|
69
|
+
"@forestrie/delegation-cose": "0.4.0",
|
|
70
70
|
"@forestrie/deploy-core": "^0.7.1",
|
|
71
|
-
"@forestrie/encoding": "^0.
|
|
71
|
+
"@forestrie/encoding": "^0.9.1",
|
|
72
72
|
"@forestrie/grant-builder": "^0.5.0",
|
|
73
|
-
"@forestrie/merklelog": "^0.
|
|
74
|
-
"@forestrie/receipt-verify": "
|
|
73
|
+
"@forestrie/merklelog": "^0.5.0",
|
|
74
|
+
"@forestrie/receipt-verify": "3.1.0",
|
|
75
75
|
"@forestrie/scrapi-client": "^0.2.2",
|
|
76
76
|
"citty": "^0.2.2",
|
|
77
77
|
"viem": "^2.31.0"
|
package/src/command.ts
CHANGED
|
@@ -45,6 +45,8 @@ export default defineForestrieCommand({
|
|
|
45
45
|
import("./commands/verify-grant.js").then((m) => m.default),
|
|
46
46
|
"fetch-accumulator": () =>
|
|
47
47
|
import("./commands/fetch-accumulator.js").then((m) => m.default),
|
|
48
|
+
"fetch-ks256-attestation": () =>
|
|
49
|
+
import("./commands/fetch-ks256-attestation.js").then((m) => m.default),
|
|
48
50
|
"create-consistency-proof": () =>
|
|
49
51
|
import("./commands/create-consistency-proof.js").then((m) => m.default),
|
|
50
52
|
},
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { defineCommandRunner, defineForestrieCommand } from "../commoncli.js";
|
|
2
|
+
import { runFetchKs256Attestation } from "../main/fetch-ks256-attestation.js";
|
|
3
|
+
import { parseFetchKs256AttestationOptions } from "../options/fetch-ks256-attestation.js";
|
|
4
|
+
|
|
5
|
+
export default defineForestrieCommand({
|
|
6
|
+
meta: {
|
|
7
|
+
name: "fetch-ks256-attestation",
|
|
8
|
+
description:
|
|
9
|
+
"Ask a KS256 contract root (a Safe) over ERC-1271 whether it signs the receipt's delegation certificate, and cache the block-bound answer as a --ks256-attestation artifact (CBOR binding chainId/address/digest/signature hash/block) — verify then accepts the contract-rooted receipt offline as valid under the contract's signers at that block. Auditable: anyone with RPC can re-run the call at that block. An EOA root needs no attestation (verify recovers it)",
|
|
10
|
+
},
|
|
11
|
+
args: {
|
|
12
|
+
receipt: {
|
|
13
|
+
type: "string",
|
|
14
|
+
description: "Receipt carrying the KS256 delegation certificate (label 1000)",
|
|
15
|
+
valueHint: "path",
|
|
16
|
+
required: true,
|
|
17
|
+
},
|
|
18
|
+
root: {
|
|
19
|
+
type: "string",
|
|
20
|
+
description:
|
|
21
|
+
"Contract root address (0x + 40 hex); overrides the genesis root",
|
|
22
|
+
valueHint: "address",
|
|
23
|
+
},
|
|
24
|
+
genesis: {
|
|
25
|
+
type: "string",
|
|
26
|
+
description:
|
|
27
|
+
"Forest genesis (genesis.cbor): supplies the KS256 root unless --root is given, and the chain id the RPC must match",
|
|
28
|
+
valueHint: "path",
|
|
29
|
+
},
|
|
30
|
+
"rpc-url": {
|
|
31
|
+
type: "string",
|
|
32
|
+
description: "JSON-RPC endpoint (default: ${env} → RPC_URL)",
|
|
33
|
+
valueHint: "url",
|
|
34
|
+
},
|
|
35
|
+
block: {
|
|
36
|
+
type: "string",
|
|
37
|
+
description:
|
|
38
|
+
"Block number to pin the calls to, or 'latest' (default) — needs an archive node for old blocks",
|
|
39
|
+
valueHint: "number|latest",
|
|
40
|
+
},
|
|
41
|
+
out: {
|
|
42
|
+
type: "string",
|
|
43
|
+
description: "Output path for the attestation CBOR",
|
|
44
|
+
valueHint: "path",
|
|
45
|
+
required: true,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
run: defineCommandRunner(
|
|
49
|
+
parseFetchKs256AttestationOptions,
|
|
50
|
+
runFetchKs256Attestation,
|
|
51
|
+
),
|
|
52
|
+
});
|
|
@@ -18,8 +18,14 @@ export default defineForestrieCommand({
|
|
|
18
18
|
"known-log-key": {
|
|
19
19
|
type: "string",
|
|
20
20
|
description:
|
|
21
|
-
"Caller-known log OWNER key (the delegation issuer)
|
|
22
|
-
valueHint: "base64",
|
|
21
|
+
"Caller-known log OWNER key (the delegation issuer): base64 x||y (64-byte ES256 key) or a 20-byte KS256 address (0x + 40 hex, or base64) (env KNOWN_LOG_KEY). Offline signer-provenance anchor that replaces --genesis (give --genesis too and it only supplies the forest chain id an attestation must match); asserts (does not prove) the key-to-log binding — --genesis roots it in the forest genesis document's bootstrap key (the root log, or a delegation directly under it), while the accumulator roots (--known-accumulator / --rpc-url) supply split-view (freshness is a weaker, separate axis bearing only on coverage). See https://github.com/forestrie/protocol/blob/main/spec/receipt-trust-model.md",
|
|
22
|
+
valueHint: "base64|0xaddress",
|
|
23
|
+
},
|
|
24
|
+
"ks256-attestation": {
|
|
25
|
+
type: "string",
|
|
26
|
+
description:
|
|
27
|
+
"ERC-1271 attestation (CBOR from `forestrie fetch-ks256-attestation`) for a KS256 contract root such as a Safe: the delegation certificate then verifies offline as valid under the contract's signers at the attested block, not as a signature by one key. Needs the forest chain id from --genesis. verify never calls the chain",
|
|
28
|
+
valueHint: "path",
|
|
23
29
|
},
|
|
24
30
|
receipt: {
|
|
25
31
|
type: "string",
|
package/src/commands/verify.ts
CHANGED
|
@@ -18,8 +18,14 @@ export default defineForestrieCommand({
|
|
|
18
18
|
"known-log-key": {
|
|
19
19
|
type: "string",
|
|
20
20
|
description:
|
|
21
|
-
"Caller-known log OWNER key (the delegation issuer)
|
|
22
|
-
valueHint: "base64",
|
|
21
|
+
"Caller-known log OWNER key (the delegation issuer): base64 x||y (64-byte ES256 key) or a 20-byte KS256 address (0x + 40 hex, or base64) (env KNOWN_LOG_KEY). Offline signer-provenance anchor that replaces --genesis (give --genesis too and it only supplies the forest chain id an attestation must match); asserts (does not prove) the key-to-log binding — --genesis roots it in the forest genesis document's bootstrap key (the root log, or a delegation directly under it), while the accumulator roots (--known-accumulator / --rpc-url) supply split-view (freshness is a weaker, separate axis bearing only on coverage). See https://github.com/forestrie/protocol/blob/main/spec/receipt-trust-model.md",
|
|
22
|
+
valueHint: "base64|0xaddress",
|
|
23
|
+
},
|
|
24
|
+
"ks256-attestation": {
|
|
25
|
+
type: "string",
|
|
26
|
+
description:
|
|
27
|
+
"ERC-1271 attestation (CBOR from `forestrie fetch-ks256-attestation`) for a KS256 contract root such as a Safe: the delegation certificate then verifies offline as valid under the contract's signers at the attested block, not as a signature by one key. Needs the forest chain id from --genesis. verify never calls the chain",
|
|
28
|
+
valueHint: "path",
|
|
23
29
|
},
|
|
24
30
|
receipt: {
|
|
25
31
|
type: "string",
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
computeCheckpointAccumulator,
|
|
32
32
|
checkpointConsistencyProof,
|
|
33
33
|
type CheckpointConsistencyProof,
|
|
34
|
+
type DecodedConsistencyProof,
|
|
34
35
|
} from "@forestrie/receipt-verify";
|
|
35
36
|
import { fetchPublishedCheckpoints } from "./verify-eventscan.js";
|
|
36
37
|
import {
|
|
@@ -56,21 +57,38 @@ export type CheckpointSeal =
|
|
|
56
57
|
| { kind: "sth"; checkpointBytes: Uint8Array };
|
|
57
58
|
|
|
58
59
|
/**
|
|
59
|
-
*
|
|
60
|
-
* proofs (`CalldataConsistencyProof` —
|
|
61
|
-
*
|
|
62
|
-
* `CheckpointConsistencyProof` (
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
60
|
+
* One fold step: a single consistency proof, shared by calldata-decoded
|
|
61
|
+
* proofs (`CalldataConsistencyProof` — on-chain calldata carries no protected
|
|
62
|
+
* header, so no signed size) and `.sth`-decoded proofs. receipt-verify
|
|
63
|
+
* decodes an `.sth` to a `CheckpointConsistencyProof` CHAIN (`proofs[]`, one
|
|
64
|
+
* or more relayed steps under the head signature, ADR-0066 D2); this provider
|
|
65
|
+
* keeps one link per step, so a link's proof is the single-step
|
|
66
|
+
* `DecodedConsistencyProof`, and {@link singleProofChain} wraps it back into
|
|
67
|
+
* the chain shape where receipt-verify's parameter type demands it. The fold
|
|
68
|
+
* never reads `signedTreeSize2` — only the trusted base SIZE (passed
|
|
69
|
+
* separately, never off the proof) and `treeSize1`/`treeSize2`/`paths`/
|
|
70
|
+
* `rightPeaks` matter — so it is backfilled with `treeSize2`.
|
|
69
71
|
*/
|
|
70
|
-
export type FoldableConsistencyProof =
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
export type FoldableConsistencyProof = DecodedConsistencyProof;
|
|
73
|
+
|
|
74
|
+
/** Wrap one fold step as a one-proof chain for receipt-verify's chain-typed
|
|
75
|
+
* parameters. `signedTreeSize2` is the checkpoint's SIGNED size when the
|
|
76
|
+
* step is the last one a sealed `.sth` relays (so `freshenReceipt`'s
|
|
77
|
+
* signed-vs-declared guard stays live for it), and is backfilled from
|
|
78
|
+
* `treeSize2` for steps that carry no signature of their own: calldata
|
|
79
|
+
* proofs, and the earlier steps a checkpoint relays (see
|
|
80
|
+
* {@link FoldableConsistencyProof}). */
|
|
81
|
+
export function singleProofChain(
|
|
82
|
+
p: FoldableConsistencyProof,
|
|
83
|
+
signedTreeSize2: bigint = p.treeSize2,
|
|
84
|
+
): CheckpointConsistencyProof {
|
|
85
|
+
return {
|
|
86
|
+
proofs: [p],
|
|
87
|
+
treeSize1: p.treeSize1,
|
|
88
|
+
treeSize2: p.treeSize2,
|
|
89
|
+
signedTreeSize2,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
74
92
|
|
|
75
93
|
/** One folded link: the accumulator committed at `treeSize2`. */
|
|
76
94
|
export type CheckpointLink = {
|
|
@@ -82,6 +100,10 @@ export type CheckpointLink = {
|
|
|
82
100
|
* (the tile-free climb material). Retained so a path-extending consumer
|
|
83
101
|
* (freshen) gets it without a re-decode; folding consumers ignore it. */
|
|
84
102
|
proof: FoldableConsistencyProof;
|
|
103
|
+
/** The SIGNED tree-size-2 covering this link, when it is the last step of a
|
|
104
|
+
* sealed `.sth` (cross-checked against `proof.treeSize2` at decode). Absent
|
|
105
|
+
* for calldata links and for the earlier steps a checkpoint relays. */
|
|
106
|
+
signedTreeSize2?: bigint;
|
|
85
107
|
/** The signature material to verify this link's accumulator, when directly
|
|
86
108
|
* signed (see {@link CheckpointSeal}). The provider does NOT verify it. */
|
|
87
109
|
seal?: CheckpointSeal;
|
|
@@ -103,7 +125,9 @@ export async function foldProofChain(
|
|
|
103
125
|
accumulatorFrom?: Uint8Array[];
|
|
104
126
|
accumulatorFromSize?: bigint;
|
|
105
127
|
seals?: readonly (CheckpointSeal | undefined)[];
|
|
106
|
-
sourceRefs?: readonly string[];
|
|
128
|
+
sourceRefs?: readonly (string | undefined)[];
|
|
129
|
+
/** Per-step signed tree-size-2, for the steps that have one. */
|
|
130
|
+
signedSizes?: readonly (bigint | undefined)[];
|
|
107
131
|
} = {},
|
|
108
132
|
): Promise<CheckpointLink[]> {
|
|
109
133
|
let accumulator = opts.accumulatorFrom ?? [];
|
|
@@ -126,13 +150,13 @@ export async function foldProofChain(
|
|
|
126
150
|
`checkpoint chain is not contiguous at link ${i}: base ${p.treeSize1} != expected ${expectedBase}`,
|
|
127
151
|
);
|
|
128
152
|
}
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
|
|
153
|
+
// The trusted base size is a parameter, never read off the proof
|
|
154
|
+
// (ADR-0066 D5.4) — `expectedBase` is exactly that, already checked
|
|
155
|
+
// against `p.treeSize1` above. One link is one step, folded as a
|
|
156
|
+
// one-proof chain (see `singleProofChain`).
|
|
157
|
+
const signed = opts.signedSizes?.[i];
|
|
134
158
|
accumulator = await computeCheckpointAccumulator(
|
|
135
|
-
|
|
159
|
+
singleProofChain(p, signed),
|
|
136
160
|
accumulator,
|
|
137
161
|
expectedBase,
|
|
138
162
|
);
|
|
@@ -142,6 +166,7 @@ export async function foldProofChain(
|
|
|
142
166
|
accumulator,
|
|
143
167
|
proof: p,
|
|
144
168
|
};
|
|
169
|
+
if (signed !== undefined) link.signedTreeSize2 = signed;
|
|
145
170
|
const seal = opts.seals?.[i];
|
|
146
171
|
if (seal !== undefined) link.seal = seal;
|
|
147
172
|
const ref = opts.sourceRefs?.[i];
|
|
@@ -153,9 +178,17 @@ export async function foldProofChain(
|
|
|
153
178
|
}
|
|
154
179
|
|
|
155
180
|
/**
|
|
156
|
-
* Retained-`.sth` provider: decode each checkpoint's embedded consistency
|
|
157
|
-
* and fold. `checkpoints` are the raw `.sth` bytes in ascending massif
|
|
158
|
-
* `sourceRefs` optionally names them (e.g. filenames) for narration.
|
|
181
|
+
* Retained-`.sth` provider: decode each checkpoint's embedded consistency
|
|
182
|
+
* proofs and fold. `checkpoints` are the raw `.sth` bytes in ascending massif
|
|
183
|
+
* order; `sourceRefs` optionally names them (e.g. filenames) for narration.
|
|
184
|
+
* A checkpoint may relay several proofs under one signature (ADR-0066 D2);
|
|
185
|
+
* each becomes its own link, and only the LAST link of a checkpoint carries
|
|
186
|
+
* that checkpoint's seal, since the signature covers only the head size.
|
|
187
|
+
* A log's initialising proof `[0, n, [], peaks]` (empty consistency-paths:
|
|
188
|
+
* the empty tree has no peaks) is a valid first step only; `foldProofChain`
|
|
189
|
+
* binds each step's tree-size-1 to the size reached, so it verifies only
|
|
190
|
+
* against the empty tree. A checkpoint with no proof at all is malformed and
|
|
191
|
+
* the decode throws.
|
|
159
192
|
*/
|
|
160
193
|
export async function sthCheckpointChain(
|
|
161
194
|
checkpoints: readonly Uint8Array[],
|
|
@@ -165,20 +198,30 @@ export async function sthCheckpointChain(
|
|
|
165
198
|
sourceRefs?: readonly string[];
|
|
166
199
|
} = {},
|
|
167
200
|
): Promise<CheckpointLink[]> {
|
|
168
|
-
const proofs =
|
|
169
|
-
const seals: CheckpointSeal[] =
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
201
|
+
const proofs: FoldableConsistencyProof[] = [];
|
|
202
|
+
const seals: (CheckpointSeal | undefined)[] = [];
|
|
203
|
+
const sourceRefs: (string | undefined)[] = [];
|
|
204
|
+
const signedSizes: (bigint | undefined)[] = [];
|
|
205
|
+
checkpoints.forEach((bytes, i) => {
|
|
206
|
+
const chain = checkpointConsistencyProof(bytes);
|
|
207
|
+
chain.proofs.forEach((step, j) => {
|
|
208
|
+
const last = j === chain.proofs.length - 1;
|
|
209
|
+
proofs.push(step);
|
|
210
|
+
seals.push(last ? { kind: "sth", checkpointBytes: bytes } : undefined);
|
|
211
|
+
signedSizes.push(last ? chain.signedTreeSize2 : undefined);
|
|
212
|
+
sourceRefs.push(opts.sourceRefs?.[i]);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
173
215
|
return foldProofChain(proofs, {
|
|
174
216
|
seals,
|
|
217
|
+
signedSizes,
|
|
175
218
|
...(opts.accumulatorFrom !== undefined
|
|
176
219
|
? { accumulatorFrom: opts.accumulatorFrom }
|
|
177
220
|
: {}),
|
|
178
221
|
...(opts.accumulatorFromSize !== undefined
|
|
179
222
|
? { accumulatorFromSize: opts.accumulatorFromSize }
|
|
180
223
|
: {}),
|
|
181
|
-
...(opts.sourceRefs !== undefined ? { sourceRefs
|
|
224
|
+
...(opts.sourceRefs !== undefined ? { sourceRefs } : {}),
|
|
182
225
|
});
|
|
183
226
|
}
|
|
184
227
|
|
|
@@ -38,6 +38,7 @@ export type CompleteGrantReason =
|
|
|
38
38
|
| "checkpoint_parse_failed"
|
|
39
39
|
| "checkpoint_missing_peak_receipts"
|
|
40
40
|
| "checkpoint_missing_sealed_size"
|
|
41
|
+
| "checkpoint_consistency_invalid"
|
|
41
42
|
| "checkpoint_does_not_cover_leaf"
|
|
42
43
|
| "leaf_not_in_massif"
|
|
43
44
|
| "derive_failed";
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* the log exists — no operator onboard token, authority flows down the log
|
|
10
10
|
* hierarchy (ADR-0053). Node/Bun HTTP only.
|
|
11
11
|
*/
|
|
12
|
+
import { responseProblemDetail } from "./http-problem.js";
|
|
13
|
+
|
|
12
14
|
|
|
13
15
|
/** Prepare (child public-root pre-registration) failure. */
|
|
14
16
|
export class PrepareLogError extends Error {
|
|
@@ -48,7 +50,7 @@ export async function prepareChildLog(
|
|
|
48
50
|
headers: { Authorization: `Forestrie-Grant ${params.grantBase64}` },
|
|
49
51
|
});
|
|
50
52
|
if (!res.ok) {
|
|
51
|
-
const preview =
|
|
53
|
+
const preview = await responseProblemDetail(res, 300);
|
|
52
54
|
throw new PrepareLogError(
|
|
53
55
|
`prepare failed: HTTP ${res.status} ${preview}`,
|
|
54
56
|
res.status,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildReceiptOffline,
|
|
3
|
+
checkpointConsistencyProof,
|
|
3
4
|
computeAccumulatorPeak,
|
|
4
5
|
openMassifNodeStore,
|
|
5
6
|
parseCheckpoint,
|
|
@@ -27,6 +28,7 @@ export type CreateReceiptReason =
|
|
|
27
28
|
| "checkpoint_parse_failed"
|
|
28
29
|
| "checkpoint_missing_peak_receipts"
|
|
29
30
|
| "checkpoint_missing_sealed_size"
|
|
31
|
+
| "checkpoint_consistency_invalid"
|
|
30
32
|
| "checkpoint_does_not_cover_leaf"
|
|
31
33
|
| "leaf_not_in_massif"
|
|
32
34
|
| "derive_failed";
|
|
@@ -123,6 +125,20 @@ export async function deriveCheckpointReceipt(input: {
|
|
|
123
125
|
"checkpoint carries no consistency proof (cannot determine sealed size)",
|
|
124
126
|
);
|
|
125
127
|
}
|
|
128
|
+
// The embedded consistency proofs must agree with the signed size: the
|
|
129
|
+
// last proof's tree-size-2 is the size the signature covers (ADR-0066 D1,
|
|
130
|
+
// D2). A checkpoint whose proofs contradict its signed size is malformed,
|
|
131
|
+
// and this builder refuses it rather than deriving from the signed half
|
|
132
|
+
// alone. (parseCheckpoint only requires that a proof be present.)
|
|
133
|
+
try {
|
|
134
|
+
checkpointConsistencyProof(checkpointBytes);
|
|
135
|
+
} catch (err) {
|
|
136
|
+
fail(
|
|
137
|
+
"parse",
|
|
138
|
+
"checkpoint_consistency_invalid",
|
|
139
|
+
`checkpoint consistency proofs do not agree with its signed size: ${errorMessage(err)}`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
126
142
|
|
|
127
143
|
let store: MassifNodeStore;
|
|
128
144
|
try {
|
|
@@ -57,11 +57,11 @@ export const HEADER_LABELS: ReadonlyMap<number, LabelInfo> = new Map([
|
|
|
57
57
|
[CWT_CLAIMS_LABEL, { name: "CWT claims", note: "RFC 9597" }],
|
|
58
58
|
[
|
|
59
59
|
VDS_LABEL,
|
|
60
|
-
{ name: "verifiable data structure", note: "COSE receipts (
|
|
60
|
+
{ name: "verifiable data structure", note: "COSE receipts (RFC 9942)" },
|
|
61
61
|
],
|
|
62
62
|
[
|
|
63
63
|
VERIFIABLE_PROOFS_LABEL,
|
|
64
|
-
{ name: "verifiable proofs", note: "COSE receipts (
|
|
64
|
+
{ name: "verifiable proofs", note: "COSE receipts (RFC 9942)" },
|
|
65
65
|
],
|
|
66
66
|
[
|
|
67
67
|
DELEGATION_CERT_LABEL,
|
|
@@ -129,18 +129,19 @@ export const ALG_NAMES: ReadonlyMap<number, string> = new Map([
|
|
|
129
129
|
[-65799, "KS256 (secp256k1 + Keccak-256, forestrie private-use)"],
|
|
130
130
|
[
|
|
131
131
|
-65800,
|
|
132
|
-
"ES256-WebAuthn (forestrie private-use; delegation proofs
|
|
133
|
-
"
|
|
132
|
+
"ES256-WebAuthn (forestrie private-use; delegation proofs, certificates " +
|
|
133
|
+
"and session-key endorsements, never checkpoint-signing)",
|
|
134
134
|
],
|
|
135
135
|
]);
|
|
136
136
|
|
|
137
137
|
/**
|
|
138
|
-
* Verifiable data structure ids (
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* never as registry fact
|
|
138
|
+
* Verifiable data structure ids (the COSE receipts registry, RFC 9942).
|
|
139
|
+
* 3 is NOT a registered codepoint: draft-bryce-cose-receipts-mmr-profile
|
|
140
|
+
* requests TBD_1 (registration pending). The Go sealer emits `395: 3` on
|
|
141
|
+
* every checkpoint and peak receipt; the TypeScript exporters do not, so a
|
|
142
|
+
* verifier must not require it. Render it as the draft's unregistered
|
|
143
|
+
* codepoint, never as registry fact. The display strings are the protocol
|
|
144
|
+
* registry's (spec/label-registry.md).
|
|
144
145
|
*/
|
|
145
146
|
export const VDS_NAMES: ReadonlyMap<number, string> = new Map([
|
|
146
147
|
[1, "RFC9162_SHA256 (Certificate Transparency)"],
|
package/src/lib/delegate-flow.ts
CHANGED
|
@@ -33,6 +33,8 @@ import {
|
|
|
33
33
|
verifyDelegateKeyVoucher,
|
|
34
34
|
} from "./delegate-voucher.js";
|
|
35
35
|
|
|
36
|
+
import { responseProblemDetail } from "./http-problem.js";
|
|
37
|
+
|
|
36
38
|
/** Fixed inclusive MMR start for a horizon delegation lease. */
|
|
37
39
|
const MMR_START = 0;
|
|
38
40
|
|
|
@@ -170,7 +172,7 @@ export async function runDelegateFlow(
|
|
|
170
172
|
const pendingUrl = `${params.coordinatorUrl}/api/logs/${params.logId}/pending-delegation`;
|
|
171
173
|
const pendingRes = await fetchImpl(pendingUrl);
|
|
172
174
|
if (!pendingRes.ok) {
|
|
173
|
-
const preview =
|
|
175
|
+
const preview = await responseProblemDetail(pendingRes, 200);
|
|
174
176
|
throw new DelegateFlowError(
|
|
175
177
|
`pending-delegation fetch failed: HTTP ${pendingRes.status} ${preview}`,
|
|
176
178
|
pendingRes.status,
|
|
@@ -270,7 +272,7 @@ export async function runDelegateFlow(
|
|
|
270
272
|
}),
|
|
271
273
|
});
|
|
272
274
|
if (!submitRes.ok) {
|
|
273
|
-
const preview =
|
|
275
|
+
const preview = await responseProblemDetail(submitRes, 200);
|
|
274
276
|
throw new DelegateFlowError(
|
|
275
277
|
`certificate submit failed: HTTP ${submitRes.status} ${preview}`,
|
|
276
278
|
submitRes.status,
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The human-readable reason a canopy / coordinator request was rejected
|
|
3
|
+
* (FOR-579).
|
|
4
|
+
*
|
|
5
|
+
* canopy-api answers with RFC 9457 problem details encoded as CBOR (served
|
|
6
|
+
* as `application/cbor` on most routes, `application/problem+cbor` on the
|
|
7
|
+
* SCRAPI ones); the delegation coordinator uses `application/problem+json`.
|
|
8
|
+
* Reading such a body with `response.text()` and printing it yields
|
|
9
|
+
* unreadable bytes, which is how a `400` from a strict CBOR decoder hid its
|
|
10
|
+
* cause (`Invalid CBOR body`) through four qualification runs on
|
|
11
|
+
* 2026-09-20. This decodes whichever form arrived and falls back to the
|
|
12
|
+
* text body. It never throws.
|
|
13
|
+
*/
|
|
14
|
+
import { decodeProblemDetailsBytes } from "@forestrie/scrapi-client";
|
|
15
|
+
|
|
16
|
+
/** Longest string returned; problem bodies are short, this is a guard. */
|
|
17
|
+
export const MAX_PROBLEM_DETAIL = 512;
|
|
18
|
+
|
|
19
|
+
function describe(problem: {
|
|
20
|
+
title?: string;
|
|
21
|
+
detail?: string;
|
|
22
|
+
}): string | undefined {
|
|
23
|
+
const { title, detail } = problem;
|
|
24
|
+
if (detail && title) return `${title}: ${detail}`;
|
|
25
|
+
return detail ?? title;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function responseProblemDetail(
|
|
29
|
+
response: Response,
|
|
30
|
+
max = MAX_PROBLEM_DETAIL,
|
|
31
|
+
): Promise<string> {
|
|
32
|
+
const contentType = (response.headers.get("content-type") ?? "").toLowerCase();
|
|
33
|
+
let text = "";
|
|
34
|
+
try {
|
|
35
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
36
|
+
if (bytes.length === 0) return "";
|
|
37
|
+
if (contentType.includes("cbor")) {
|
|
38
|
+
// No content type passed: canopy-api serves most problem bodies as
|
|
39
|
+
// plain application/cbor, so decode opportunistically.
|
|
40
|
+
const problem = decodeProblemDetailsBytes(bytes);
|
|
41
|
+
const described = problem && describe(problem);
|
|
42
|
+
if (described) return described.slice(0, max);
|
|
43
|
+
}
|
|
44
|
+
text = new TextDecoder().decode(bytes);
|
|
45
|
+
if (contentType.includes("json")) {
|
|
46
|
+
const described = describe(JSON.parse(text) as { title?: string; detail?: string });
|
|
47
|
+
if (described) return described.slice(0, max);
|
|
48
|
+
}
|
|
49
|
+
} catch {
|
|
50
|
+
// fall through to whatever text we have
|
|
51
|
+
}
|
|
52
|
+
return text.slice(0, max);
|
|
53
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {
|
|
2
|
+
knownLogKeyFromBytes,
|
|
3
|
+
type RootVerifyKey,
|
|
4
|
+
} from "@forestrie/receipt-verify";
|
|
5
|
+
|
|
6
|
+
/** The accepted `--known-log-key` forms, for error messages and help. */
|
|
7
|
+
export const KNOWN_LOG_KEY_FORMS =
|
|
8
|
+
"base64 x||y (64-byte ES256 public key) or a 20-byte KS256 address (0x + 40 hex, or base64)";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Parse the caller-known log OWNER key (`--known-log-key`). Forms:
|
|
12
|
+
*
|
|
13
|
+
* - base64 of 64 bytes: an ES256 public key, x||y;
|
|
14
|
+
* - `0x` + 40 hex, or base64 of 20 bytes: a KS256 (Ethereum) address.
|
|
15
|
+
*
|
|
16
|
+
* Length alone decides the algorithm (via `knownLogKeyFromBytes`, the same
|
|
17
|
+
* rule univocity applies). Either way the caller ASSERTS the key owns the log.
|
|
18
|
+
*/
|
|
19
|
+
export async function parseKnownLogKey(value: string): Promise<RootVerifyKey> {
|
|
20
|
+
const v = value.trim();
|
|
21
|
+
const bytes = /^0x[0-9a-fA-F]{40}$/.test(v)
|
|
22
|
+
? new Uint8Array(Buffer.from(v.slice(2), "hex"))
|
|
23
|
+
: new Uint8Array(Buffer.from(v, "base64"));
|
|
24
|
+
if (bytes.length !== 64 && bytes.length !== 20) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`--known-log-key must be ${KNOWN_LOG_KEY_FORMS}; decoded ${bytes.length} bytes`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return knownLogKeyFromBytes(bytes);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** True for a KS256 root (a `ParsedKs256RootKey`, not a WebCrypto key). */
|
|
33
|
+
export function isKs256Root(key: RootVerifyKey): boolean {
|
|
34
|
+
return !(key instanceof CryptoKey);
|
|
35
|
+
}
|