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.
135 lines
4.2 KiB
135 lines
4.2 KiB
|
2 weeks ago
|
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)
|
||
|
|
}
|
||
|
|
}
|