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.
234 lines
8.5 KiB
234 lines
8.5 KiB
package profile_test
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/hex"
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
|
|
datekeys "g.activething.com/go/DateKeys"
|
|
"g.activething.com/go/DateKeys/codec"
|
|
"g.activething.com/go/DateKeys/internal/testkit"
|
|
"g.activething.com/go/DateKeys/profile"
|
|
)
|
|
|
|
func TestQuicknetMatchesGoldenVector(t *testing.T) {
|
|
var golden testkit.ProfileVector
|
|
if err := testkit.ReadJSON("../testdata/vectors/profile_quicknet.json", &golden); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := testkit.QuicknetProfileVector()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got != golden {
|
|
t.Fatalf("Quicknet profile vector changed:\n got %+v\nwant %+v", got, golden)
|
|
}
|
|
if golden.ProfileHash != profile.QuicknetProfileHash {
|
|
t.Fatalf("pinned hash %s differs from golden %s", profile.QuicknetProfileHash, golden.ProfileHash)
|
|
}
|
|
// Spec §12 values, restated independently of the constants.
|
|
p := profile.Quicknet()
|
|
if p.ID != "datekeys:quicknet:v1" || p.Provider != "drand" || p.Network != "quicknet" ||
|
|
p.Period != 3*time.Second || p.GenesisTime != 1692803367 || p.Scheme != "bls-unchained-g1-rfc9380" ||
|
|
p.ChainHashHex() != "52db9ba70e0cc0f6eaf7803dd07447a1f5477735fd3f661792ba94600c84e971" ||
|
|
hex.EncodeToString(p.GenesisSeed[:]) != "f477d5c89f21a17c863a7f937c6a6d15859414d2be09cd448d4279af331c5d3e" {
|
|
t.Fatal("Quicknet parameters differ from spec §12")
|
|
}
|
|
}
|
|
|
|
func TestQuicknetCBORLayout(t *testing.T) {
|
|
b, err := profile.Quicknet().CanonicalCBOR()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Map of 11 entries whose keys 0..10 appear in order (spec §11).
|
|
if b[0] != 0xab {
|
|
t.Fatalf("map header %#x", b[0])
|
|
}
|
|
var m map[uint64]any
|
|
if err := codec.Unmarshal(b, &m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := map[uint64]any{0: "datekeys-provider-profile", 1: uint64(1), 2: "datekeys:quicknet:v1", 3: "drand", 4: "quicknet", 7: uint64(3), 8: uint64(1692803367), 9: "bls-unchained-g1-rfc9380"}
|
|
for k, v := range want {
|
|
if m[k] != v {
|
|
t.Errorf("key %d = %v, want %v", k, m[k], v)
|
|
}
|
|
}
|
|
for _, k := range []uint64{5, 6, 10} {
|
|
if _, ok := m[k].([]byte); !ok {
|
|
t.Errorf("key %d is not a byte string", k)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDecodeRoundTrip(t *testing.T) {
|
|
b, _ := profile.Quicknet().CanonicalCBOR()
|
|
p, err := profile.Decode(b)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b2, _ := p.CanonicalCBOR()
|
|
if !bytes.Equal(b, b2) {
|
|
t.Fatal("Decode/CanonicalCBOR is not the identity")
|
|
}
|
|
// The same values with a different key order or integer width are rejected.
|
|
period, genesis := []byte{0x07, 0x03}, []byte{0x08, 0x1a, 0x64, 0xe6, 0x21, 0x27}
|
|
pair := append(append([]byte(nil), period...), genesis...)
|
|
if !bytes.Contains(b, pair) {
|
|
t.Fatal("unexpected layout")
|
|
}
|
|
swapped := bytes.Replace(b, pair, append(append([]byte(nil), genesis...), period...), 1)
|
|
widened := bytes.Replace(b, period, []byte{0x07, 0x18, 0x03}, 1)
|
|
for name, in := range map[string][]byte{"keys out of order": swapped, "integer not in shortest form": widened} {
|
|
if _, err := profile.Decode(in); !errors.Is(err, datekeys.ErrNonCanonicalCBOR) {
|
|
t.Errorf("%s accepted: %v", name, err)
|
|
}
|
|
}
|
|
future, _ := codec.Marshal(map[uint64]any{0: profile.TypeTag, 1: uint64(2)})
|
|
if _, err := profile.Decode(future); !errors.Is(err, datekeys.ErrUnsupportedVersion) {
|
|
t.Fatalf("future schema: %v", err)
|
|
}
|
|
}
|
|
|
|
// Spec §11, §58: period in 1..2^53-1 and genesis_time in 0..2^53-1, both
|
|
// unsigned. Out of the schema is ErrNonCanonicalCBOR, whatever Validate would
|
|
// say of the value.
|
|
func TestIntegerRanges(t *testing.T) {
|
|
b, _ := profile.Quicknet().CanonicalCBOR()
|
|
var m map[uint64]any
|
|
if err := codec.Unmarshal(b, &m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for name, v := range map[string]struct {
|
|
key uint64
|
|
val any
|
|
}{
|
|
"negative genesis_time": {8, int64(-1)},
|
|
"genesis_time 2^53": {8, uint64(codec.MaxSafeUint + 1)},
|
|
"period 2^53": {7, uint64(codec.MaxSafeUint + 1)},
|
|
"period 0": {7, uint64(0)},
|
|
"period above one day": {7, uint64(86401)},
|
|
} {
|
|
c := map[uint64]any{}
|
|
for k, x := range m {
|
|
c[k] = x
|
|
}
|
|
c[v.key] = v.val
|
|
in, err := codec.Marshal(c)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := profile.Decode(in); !errors.Is(err, datekeys.ErrNonCanonicalCBOR) {
|
|
t.Errorf("%s: %v", name, err)
|
|
}
|
|
}
|
|
// The encoder refuses the same values with the same code.
|
|
for _, g := range []int64{-1, codec.MaxSafeUint + 1} {
|
|
p := profile.Quicknet()
|
|
p.GenesisTime = g
|
|
if _, err := p.CanonicalCBOR(); !errors.Is(err, datekeys.ErrNonCanonicalCBOR) {
|
|
t.Errorf("genesis time %d encoded: %v", g, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidateRejectsTamperedProfiles(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
mutate func(p *profile.Profile)
|
|
want error
|
|
}{
|
|
{"chain hash changed", func(p *profile.Profile) { p.ChainHash[0] ^= 1 }, datekeys.ErrProfileMismatch},
|
|
{"genesis seed changed", func(p *profile.Profile) { p.GenesisSeed[0] ^= 1 }, datekeys.ErrProfileMismatch},
|
|
{"genesis time changed", func(p *profile.Profile) { p.GenesisTime++ }, datekeys.ErrProfileMismatch},
|
|
{"period changed", func(p *profile.Profile) { p.Period = 30 * time.Second }, datekeys.ErrProfileMismatch},
|
|
{"network id changed", func(p *profile.Profile) { p.Network = "default" }, datekeys.ErrProfileMismatch},
|
|
{"public key not a point", func(p *profile.Profile) { p.PublicKey = bytes.Repeat([]byte{0xff}, 96) }, datekeys.ErrUnknownProfile},
|
|
{"public key truncated", func(p *profile.Profile) { p.PublicKey = p.PublicKey[:95] }, datekeys.ErrUnknownProfile},
|
|
{"unknown scheme", func(p *profile.Profile) { p.Scheme = "bls-unchained-g9" }, datekeys.ErrUnknownProfile},
|
|
{"scheme without tlock support", func(p *profile.Profile) { p.Scheme = "pedersen-bls-chained" }, datekeys.ErrUnknownProfile},
|
|
{"unknown provider", func(p *profile.Profile) { p.Provider = "roughtime" }, datekeys.ErrUnknownProfile},
|
|
{"sub-second period", func(p *profile.Profile) { p.Period = 1500 * time.Millisecond }, datekeys.ErrUnknownProfile},
|
|
{"uppercase profile id", func(p *profile.Profile) { p.ID = "DateKeys:quicknet:v1" }, datekeys.ErrUnknownProfile},
|
|
{"profile id with quote", func(p *profile.Profile) { p.ID = `datekeys:"quicknet` }, datekeys.ErrUnknownProfile},
|
|
// Spec §57: the Provider Profile names and public key outside their
|
|
// CDDL rules are ERR_UNKNOWN_PROFILE.
|
|
{"uppercase provider", func(p *profile.Profile) { p.Provider = "Drand" }, datekeys.ErrUnknownProfile},
|
|
{"empty public key", func(p *profile.Profile) { p.PublicKey = []byte{} }, datekeys.ErrUnknownProfile},
|
|
{"public key above 1024 bytes", func(p *profile.Profile) { p.PublicKey = make([]byte, 1025) }, datekeys.ErrUnknownProfile},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
p := profile.Quicknet()
|
|
tc.mutate(p)
|
|
if err := p.Validate(); !errors.Is(err, tc.want) {
|
|
t.Fatalf("got %v, want %v", err, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRegistry(t *testing.T) {
|
|
reg, err := profile.Default()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
p, ok := reg.Lookup(profile.QuicknetID)
|
|
if !ok {
|
|
t.Fatal("Quicknet not pinned")
|
|
}
|
|
// Lookup hands out copies: mutating one does not alter the registry.
|
|
p.PublicKey[0] ^= 0xff
|
|
p.ChainHash[0] ^= 0xff
|
|
again, _ := reg.Lookup(profile.QuicknetID)
|
|
if err := again.Validate(); err != nil {
|
|
t.Fatalf("registry state was mutated through a lookup: %v", err)
|
|
}
|
|
if _, ok := reg.Lookup("datekeys:evmnet:v1"); ok {
|
|
t.Fatal("unknown profile found")
|
|
}
|
|
|
|
var wrong [32]byte
|
|
if _, err := profile.NewRegistry(profile.Pin{Profile: profile.Quicknet(), Hash: wrong}); !errors.Is(err, datekeys.ErrProfileMismatch) {
|
|
t.Fatalf("wrong pinned hash accepted: %v", err)
|
|
}
|
|
h, _ := profile.Quicknet().Hash()
|
|
if _, err := profile.NewRegistry(profile.Pin{Profile: profile.Quicknet(), Hash: h}, profile.Pin{Profile: profile.Quicknet(), Hash: h}); err == nil {
|
|
t.Fatal("duplicate profile accepted")
|
|
}
|
|
bad := profile.Quicknet()
|
|
bad.ChainHash[31] ^= 1
|
|
bh, _ := bad.Hash()
|
|
if _, err := profile.NewRegistry(profile.Pin{Profile: bad, Hash: bh}); !errors.Is(err, datekeys.ErrProfileMismatch) {
|
|
t.Fatalf("self-inconsistent profile pinned: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestMaxRound(t *testing.T) {
|
|
p := profile.Quicknet()
|
|
if got := p.MaxRound(); got != 83903165811 {
|
|
t.Fatalf("MaxRound = %d", got)
|
|
}
|
|
}
|
|
|
|
func FuzzDecode(f *testing.F) {
|
|
b, _ := profile.Quicknet().CanonicalCBOR()
|
|
f.Add(b)
|
|
f.Add(b[:100])
|
|
f.Fuzz(func(t *testing.T, in []byte) {
|
|
p, err := profile.Decode(in)
|
|
if err != nil {
|
|
if datekeys.Code(err) == "" {
|
|
t.Fatalf("error without a normative code: %v", err)
|
|
}
|
|
return
|
|
}
|
|
out, err := p.CanonicalCBOR()
|
|
if err != nil || !bytes.Equal(out, in) {
|
|
t.Fatal("accepted a profile that does not re-encode to its input")
|
|
}
|
|
})
|
|
}
|