Skip to content

Commit 23a5ba7

Browse files
committed
test(golang): host refusals around a target field, and a tampered EQL value
Review of #1095: the errNoEQL branch in Open and the two field-selection refusals in Query ran in no test, and no Go test opened a changed EQL value. Over the uninitialised guest, Open refuses a target field with no EQL value, with a ciphertext where the value should be, and missing, naming the field before the guest is asked; Query refuses a field the plan does not have and a field that names no EQL type the same way. Over the deterministic eql build, a TextEq value moved to another column does not open, bytes that are not a TextEq (an empty object, non-JSON, a query value) are ErrEncoding, a flipped ciphertext byte fails authentication, and the untouched value still opens. Claude-Session: https://claude.ai/code/session_01V3WFXwax4J3uecpFEJ6yHc
1 parent 535d23b commit 23a5ba7

2 files changed

Lines changed: 111 additions & 0 deletions

File tree

‎languages/golang/encrypt/eql_test.go‎

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,82 @@ func TestTextEqQueryMatchesTheStoredValueAndTheRustFixture(t *testing.T) {
189189
}
190190
}
191191

192+
// A stored EQL value changed before it is opened: moved to another column,
193+
// or replaced with bytes that are not a TextEq. Neither opens, and the
194+
// second is refused as malformed input before any key is retrieved.
195+
func TestATamperedEQLValueDoesNotOpen(t *testing.T) {
196+
c := deterministicEQLClient(t)
197+
ctx := context.Background()
198+
cipher := c.DefaultKeyset()
199+
200+
moved, err := testusers.EncryptContact(ctx, cipher, contacts[:1])
201+
if err != nil {
202+
t.Fatal(err)
203+
}
204+
original := moved[0].Email
205+
moved[0].Email = eql.TextEq(bytes.Replace(original, []byte(`"c":"email"`), []byte(`"c":"notes"`), 1))
206+
if bytes.Equal(moved[0].Email, original) {
207+
t.Fatal("the stored identifier was not where the test expected it")
208+
}
209+
if _, err := testusers.DecryptContact(ctx, cipher, moved); err == nil {
210+
t.Fatal("an EQL value moved to another column opened")
211+
}
212+
213+
junk, err := testusers.EncryptContact(ctx, cipher, contacts[:1])
214+
if err != nil {
215+
t.Fatal(err)
216+
}
217+
for name, value := range map[string]eql.TextEq{
218+
"an empty object": eql.TextEq(`{}`),
219+
"not JSON": eql.TextEq(`not json`),
220+
"a query value": eql.TextEq(`{"v":3,"i":{"t":"users","c":"email"},"hm":"00"}`),
221+
} {
222+
junk[0].Email = value
223+
if _, err := testusers.DecryptContact(ctx, cipher, junk); !errors.Is(err, encrypt.ErrEncoding) {
224+
t.Errorf("%s: Decrypt = %v, want ErrEncoding", name, err)
225+
}
226+
}
227+
228+
// One flipped byte in the ciphertext inside the value: authenticated,
229+
// so it does not open; the untouched value still does.
230+
flipped, err := testusers.EncryptContact(ctx, cipher, contacts[:1])
231+
if err != nil {
232+
t.Fatal(err)
233+
}
234+
var doc map[string]json.RawMessage
235+
if err := json.Unmarshal(flipped[0].Email, &doc); err != nil {
236+
t.Fatal(err)
237+
}
238+
var ct string
239+
if err := json.Unmarshal(doc["c"], &ct); err != nil {
240+
t.Fatal(err)
241+
}
242+
last := []byte(ct)
243+
if last[len(last)-2] == 'A' {
244+
last[len(last)-2] = 'B'
245+
} else {
246+
last[len(last)-2] = 'A'
247+
}
248+
doc["c"], _ = json.Marshal(string(last))
249+
tampered, _ := json.Marshal(doc)
250+
flipped[0].Email = eql.TextEq(tampered)
251+
if _, err := testusers.DecryptContact(ctx, cipher, flipped); err == nil {
252+
t.Fatal("a tampered ciphertext opened")
253+
}
254+
if _, err := testusers.DecryptContact(ctx, cipher, contactsSealed(t, ctx, cipher)); err != nil {
255+
t.Fatalf("the untouched value no longer opens: %v", err)
256+
}
257+
}
258+
259+
func contactsSealed(t *testing.T, ctx context.Context, cipher *encrypt.Cipher) []testusers.EncryptedContact {
260+
t.Helper()
261+
sealed, err := testusers.EncryptContact(ctx, cipher, contacts[:1])
262+
if err != nil {
263+
t.Fatal(err)
264+
}
265+
return sealed
266+
}
267+
192268
func TestTextEqIsRefusedByTheBuildWithoutEQLTypes(t *testing.T) {
193269
ctx := context.Background()
194270
plain, err := encrypt.PlainGuest()

‎languages/golang/encrypt/guest_test.go‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,41 @@ func fixtureRecord() record.Sealed {
597597

598598
// A record opened under a plan that seals a field it does not carry is
599599
// refused on the host, with the field named, before the guest is asked.
600+
// A record whose target field has no EQL value is refused by the host,
601+
// before the guest sees it: the errNoEQL branch, which a change could
602+
// otherwise drop and send a malformed record on.
603+
func TestTargetWithoutEQLIsRefusedBeforeTheGuest(t *testing.T) {
604+
ctx := context.Background()
605+
c := rawInstance(t)
606+
plan := &record.Plan{
607+
Context: []string{"users"},
608+
Fields: []record.Field{{Name: "email", Kind: record.String, Target: "TextEq"}},
609+
}
610+
for name, rec := range map[string]record.Sealed{
611+
"no outputs": {"email": {}},
612+
"a ciphertext where the EQL value should be": {"email": {Ciphertext: fixtureLeaf}},
613+
"the field missing": {},
614+
} {
615+
_, err := c.DefaultKeyset().Open(ctx, plan, []record.Sealed{rec})
616+
if !errors.Is(err, errNoEQL) || errors.Is(err, ErrState) || !strings.Contains(err.Error(), `field "email"`) {
617+
t.Errorf("%s: Open = %v, want errNoEQL before the guest, naming the field", name, err)
618+
}
619+
}
620+
}
621+
622+
// Query refuses a field the plan does not have, and a field that names no
623+
// EQL type, before the guest is asked.
624+
func TestQueryRejectsMissingAndNonTargetFieldsBeforeTheGuest(t *testing.T) {
625+
ctx := context.Background()
626+
c := rawInstance(t)
627+
for _, field := range []string{"email", "missing"} {
628+
_, err := c.DefaultKeyset().Query(ctx, usersPlan(), field, "a@b.c")
629+
if !errors.Is(err, ErrEncoding) || errors.Is(err, ErrState) || !strings.Contains(err.Error(), field) {
630+
t.Errorf("Query(%q) = %v, want ErrEncoding before the guest, naming the field", field, err)
631+
}
632+
}
633+
}
634+
600635
func TestMismatchedPlanIsRefusedBeforeTheGuest(t *testing.T) {
601636
ctx := context.Background()
602637
c := rawInstance(t)

0 commit comments

Comments
 (0)