@forestrie/receipt-verify 0.6.1 → 0.6.3

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/dist/index.d.ts CHANGED
@@ -14,6 +14,17 @@ export { peakMMRIndexes } from "@forestrie/merklelog";
14
14
  export { decodeTrustRootFromGenesis } from "./decode-trust-root-from-genesis.js";
15
15
  export { verifyGrantReceiptOffline } from "./verify-grant-receipt-offline.js";
16
16
  export { verifyReceiptOffline } from "./verify-grant-receipt-offline.js";
17
+ /**
18
+ * Caller-supplied trust anchors (FOR-297 "known log key"): verify offline
19
+ * under keys the caller trusts out of band instead of the genesis trust root.
20
+ * See the trust-ladder notes on verifyReceiptOfflineWithKeys.
21
+ */
22
+ export { verifyGrantReceiptOfflineWithKeys, verifyReceiptOfflineWithKeys, } from "./verify-grant-receipt-offline.js";
23
+ export type { VerifyGrantReceiptOfflineWithKeysInput, VerifyReceiptOfflineWithKeysInput, } from "./verify-grant-receipt-offline.js";
24
+ export { resolveDelegatedVerifyKey } from "./resolve-delegated-verify-key.js";
25
+ export type { DelegatedResolution } from "./resolve-delegated-verify-key.js";
26
+ /** Import a raw 64-byte x||y P-256 public key as an ES256 verify key. */
27
+ export { importEs256PublicKeyFromGrantDataXy64 } from "./decode-trust-root-cbor.js";
17
28
  export { decodeForestrieGrantCose } from "./decode-forestrie-grant-cose.js";
18
29
  export { decodeGrantPayload, decodeGrantResponse } from "./grant-codec.js";
19
30
  /** Deterministic receipt construction (plan-2607-12 Phase 2, FOR-353). */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,4BAA4B,CAAC;AACpC,oFAAoF;AACpF,YAAY,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AACjD,YAAY,EAAE,8BAA8B,EAAE,MAAM,mCAAmC,CAAC;AACxF,YAAY,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AACnF,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,YAAY,EACV,wBAAwB,EACxB,uBAAuB,EACvB,eAAe,EACf,gBAAgB,GACjB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,mBAAmB,EACnB,sBAAsB,EACtB,mBAAmB,EACnB,eAAe,GAChB,MAAM,4BAA4B,CAAC;AACpC;;;GAGG;AACH,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,0BAA0B,EAAE,MAAM,qCAAqC,CAAC;AACjF,OAAO,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,OAAO,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AACzE,OAAO,EAAE,wBAAwB,EAAE,MAAM,kCAAkC,CAAC;AAC5E,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAE3E,0EAA0E;AAC1E,OAAO,EACL,cAAc,EACd,iDAAiD,GAClD,MAAM,2CAA2C,CAAC;AACnD,OAAO,EAAE,gBAAgB,EAAE,0BAA0B,EAAE,MAAM,eAAe,CAAC;AAC7E,YAAY,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAEpD,6EAA6E;AAC7E,OAAO,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AACrE,oEAAoE;AACpE,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,YAAY,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxD,8EAA8E;AAC9E,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,mBAAmB,EACnB,kBAAkB,GACnB,MAAM,4BAA4B,CAAC;AACpC,oFAAoF;AACpF,YAAY,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AACjD,YAAY,EAAE,8BAA8B,EAAE,MAAM,mCAAmC,CAAC;AACxF,YAAY,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AACnF,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,YAAY,EACV,wBAAwB,EACxB,uBAAuB,EACvB,eAAe,EACf,gBAAgB,GACjB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,mBAAmB,EACnB,sBAAsB,EACtB,mBAAmB,EACnB,eAAe,GAChB,MAAM,4BAA4B,CAAC;AACpC;;;GAGG;AACH,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,0BAA0B,EAAE,MAAM,qCAAqC,CAAC;AACjF,OAAO,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,OAAO,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AACzE;;;;GAIG;AACH,OAAO,EACL,iCAAiC,EACjC,4BAA4B,GAC7B,MAAM,mCAAmC,CAAC;AAC3C,YAAY,EACV,sCAAsC,EACtC,iCAAiC,GAClC,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAE,yBAAyB,EAAE,MAAM,mCAAmC,CAAC;AAC9E,YAAY,EAAE,mBAAmB,EAAE,MAAM,mCAAmC,CAAC;AAC7E,yEAAyE;AACzE,OAAO,EAAE,qCAAqC,EAAE,MAAM,6BAA6B,CAAC;AACpF,OAAO,EAAE,wBAAwB,EAAE,MAAM,kCAAkC,CAAC;AAC5E,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAE3E,0EAA0E;AAC1E,OAAO,EACL,cAAc,EACd,iDAAiD,GAClD,MAAM,2CAA2C,CAAC;AACnD,OAAO,EAAE,gBAAgB,EAAE,0BAA0B,EAAE,MAAM,eAAe,CAAC;AAC7E,YAAY,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAEpD,6EAA6E;AAC7E,OAAO,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AACrE,oEAAoE;AACpE,OAAO,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAC7D,YAAY,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxD,8EAA8E;AAC9E,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC"}
package/dist/index.js CHANGED
@@ -8,6 +8,15 @@ export { peakMMRIndexes } from "@forestrie/merklelog";
8
8
  export { decodeTrustRootFromGenesis } from "./decode-trust-root-from-genesis.js";
9
9
  export { verifyGrantReceiptOffline } from "./verify-grant-receipt-offline.js";
10
10
  export { verifyReceiptOffline } from "./verify-grant-receipt-offline.js";
11
+ /**
12
+ * Caller-supplied trust anchors (FOR-297 "known log key"): verify offline
13
+ * under keys the caller trusts out of band instead of the genesis trust root.
14
+ * See the trust-ladder notes on verifyReceiptOfflineWithKeys.
15
+ */
16
+ export { verifyGrantReceiptOfflineWithKeys, verifyReceiptOfflineWithKeys, } from "./verify-grant-receipt-offline.js";
17
+ export { resolveDelegatedVerifyKey } from "./resolve-delegated-verify-key.js";
18
+ /** Import a raw 64-byte x||y P-256 public key as an ES256 verify key. */
19
+ export { importEs256PublicKeyFromGrantDataXy64 } from "./decode-trust-root-cbor.js";
11
20
  export { decodeForestrieGrantCose } from "./decode-forestrie-grant-cose.js";
12
21
  export { decodeGrantPayload, decodeGrantResponse } from "./grant-codec.js";
13
22
  /** Deterministic receipt construction (plan-2607-12 Phase 2, FOR-353). */
@@ -10,6 +10,16 @@
10
10
  * sealer delegate keys are always secp256r1. KS256-rooted delegation is a
11
11
  * server-only concern.
12
12
  *
13
+ * Trust anchors (FOR-297 trust ladder): `rootKeys` need not be genesis-derived
14
+ * — a caller-known log OWNER key works identically (see
15
+ * `verifyReceiptOfflineWithKeys`), because the certificate issuer for a child
16
+ * log IS its owner. What differs is why you trust the keys: genesis-derived
17
+ * keys prove the key↔log binding from the bootstrap; a caller-known key merely
18
+ * asserts it (provenance of the key is the only defence, and there is no grant
19
+ * lifecycle visibility or split-view protection). The grant-chain walk
20
+ * (approach A, open) will derive per-log owner keys from genesis + public
21
+ * tiles, closing that gap without key distribution.
22
+ *
13
23
  * Note (parity with the server port source): this establishes that the root
14
24
  * authorized the delegated key, but does NOT yet enforce the certificate's MMR
15
25
  * window or expiry-at-issuance against the leaf — that hardening is shared with
@@ -1 +1 @@
1
- {"version":3,"file":"resolve-delegated-verify-key.d.ts","sourceRoot":"","sources":["../src/resolve-delegated-verify-key.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAuBH;;;;;GAKG;AACH,MAAM,MAAM,mBAAmB,GAC3B;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,GACrB;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,YAAY,EAAE,SAAS,CAAA;CAAE,GAC7C;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC;AAqEvB;;;;;GAKG;AACH,wBAAsB,yBAAyB,CAC7C,WAAW,EAAE,UAAU,EACvB,QAAQ,EAAE,SAAS,EAAE,GACpB,OAAO,CAAC,mBAAmB,CAAC,CA4B9B"}
1
+ {"version":3,"file":"resolve-delegated-verify-key.d.ts","sourceRoot":"","sources":["../src/resolve-delegated-verify-key.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAuBH;;;;;GAKG;AACH,MAAM,MAAM,mBAAmB,GAC3B;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,GACrB;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,YAAY,EAAE,SAAS,CAAA;CAAE,GAC7C;IAAE,IAAI,EAAE,QAAQ,CAAA;CAAE,CAAC;AAqEvB;;;;;GAKG;AACH,wBAAsB,yBAAyB,CAC7C,WAAW,EAAE,UAAU,EACvB,QAAQ,EAAE,SAAS,EAAE,GACpB,OAAO,CAAC,mBAAmB,CAAC,CA4B9B"}
@@ -10,6 +10,16 @@
10
10
  * sealer delegate keys are always secp256r1. KS256-rooted delegation is a
11
11
  * server-only concern.
12
12
  *
13
+ * Trust anchors (FOR-297 trust ladder): `rootKeys` need not be genesis-derived
14
+ * — a caller-known log OWNER key works identically (see
15
+ * `verifyReceiptOfflineWithKeys`), because the certificate issuer for a child
16
+ * log IS its owner. What differs is why you trust the keys: genesis-derived
17
+ * keys prove the key↔log binding from the bootstrap; a caller-known key merely
18
+ * asserts it (provenance of the key is the only defence, and there is no grant
19
+ * lifecycle visibility or split-view protection). The grant-chain walk
20
+ * (approach A, open) will derive per-log owner keys from genesis + public
21
+ * tiles, closing that gap without key distribution.
22
+ *
13
23
  * Note (parity with the server port source): this establishes that the root
14
24
  * authorized the delegated key, but does NOT yet enforce the certificate's MMR
15
25
  * window or expiry-at-issuance against the leaf — that hardening is shared with
@@ -10,6 +10,16 @@ export type VerifyGrantReceiptOfflineInput = {
10
10
  * Offline grant receipt verify (layers A–C, ADR-0045). Pure over bytes; no network.
11
11
  */
12
12
  export declare function verifyGrantReceiptOffline(input: VerifyGrantReceiptOfflineInput): Promise<ReceiptVerifyResult>;
13
+ export type VerifyGrantReceiptOfflineWithKeysInput = Omit<VerifyGrantReceiptOfflineInput, "genesisCbor"> & {
14
+ /** Caller-known ES256 trust keys — see {@link VerifyReceiptOfflineWithKeysInput}. */
15
+ trustKeys: CryptoKey[];
16
+ };
17
+ /**
18
+ * {@link verifyGrantReceiptOffline} under caller-supplied trust keys instead of
19
+ * the genesis trust root — see {@link verifyReceiptOfflineWithKeys} for the
20
+ * trust model this buys (and what it does not).
21
+ */
22
+ export declare function verifyGrantReceiptOfflineWithKeys(input: VerifyGrantReceiptOfflineWithKeysInput): Promise<ReceiptVerifyResult>;
13
23
  export type VerifyReceiptOfflineInput = {
14
24
  genesisCbor: Uint8Array;
15
25
  receiptCbor: Uint8Array;
@@ -30,4 +40,31 @@ export type VerifyReceiptOfflineInput = {
30
40
  * resolution, inclusion, and signature are all standard.
31
41
  */
32
42
  export declare function verifyReceiptOffline(input: VerifyReceiptOfflineInput): Promise<ReceiptVerifyResult>;
43
+ export type VerifyReceiptOfflineWithKeysInput = Omit<VerifyReceiptOfflineInput, "genesisCbor"> & {
44
+ /**
45
+ * Caller-known ES256 trust keys, tried in order. For a child-log receipt
46
+ * this is the log OWNER's key (the delegation-cert issuer), NOT the sealer
47
+ * key — the label-1000 cert is verified under these keys and the delegated
48
+ * sealer key is extracted from it, so the anchor survives sealer rotation.
49
+ */
50
+ trustKeys: CryptoKey[];
51
+ };
52
+ /**
53
+ * {@link verifyReceiptOffline} under caller-supplied trust keys instead of the
54
+ * genesis trust root (FOR-297 "known log key"). Standard SCITT relying-party
55
+ * posture: the caller obtained the log owner's key out of band and trusts it.
56
+ *
57
+ * Trust ladder — what this rung gives up relative to its neighbours:
58
+ * - Known log key (this entry): fully offline, but the "key K owns log L"
59
+ * binding is ASSERTED by the caller's key provenance, not proven; no grant
60
+ * lifecycle/expiry visibility; no split-view protection.
61
+ * - Grant-chain walk (approach A, open): derives the binding from
62
+ * `genesis.cbor` + public tiles and adds lifecycle visibility.
63
+ * - Chain-anchored (live or cached accumulator): adds split-view protection —
64
+ * the contract-enforced on-chain accumulator is the strongest anchor.
65
+ *
66
+ * Do NOT fetch the trust key from the log operator's API or tile store — that
67
+ * silently reintroduces the operator trust this entry exists to remove.
68
+ */
69
+ export declare function verifyReceiptOfflineWithKeys(input: VerifyReceiptOfflineWithKeysInput): Promise<ReceiptVerifyResult>;
33
70
  //# sourceMappingURL=verify-grant-receipt-offline.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"verify-grant-receipt-offline.d.ts","sourceRoot":"","sources":["../src/verify-grant-receipt-offline.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAOjD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAGtE,MAAM,MAAM,8BAA8B,GAAG;IAC3C,WAAW,EAAE,UAAU,CAAC;IACxB,WAAW,EAAE,UAAU,CAAC;IACxB,KAAK,EAAE,KAAK,CAAC;IACb,cAAc,EAAE,UAAU,CAAC;CAC5B,CAAC;AA4EF;;GAEG;AACH,wBAAsB,yBAAyB,CAC7C,KAAK,EAAE,8BAA8B,GACpC,OAAO,CAAC,mBAAmB,CAAC,CAc9B;AAED,MAAM,MAAM,yBAAyB,GAAG;IACtC,WAAW,EAAE,UAAU,CAAC;IACxB,WAAW,EAAE,UAAU,CAAC;IACxB;;;;;OAKG;IACH,OAAO,EAAE,UAAU,CAAC;IACpB,cAAc,EAAE,UAAU,CAAC;CAC5B,CAAC;AAEF;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,yBAAyB,GAC/B,OAAO,CAAC,mBAAmB,CAAC,CAa9B"}
1
+ {"version":3,"file":"verify-grant-receipt-offline.d.ts","sourceRoot":"","sources":["../src/verify-grant-receipt-offline.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAOjD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAGtE,MAAM,MAAM,8BAA8B,GAAG;IAC3C,WAAW,EAAE,UAAU,CAAC;IACxB,WAAW,EAAE,UAAU,CAAC;IACxB,KAAK,EAAE,KAAK,CAAC;IACb,cAAc,EAAE,UAAU,CAAC;CAC5B,CAAC;AA4EF;;GAEG;AACH,wBAAsB,yBAAyB,CAC7C,KAAK,EAAE,8BAA8B,GACpC,OAAO,CAAC,mBAAmB,CAAC,CAc9B;AAED,MAAM,MAAM,sCAAsC,GAAG,IAAI,CACvD,8BAA8B,EAC9B,aAAa,CACd,GAAG;IACF,qFAAqF;IACrF,SAAS,EAAE,SAAS,EAAE,CAAC;CACxB,CAAC;AAEF;;;;GAIG;AACH,wBAAsB,iCAAiC,CACrD,KAAK,EAAE,sCAAsC,GAC5C,OAAO,CAAC,mBAAmB,CAAC,CAa9B;AAED,MAAM,MAAM,yBAAyB,GAAG;IACtC,WAAW,EAAE,UAAU,CAAC;IACxB,WAAW,EAAE,UAAU,CAAC;IACxB;;;;;OAKG;IACH,OAAO,EAAE,UAAU,CAAC;IACpB,cAAc,EAAE,UAAU,CAAC;CAC5B,CAAC;AAEF;;;;;;GAMG;AACH,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,yBAAyB,GAC/B,OAAO,CAAC,mBAAmB,CAAC,CAa9B;AAED,MAAM,MAAM,iCAAiC,GAAG,IAAI,CAClD,yBAAyB,EACzB,aAAa,CACd,GAAG;IACF;;;;;OAKG;IACH,SAAS,EAAE,SAAS,EAAE,CAAC;CACxB,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,4BAA4B,CAChD,KAAK,EAAE,iCAAiC,GACvC,OAAO,CAAC,mBAAmB,CAAC,CAa9B"}
@@ -74,6 +74,26 @@ export async function verifyGrantReceiptOffline(input) {
74
74
  inner,
75
75
  });
76
76
  }
77
+ /**
78
+ * {@link verifyGrantReceiptOffline} under caller-supplied trust keys instead of
79
+ * the genesis trust root — see {@link verifyReceiptOfflineWithKeys} for the
80
+ * trust model this buys (and what it does not).
81
+ */
82
+ export async function verifyGrantReceiptOfflineWithKeys(input) {
83
+ let inner;
84
+ try {
85
+ inner = await grantCommitmentHashFromGrant(input.grant);
86
+ }
87
+ catch {
88
+ return { ok: false, stage: "binding", reason: "grant_invalid" };
89
+ }
90
+ return verifyReceiptOfflineWithLeafInnerKeys({
91
+ receiptCbor: input.receiptCbor,
92
+ idtimestampBe8: input.idtimestampBe8,
93
+ inner,
94
+ trustKeys: input.trustKeys,
95
+ });
96
+ }
77
97
  /**
78
98
  * Offline verify of a receipt against the EXACT registered payload. The log
79
99
  * leaf commits `SHA-256(idtimestamp ‖ SHA-256(payload))`; this is the standard,
@@ -90,6 +110,32 @@ export async function verifyReceiptOffline(input) {
90
110
  inner,
91
111
  });
92
112
  }
113
+ /**
114
+ * {@link verifyReceiptOffline} under caller-supplied trust keys instead of the
115
+ * genesis trust root (FOR-297 "known log key"). Standard SCITT relying-party
116
+ * posture: the caller obtained the log owner's key out of band and trusts it.
117
+ *
118
+ * Trust ladder — what this rung gives up relative to its neighbours:
119
+ * - Known log key (this entry): fully offline, but the "key K owns log L"
120
+ * binding is ASSERTED by the caller's key provenance, not proven; no grant
121
+ * lifecycle/expiry visibility; no split-view protection.
122
+ * - Grant-chain walk (approach A, open): derives the binding from
123
+ * `genesis.cbor` + public tiles and adds lifecycle visibility.
124
+ * - Chain-anchored (live or cached accumulator): adds split-view protection —
125
+ * the contract-enforced on-chain accumulator is the strongest anchor.
126
+ *
127
+ * Do NOT fetch the trust key from the log operator's API or tile store — that
128
+ * silently reintroduces the operator trust this entry exists to remove.
129
+ */
130
+ export async function verifyReceiptOfflineWithKeys(input) {
131
+ const inner = new Uint8Array(await crypto.subtle.digest("SHA-256", input.payload));
132
+ return verifyReceiptOfflineWithLeafInnerKeys({
133
+ receiptCbor: input.receiptCbor,
134
+ idtimestampBe8: input.idtimestampBe8,
135
+ inner,
136
+ trustKeys: input.trustKeys,
137
+ });
138
+ }
93
139
  /**
94
140
  * Shared offline-verify core: reconstruct the leaf as
95
141
  * `univocityLeafHash(idtimestamp, inner)`, resolve the (possibly delegated,
@@ -98,13 +144,6 @@ export async function verifyReceiptOffline(input) {
98
144
  * commitment for a grant leaf, or `SHA-256(statement)` for a statement leaf.
99
145
  */
100
146
  async function verifyReceiptOfflineWithLeafInner(input) {
101
- let parsed;
102
- try {
103
- parsed = parseReceipt(input.receiptCbor);
104
- }
105
- catch {
106
- return { ok: false, stage: "parse", reason: "receipt_malformed" };
107
- }
108
147
  let trustRoot;
109
148
  try {
110
149
  trustRoot = await decodeTrustRootFromGenesis(input.genesisCbor);
@@ -116,18 +155,41 @@ async function verifyReceiptOfflineWithLeafInner(input) {
116
155
  if (!verifyKeys.length) {
117
156
  return { ok: false, stage: "signature", reason: "no_es256_trust_key" };
118
157
  }
158
+ return verifyReceiptOfflineWithLeafInnerKeys({
159
+ receiptCbor: input.receiptCbor,
160
+ idtimestampBe8: input.idtimestampBe8,
161
+ inner: input.inner,
162
+ trustKeys: verifyKeys,
163
+ });
164
+ }
165
+ /**
166
+ * Trust-key-parameterised offline-verify core. Identical to the genesis path
167
+ * once the trust keys are in hand: resolve the (possibly delegated, FOR-297)
168
+ * verify key against `trustKeys`, then check signature + inclusion.
169
+ */
170
+ async function verifyReceiptOfflineWithLeafInnerKeys(input) {
171
+ let parsed;
172
+ try {
173
+ parsed = parseReceipt(input.receiptCbor);
174
+ }
175
+ catch {
176
+ return { ok: false, stage: "parse", reason: "receipt_malformed" };
177
+ }
178
+ if (!input.trustKeys.length) {
179
+ return { ok: false, stage: "signature", reason: "no_es256_trust_key" };
180
+ }
119
181
  // FOR-297: when the receipt was signed by a DELEGATED key, verify the
120
- // label-1000 delegation certificate under the root and try the delegated key
121
- // first. A cert present but not chaining to the root is a hard failure — do
122
- // not silently fall back to the root key (which cannot verify a
123
- // delegated-signed receipt anyway).
124
- const delegation = await resolveDelegatedVerifyKey(input.receiptCbor, verifyKeys);
182
+ // label-1000 delegation certificate under the trust keys and try the
183
+ // delegated key first. A cert present but not chaining to a trust key is a
184
+ // hard failure — do not silently fall back to the trust key (which cannot
185
+ // verify a delegated-signed receipt anyway).
186
+ const delegation = await resolveDelegatedVerifyKey(input.receiptCbor, input.trustKeys);
125
187
  if (delegation.kind === "broken") {
126
188
  return { ok: false, stage: "signature", reason: "delegation_invalid" };
127
189
  }
128
190
  const allVerifyKeys = delegation.kind === "resolved"
129
- ? [delegation.delegatedKey, ...verifyKeys]
130
- : verifyKeys;
191
+ ? [delegation.delegatedKey, ...input.trustKeys]
192
+ : input.trustKeys;
131
193
  let idtimestamp;
132
194
  try {
133
195
  idtimestamp = readIdtimestampBe8(input.idtimestampBe8);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forestrie/receipt-verify",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "Offline SCITT grant receipt verification (ADR-0045)",
@@ -28,7 +28,7 @@
28
28
  ],
29
29
  "dependencies": {
30
30
  "@noble/hashes": "^1.7.1",
31
- "@forestrie/encoding": "0.3.0",
31
+ "@forestrie/encoding": "0.5.0",
32
32
  "@forestrie/merklelog": "0.1.1"
33
33
  },
34
34
  "devDependencies": {
package/src/index.ts CHANGED
@@ -27,6 +27,23 @@ export { peakMMRIndexes } from "@forestrie/merklelog";
27
27
  export { decodeTrustRootFromGenesis } from "./decode-trust-root-from-genesis.js";
28
28
  export { verifyGrantReceiptOffline } from "./verify-grant-receipt-offline.js";
29
29
  export { verifyReceiptOffline } from "./verify-grant-receipt-offline.js";
30
+ /**
31
+ * Caller-supplied trust anchors (FOR-297 "known log key"): verify offline
32
+ * under keys the caller trusts out of band instead of the genesis trust root.
33
+ * See the trust-ladder notes on verifyReceiptOfflineWithKeys.
34
+ */
35
+ export {
36
+ verifyGrantReceiptOfflineWithKeys,
37
+ verifyReceiptOfflineWithKeys,
38
+ } from "./verify-grant-receipt-offline.js";
39
+ export type {
40
+ VerifyGrantReceiptOfflineWithKeysInput,
41
+ VerifyReceiptOfflineWithKeysInput,
42
+ } from "./verify-grant-receipt-offline.js";
43
+ export { resolveDelegatedVerifyKey } from "./resolve-delegated-verify-key.js";
44
+ export type { DelegatedResolution } from "./resolve-delegated-verify-key.js";
45
+ /** Import a raw 64-byte x||y P-256 public key as an ES256 verify key. */
46
+ export { importEs256PublicKeyFromGrantDataXy64 } from "./decode-trust-root-cbor.js";
30
47
  export { decodeForestrieGrantCose } from "./decode-forestrie-grant-cose.js";
31
48
  export { decodeGrantPayload, decodeGrantResponse } from "./grant-codec.js";
32
49
 
@@ -10,6 +10,16 @@
10
10
  * sealer delegate keys are always secp256r1. KS256-rooted delegation is a
11
11
  * server-only concern.
12
12
  *
13
+ * Trust anchors (FOR-297 trust ladder): `rootKeys` need not be genesis-derived
14
+ * — a caller-known log OWNER key works identically (see
15
+ * `verifyReceiptOfflineWithKeys`), because the certificate issuer for a child
16
+ * log IS its owner. What differs is why you trust the keys: genesis-derived
17
+ * keys prove the key↔log binding from the bootstrap; a caller-known key merely
18
+ * asserts it (provenance of the key is the only defence, and there is no grant
19
+ * lifecycle visibility or split-view protection). The grant-chain walk
20
+ * (approach A, open) will derive per-log owner keys from genesis + public
21
+ * tiles, closing that gap without key distribution.
22
+ *
13
23
  * Note (parity with the server port source): this establishes that the root
14
24
  * authorized the delegated key, but does NOT yet enforce the certificate's MMR
15
25
  * window or expiry-at-issuance against the leaf — that hardening is shared with
@@ -116,6 +116,36 @@ export async function verifyGrantReceiptOffline(
116
116
  });
117
117
  }
118
118
 
119
+ export type VerifyGrantReceiptOfflineWithKeysInput = Omit<
120
+ VerifyGrantReceiptOfflineInput,
121
+ "genesisCbor"
122
+ > & {
123
+ /** Caller-known ES256 trust keys — see {@link VerifyReceiptOfflineWithKeysInput}. */
124
+ trustKeys: CryptoKey[];
125
+ };
126
+
127
+ /**
128
+ * {@link verifyGrantReceiptOffline} under caller-supplied trust keys instead of
129
+ * the genesis trust root — see {@link verifyReceiptOfflineWithKeys} for the
130
+ * trust model this buys (and what it does not).
131
+ */
132
+ export async function verifyGrantReceiptOfflineWithKeys(
133
+ input: VerifyGrantReceiptOfflineWithKeysInput,
134
+ ): Promise<ReceiptVerifyResult> {
135
+ let inner: Uint8Array;
136
+ try {
137
+ inner = await grantCommitmentHashFromGrant(input.grant);
138
+ } catch {
139
+ return { ok: false, stage: "binding", reason: "grant_invalid" };
140
+ }
141
+ return verifyReceiptOfflineWithLeafInnerKeys({
142
+ receiptCbor: input.receiptCbor,
143
+ idtimestampBe8: input.idtimestampBe8,
144
+ inner,
145
+ trustKeys: input.trustKeys,
146
+ });
147
+ }
148
+
119
149
  export type VerifyReceiptOfflineInput = {
120
150
  genesisCbor: Uint8Array;
121
151
  receiptCbor: Uint8Array;
@@ -153,6 +183,53 @@ export async function verifyReceiptOffline(
153
183
  });
154
184
  }
155
185
 
186
+ export type VerifyReceiptOfflineWithKeysInput = Omit<
187
+ VerifyReceiptOfflineInput,
188
+ "genesisCbor"
189
+ > & {
190
+ /**
191
+ * Caller-known ES256 trust keys, tried in order. For a child-log receipt
192
+ * this is the log OWNER's key (the delegation-cert issuer), NOT the sealer
193
+ * key — the label-1000 cert is verified under these keys and the delegated
194
+ * sealer key is extracted from it, so the anchor survives sealer rotation.
195
+ */
196
+ trustKeys: CryptoKey[];
197
+ };
198
+
199
+ /**
200
+ * {@link verifyReceiptOffline} under caller-supplied trust keys instead of the
201
+ * genesis trust root (FOR-297 "known log key"). Standard SCITT relying-party
202
+ * posture: the caller obtained the log owner's key out of band and trusts it.
203
+ *
204
+ * Trust ladder — what this rung gives up relative to its neighbours:
205
+ * - Known log key (this entry): fully offline, but the "key K owns log L"
206
+ * binding is ASSERTED by the caller's key provenance, not proven; no grant
207
+ * lifecycle/expiry visibility; no split-view protection.
208
+ * - Grant-chain walk (approach A, open): derives the binding from
209
+ * `genesis.cbor` + public tiles and adds lifecycle visibility.
210
+ * - Chain-anchored (live or cached accumulator): adds split-view protection —
211
+ * the contract-enforced on-chain accumulator is the strongest anchor.
212
+ *
213
+ * Do NOT fetch the trust key from the log operator's API or tile store — that
214
+ * silently reintroduces the operator trust this entry exists to remove.
215
+ */
216
+ export async function verifyReceiptOfflineWithKeys(
217
+ input: VerifyReceiptOfflineWithKeysInput,
218
+ ): Promise<ReceiptVerifyResult> {
219
+ const inner = new Uint8Array(
220
+ await crypto.subtle.digest(
221
+ "SHA-256",
222
+ input.payload as unknown as BufferSource,
223
+ ),
224
+ );
225
+ return verifyReceiptOfflineWithLeafInnerKeys({
226
+ receiptCbor: input.receiptCbor,
227
+ idtimestampBe8: input.idtimestampBe8,
228
+ inner,
229
+ trustKeys: input.trustKeys,
230
+ });
231
+ }
232
+
156
233
  /**
157
234
  * Shared offline-verify core: reconstruct the leaf as
158
235
  * `univocityLeafHash(idtimestamp, inner)`, resolve the (possibly delegated,
@@ -166,13 +243,6 @@ async function verifyReceiptOfflineWithLeafInner(input: {
166
243
  idtimestampBe8: Uint8Array;
167
244
  inner: Uint8Array;
168
245
  }): Promise<ReceiptVerifyResult> {
169
- let parsed: ReturnType<typeof parseReceipt>;
170
- try {
171
- parsed = parseReceipt(input.receiptCbor);
172
- } catch {
173
- return { ok: false, stage: "parse", reason: "receipt_malformed" };
174
- }
175
-
176
246
  let trustRoot;
177
247
  try {
178
248
  trustRoot = await decodeTrustRootFromGenesis(input.genesisCbor);
@@ -185,22 +255,52 @@ async function verifyReceiptOfflineWithLeafInner(input: {
185
255
  return { ok: false, stage: "signature", reason: "no_es256_trust_key" };
186
256
  }
187
257
 
258
+ return verifyReceiptOfflineWithLeafInnerKeys({
259
+ receiptCbor: input.receiptCbor,
260
+ idtimestampBe8: input.idtimestampBe8,
261
+ inner: input.inner,
262
+ trustKeys: verifyKeys,
263
+ });
264
+ }
265
+
266
+ /**
267
+ * Trust-key-parameterised offline-verify core. Identical to the genesis path
268
+ * once the trust keys are in hand: resolve the (possibly delegated, FOR-297)
269
+ * verify key against `trustKeys`, then check signature + inclusion.
270
+ */
271
+ async function verifyReceiptOfflineWithLeafInnerKeys(input: {
272
+ receiptCbor: Uint8Array;
273
+ idtimestampBe8: Uint8Array;
274
+ inner: Uint8Array;
275
+ trustKeys: CryptoKey[];
276
+ }): Promise<ReceiptVerifyResult> {
277
+ let parsed: ReturnType<typeof parseReceipt>;
278
+ try {
279
+ parsed = parseReceipt(input.receiptCbor);
280
+ } catch {
281
+ return { ok: false, stage: "parse", reason: "receipt_malformed" };
282
+ }
283
+
284
+ if (!input.trustKeys.length) {
285
+ return { ok: false, stage: "signature", reason: "no_es256_trust_key" };
286
+ }
287
+
188
288
  // FOR-297: when the receipt was signed by a DELEGATED key, verify the
189
- // label-1000 delegation certificate under the root and try the delegated key
190
- // first. A cert present but not chaining to the root is a hard failure — do
191
- // not silently fall back to the root key (which cannot verify a
192
- // delegated-signed receipt anyway).
289
+ // label-1000 delegation certificate under the trust keys and try the
290
+ // delegated key first. A cert present but not chaining to a trust key is a
291
+ // hard failure — do not silently fall back to the trust key (which cannot
292
+ // verify a delegated-signed receipt anyway).
193
293
  const delegation = await resolveDelegatedVerifyKey(
194
294
  input.receiptCbor,
195
- verifyKeys,
295
+ input.trustKeys,
196
296
  );
197
297
  if (delegation.kind === "broken") {
198
298
  return { ok: false, stage: "signature", reason: "delegation_invalid" };
199
299
  }
200
300
  const allVerifyKeys =
201
301
  delegation.kind === "resolved"
202
- ? [delegation.delegatedKey, ...verifyKeys]
203
- : verifyKeys;
302
+ ? [delegation.delegatedKey, ...input.trustKeys]
303
+ : input.trustKeys;
204
304
 
205
305
  let idtimestamp: bigint;
206
306
  try {