package capsule_test import ( "bytes" "context" "encoding/hex" "errors" "io" "strings" "testing" "github.com/drand/drand/v2/common" "github.com/drand/tlock" datekeys "g.activething.com/go/DateKeys" "g.activething.com/go/DateKeys/agewrap" "g.activething.com/go/DateKeys/capsule" "g.activething.com/go/DateKeys/internal/testkit" "g.activething.com/go/DateKeys/profile" ) // Spec ยง63 step 11: when the IBE check of the tlock stanza fails, kyber // reports in its error the candidate plaintext, W xor H4(sigma), and r. A // third party who edits W learns FK_TIME from the candidate and the edit, so // neither the error of Open nor the details of its checks may carry them: // the text of tlock and kyber is never copied. func TestTlockFailureDiagnosticsCarryNoSecrets(t *testing.T) { e, err := testkit.NewMutationEnv(fixtureDir) if err != nil { t.Fatal(err) } f := e.TimeOnly fk, err := f.TimeFileKey() if err != nil { t.Fatal(err) } // W ends the stanza body: flipping its last bit flips the last bit of // the candidate plaintext, sigma being unchanged. in, err := f.WithTlockBody(func(b []byte) ([]byte, error) { c := bytes.Clone(b) c[len(c)-1] ^= 1 return c, nil }) if err != nil { t.Fatal(err) } candidate := bytes.Clone(fk) candidate[len(candidate)-1] ^= 1 secrets := map[string]string{ "FK_TIME": string(fk), "FK_TIME in hex": hex.EncodeToString(fk), "candidate plaintext": string(candidate), "candidate plaintext in hex": hex.EncodeToString(candidate), } // What tlock reports for this body: kyber's error, with the candidate // and r as kyber prints it. parts, err := testkit.Split(in.DKC) if err != nil { t.Fatal(err) } stanzas, err := agewrap.Stanzas(bytes.NewReader(parts.Sealed)) if err != nil { t.Fatal(err) } p := profile.Quicknet() scheme, err := p.DrandScheme() if err != nil { t.Fatal(err) } key := scheme.KeyGroup.Point() if err := key.UnmarshalBinary(p.PublicKey); err != nil { t.Fatal(err) } ct, err := tlock.BytesToCiphertext(*scheme, stanzas[0].Body) if err != nil { t.Fatal(err) } _, tlockErr := tlock.TimeUnlock(*scheme, key, common.Beacon{Round: f.Published.Round, Signature: f.Published.Signature}, ct) if tlockErr == nil { t.Fatal("tlock accepts the edited W") } if msg := tlockErr.Error(); strings.Contains(msg, string(candidate)) { if i := strings.LastIndex(msg, ", r "); i >= 0 { r := msg[i+len(", r "):] secrets["r as kyber prints it"] = r if b, err := hex.DecodeString(r); err == nil { secrets["r"] = string(b) } } } else { t.Logf("tlock no longer reports the candidate plaintext: %q", msg) } opened, err := capsule.Open(context.Background(), io.Discard, bytes.NewReader(in.DKC), capsule.OpenOptions{ Registry: testkit.Registry(), Source: testkit.NewSource(f.Published), Now: testkit.Fixed(f.Unlock), }) checks := opened.Inspection.Checks if last := checks[len(checks)-1]; !errors.Is(err, datekeys.ErrIntegrity) || last.Step != 11 || last.Error != "ERR_INTEGRITY" { t.Fatalf("got %v at step %d, want ERR_INTEGRITY at step 11", err, last.Step) } texts := []string{err.Error()} for _, c := range checks { texts = append(texts, c.Detail) } for name, s := range secrets { for _, text := range texts { if strings.Contains(text, s) { t.Errorf("the %s is in %q", name, text) } } } } // The failures of age get fixed reasons, but a failure of the caller's // writer at step 17 is not one of age: it keeps its own text, and the code // it always had. func TestPlaintextWriterFailureKeepsItsText(t *testing.T) { e, err := testkit.NewMutationEnv(fixtureDir) if err != nil { t.Fatal(err) } f := e.TimeOnly opened, err := capsule.Open(context.Background(), failingWriter{}, bytes.NewReader(f.DKC), capsule.OpenOptions{ Registry: testkit.Registry(), Source: testkit.NewSource(f.Published), Now: testkit.Fixed(f.Unlock), }) checks := opened.Inspection.Checks if last := checks[len(checks)-1]; !errors.Is(err, datekeys.ErrIntegrity) || last.Step != 17 { t.Fatalf("got %v at step %d, want ERR_INTEGRITY at step 17", err, last.Step) } if !strings.Contains(err.Error(), "disk full") || strings.Contains(err.Error(), "STREAM") { t.Fatalf("the error of the writer is not reported as such: %v", err) } }