package capsule_test import ( "bytes" "encoding/hex" "io" "testing" "filippo.io/age" "g.activething.com/go/DateKeys/agewrap" "g.activething.com/go/DateKeys/internal/testkit" "g.activething.com/go/DateKeys/profile" ) // reducingIdentity models a reader whose decoder reduces coordinates modulo // p: it reduces c0 of U before the strict tlock identity sees the stanza. type reducingIdentity struct{ id *agewrap.TimeIdentity } func (r reducingIdentity) Unwrap(stanzas []*age.Stanza) ([]byte, error) { s := *stanzas[0] s.Body = testkit.ReduceCoordinate(s.Body, testkit.CoordinateLen) return r.id.Unwrap([]*age.Stanza{&s}) } // Spec §12.2, §64: the point mutations of the exported corpus differ from a // capsule that opens only in the encoding of one point. A reader that // reduces coordinates modulo p opens the capsules with c0 + p in U and with // x + p in the signature, which the reference rejects // (TestExportedMutationCorpus), and every edited tlock body keeps a valid // header MAC, so that only the rules of the body reject it. func TestPointMutationsChangeOnlyTheEncoding(t *testing.T) { var f testkit.MutationFile if err := testkit.ReadJSON(mutationsFile, &f); err != nil { t.Fatal(err) } cases := map[string]*testkit.MutationCase{} for i := range f.Cases { cases[f.Cases[i].Name] = &f.Cases[i] } input := func(name string) *testkit.MutationInput { t.Helper() c := cases[name] if c == nil { t.Fatalf("no case %q", name) } in, err := c.Input(fixtureDir) if err != nil { t.Fatal(err) } return in } // The frozen capsule of round XPlusPRound opens with the signature // reduced modulo p, the published one. in := input("release signature re-encoded with x + p") reduced := testkit.ReduceCoordinate(in.Release.Signature, 0) if !bytes.Equal(reduced, testkit.Release(testkit.XPlusPRound).Signature) { t.Fatalf("x + p reduces to %x", reduced) } in.Release.Signature = reduced if v, err := in.Open(); err != nil || v.Err != nil { t.Fatalf("with the published signature: %v %v", err, v.Err) } // The tlock bodies: the header MAC of OUTER_TIME_AGE verifies with // FK_TIME, and OUTER_TIME_AGE decrypts to the CONTROL_CBOR of the fixture. e, err := testkit.NewMutationEnv(fixtureDir) if err != nil { t.Fatal(err) } fk, err := e.TimeOnly.TimeFileKey() if err != nil { t.Fatal(err) } control, err := hex.DecodeString(e.TimeOnly.ControlCBOR) if err != nil { t.Fatal(err) } sealed := func(in *testkit.MutationInput, id age.Identity) []byte { t.Helper() parts, err := testkit.Split(in.DKC) if err != nil { t.Fatal(err) } r, err := age.Decrypt(bytes.NewReader(parts.Sealed), id) if err != nil { t.Fatal(err) } out, err := io.ReadAll(r) if err != nil { t.Fatal(err) } return out } for _, name := range []string{ "tlock stanza U re-encoded with c0 + p", "tlock stanza U is the point at infinity", "tlock stanza U with the infinity flag and a payload", "tlock stanza body of 127 bytes", "tlock stanza body of 129 bytes", "negated release signature and U re-encoded with c0 + p", } { if got := sealed(input(name), age.NewInjectedFileKeyIdentity(fk)); !bytes.Equal(got, control) { t.Fatalf("%s: OUTER_TIME_AGE does not decrypt to the fixture's CONTROL_CBOR", name) } } // With c0 reduced modulo p, U is the U of the fixture again. in = input("tlock stanza U re-encoded with c0 + p") id, err := agewrap.NewTimeIdentity(profile.Quicknet(), in.Release.Round, *in.Release) if err != nil { t.Fatal(err) } if got := sealed(in, reducingIdentity{id}); !bytes.Equal(got, control) { t.Fatal("a reader that reduces c0 does not open OUTER_TIME_AGE") } }