You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
DateKeys/capsule/point_test.go

118 lines
3.6 KiB

Spec v0.8.2 amendment: canonical point encoding; no library error text Amendment of the unreleased v0.8.2, recorded in §76 with its case: the second implementation's phase-2 research found that tlock-js over @noble/curves 1.9.7 accepts U re-encoded as c0 + p and a signature x + p and returns the same file key, while the reference rejects both (noble 1.9.7 differed from kilic on 5,615 of 41,686 encodings), and the spec did not say which encodings are valid. - §12.2 defines the canonical encoding of a BLS12-381 point (drand's compressed ZCash form) and requires decoders to reject every other byte string; §12.1 applies it to public_key. - §63 step 10 applies it to the release signature (ERR_RELEASE_INVALID) and step 11 defines the tlock stanza body U || V || W (96 + 16 + 16 bytes for Quicknet) with a canonical, non-infinity U (ERR_INTEGRITY). - §64 gains ten mutations, exported to mutations.json (65 cases). The signature x + p case uses published Quicknet round 1004, the first after 1000 whose x allows x + p < 2^381. The reference already gave every stated code and step. Errors no longer copy text from tlock, kyber, age, drand or kyber-bls12381. kyber's IBE error carried the candidate plaintext and r, and with one bit of W flipped the message disclosed the real tlock file key with that bit flipped. Every such place now uses a fixed reason with its normative sentinel; TestTlockFailureDiagnosticsCarryNoSecrets fails with the old wrapping. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 weeks ago
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")
}
}

Powered by TurnKey Linux.