package main import ( "bytes" "encoding/hex" "errors" "os" "path/filepath" "strings" "testing" "time" datekeys "g.activething.com/go/DateKeys" "g.activething.com/go/DateKeys/internal/testkit" ) const releases = "../../testdata/releases" // unlock1000 is the round time of round 1000, that of time_only.dkc. var unlock1000 = time.Date(2023, 8, 23, 15, 59, 24, 0, time.UTC) // releaseFile is the release object of round in testdata/releases. func releaseFile(round uint64) string { return filepath.Join(releases, testkit.ReleaseFileName(round)) } // Spec v0.15, ยง63 step 9.c: a release in hand opens the capsule without any // network request, even with a clock behind the round time, which decrypt // only warns about. func TestDecryptWithReleaseInHand(t *testing.T) { dir := t.TempDir() want, err := os.ReadFile(filepath.Join(fixtures, "time_only.plaintext")) if err != nil { t.Fatal(err) } jsonFile := filepath.Join(dir, "1000.json") if err := os.WriteFile(jsonFile, []byte(`{"round":1000,"signature":"`+hex.EncodeToString(testkit.Release(1000).Signature)+`"}`), 0o644); err != nil { t.Fatal(err) } for _, c := range []struct { name, release string now time.Time }{ {"a release object", releaseFile(1000), later}, {"a release object and a clock behind", releaseFile(1000), unlock1000.Add(-time.Hour)}, {"drand's JSON", jsonFile, later}, {"a local archive", filepath.Join(releases, testkit.ArchiveFile), unlock1000.Add(-time.Nanosecond)}, } { t.Run(c.name, func(t *testing.T) { out := filepath.Join(dir, c.name) _, stderr, err := cli(t, c.now, "decrypt", "-in", filepath.Join(fixtures, "time_only.dkc"), "-out", out, "-release", c.release) if err != nil { t.Fatal(err) } if got, _ := os.ReadFile(out); !bytes.Equal(got, want) { t.Fatal("wrong content") } if behind := strings.Contains(stderr, "may be behind"); behind != c.now.Before(unlock1000) { t.Fatalf("warning of a clock behind: %v, in %q", behind, stderr) } }) } } // A release in hand that step 10 rejects gives the code of step 10, and one // the archive lacks is ErrReleaseUnavailable; nothing is written. func TestDecryptWithBadReleaseInHand(t *testing.T) { dir := t.TempDir() write := func(name string, b []byte) string { p := filepath.Join(dir, name) if err := os.WriteFile(p, b, 0o644); err != nil { t.Fatal(err) } return p } r1001, _ := os.ReadFile(releaseFile(1001)) r1000, _ := os.ReadFile(releaseFile(1000)) v2 := bytes.Clone(r1000) v2[bytes.Index(v2, []byte{0x01, 0x01})+1] = 2 for _, c := range []struct { name string release string capsule string want *datekeys.Error }{ {"another round", write("1001.cbor", r1001), "time_only", datekeys.ErrRoundMismatch}, {"version 2", write("v2.cbor", v2), "time_only", datekeys.ErrUnsupportedVersion}, {"a byte after it", write("long.cbor", append(bytes.Clone(r1000), 0)), "time_only", datekeys.ErrNonCanonicalCBOR}, {"a large file", write("large.cbor", append(bytes.Clone(r1000), make([]byte, 20000)...)), "time_only", datekeys.ErrNonCanonicalCBOR}, {"bad JSON", write("bad.json", []byte(`{"round":1000}`)), "time_only", datekeys.ErrReleaseInvalid}, {"a round the archive lacks", filepath.Join(releases, testkit.ArchiveFile), "format2_time_and_key_sixteen", datekeys.ErrReleaseUnavailable}, } { t.Run(c.name, func(t *testing.T) { out := filepath.Join(dir, c.name) args := []string{"decrypt", "-in", filepath.Join(fixtures, c.capsule+".dkc"), "-out", out, "-release", c.release} if c.capsule == "format2_time_and_key_sixteen" { args = append(args, "-identity", filepath.Join(dir, "none")) // The capsule needs a credential before the release: give it // one that is never reached. write("none", []byte(testkit.Stranger().String()+"\n")) } _, _, err := cli(t, later, args...) if !errors.Is(err, c.want) { t.Fatalf("got %v, want %v", err, c.want) } if _, err := os.Stat(out); !os.IsNotExist(err) { t.Fatal("output written") } }) } if _, _, err := cli(t, later, "decrypt", "-in", filepath.Join(fixtures, "time_only.dkc"), "-out", filepath.Join(dir, "x"), "-release", "a.cbor", "-relay", "http://127.0.0.1:1"); err == nil { t.Fatal("-release and -relay together") } } // A network source is still never asked before the round time. func TestDecryptFromRelayBeforeTheRoundTime(t *testing.T) { _, _, err := cli(t, unlock1000.Add(-time.Second), "decrypt", "-in", filepath.Join(fixtures, "time_only.dkc"), "-out", filepath.Join(t.TempDir(), "x"), "-relay", "http://127.0.0.1:1") if !errors.Is(err, datekeys.ErrReleaseUnavailable) { t.Fatalf("got %v", err) } }