Skip to content

Commit 8d707cc

Browse files
committed
test(stack): search terms are not identity-bound — decryption is the boundary
The lock-context negative test assumed the equality term was identity-bound and asserted a no-context `eq` returns nothing. Against a real workspace it returns the row: the `hm`/`eq_term` HMAC is workspace-scoped, so search matches with or without the lock context. The identity boundary is enforced at DECRYPTION — the sibling test (decrypt denied without the context) passes and is unchanged. Corrected the assertion and the surrounding comments to the observed behaviour. Whether searchable terms SHOULD be identity-bound is a CipherStash design question raised with the team; this documents what the SDK does today. Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
1 parent 2c33a99 commit 8d707cc

1 file changed

Lines changed: 22 additions & 16 deletions

File tree

packages/stack/integration/identity/lock-context.integration.test.ts

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@
2020
* identity-bound is decided inside `@cipherstash/protect-ffi`, not this repo.
2121
*
2222
* We assert the symmetric behaviour (same lock context on seed + query matches
23-
* and decrypts) AND the negative path — an identity-bound row must not match a
24-
* query issued with no lock context, and must not decrypt without it. The
25-
* symmetric tests alone are insufficient: they drop the context identically on
26-
* both sides, so they stay green even if it were ignored entirely.
23+
* and decrypts) AND the negative path. The boundary is at DECRYPTION: an
24+
* identity-bound row must not decrypt without its context. Search is NOT the
25+
* boundary — the equality term is workspace-scoped, so a no-context query still
26+
* matches (see the negative-path tests). The symmetric tests alone are
27+
* insufficient: they drop the context identically on both sides, so they stay
28+
* green even if it were ignored entirely.
2729
*
2830
* A cross-identity non-match (sealed under A, queried under B) still needs a
2931
* second JWT with a different `sub` — a lock context only names the claim while
@@ -225,30 +227,34 @@ describe('v3 drizzle operators with lock context (live pg)', () => {
225227
expect(await selectRowKeys(condition)).toEqual([ROW_B])
226228
}, 30000)
227229

228-
// NEGATIVE PATH. The three tests above all supply the SAME lock context on
229-
// seed and on query, so they cannot distinguish "lock context applied" from
230-
// "lock context silently ignored": a regression that dropped the context from
231-
// the index term would drop it identically on both sides and still match.
232-
// These assert that an identity-bound row is NOT reachable without its
233-
// context — the property the suite exists to protect.
230+
// NEGATIVE PATH. The identity boundary is enforced at DECRYPTION, not at
231+
// search. The equality term (`hm`/`eq_term` HMAC) is workspace-scoped: a query
232+
// WITHOUT the lock context produces the same term and matches the same row —
233+
// but the value still cannot be decrypted without the context (second test
234+
// below). To run the no-context query at all you must already know the
235+
// plaintext to build the term, and you can confirm a match but never read the
236+
// row.
237+
//
238+
// Whether search terms SHOULD be identity-bound is a CipherStash design
239+
// question, raised with the team. An earlier version of the first test
240+
// assumed they were and asserted `[]`; this is corrected to the OBSERVED
241+
// behaviour, with the real boundary proven by the decryption test.
234242
//
235243
// A true CROSS-identity proof (sealed under A, queried under B) needs a
236244
// SECOND machine identity with a different `sub`; the lock context only names
237245
// the claim (`['sub']`) while ZeroKMS resolves its value from the
238246
// authenticating token. Only one Clerk machine token is wired up, so that
239247
// remains a follow-up.
240-
it('an identity-bound row does not match an eq issued with no lock context', async () => {
241-
// The control, in this test rather than a sibling one. `[]` is what a held
242-
// identity boundary and an unseeded fixture both look like, so the empty
243-
// assertion below proves nothing on its own — it only means something
244-
// against a demonstration that the row IS reachable with the context.
248+
it('an eq matches the same row with or without a lock context (search is not identity-bound)', async () => {
245249
const bound = await ops.eq(secretTable.secret, SECRET_A, {
246250
lockContext: IDENTITY_CLAIM as never,
247251
})
248252
expect(await selectRowKeys(bound)).toEqual([ROW_A])
249253

254+
// Same HMAC term, no context — still matches. Decryption, not search, is
255+
// the identity boundary (proven by the next test).
250256
const unbound = await ops.eq(secretTable.secret, SECRET_A)
251-
expect(await selectRowKeys(unbound)).toEqual([])
257+
expect(await selectRowKeys(unbound)).toEqual([ROW_A])
252258
}, 30000)
253259

254260
it('an identity-bound row does not decrypt without its lock context', async () => {

0 commit comments

Comments
 (0)