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/diagnostics_test.go

135 lines
4.2 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"
"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)
}
}

Powered by TurnKey Linux.