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.
206 lines
9.1 KiB
206 lines
9.1 KiB
package profile_test
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/sha256"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"errors"
|
|
"maps"
|
|
"math/big"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/drand/drand/v2/crypto"
|
|
|
|
datekeys "g.activething.com/go/DateKeys"
|
|
"g.activething.com/go/DateKeys/internal/cbortest"
|
|
"g.activething.com/go/DateKeys/profile"
|
|
)
|
|
|
|
// Spec §12.1, §69.1: a Provider Profile reports the code of its first failing
|
|
// layer: the type tag and the schema version, then the CDDL with the
|
|
// implementation limit on period (ERR_NON_CANONICAL_CBOR), then the rules
|
|
// of its fields (ERR_UNKNOWN_PROFILE), and last the chain-hash self-check,
|
|
// which relates several keys (ERR_PROFILE_MISMATCH).
|
|
func TestDecodePrecedence(t *testing.T) {
|
|
b, _ := profile.Quicknet().CanonicalCBOR()
|
|
m, err := cbortest.UnmarshalMap(b)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
encode := func(edit func(c map[uint64]any)) []byte {
|
|
c := maps.Clone(m)
|
|
edit(c)
|
|
out, err := cbortest.Marshal(c)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return out
|
|
}
|
|
chain := func(c map[uint64]any) { c[5] = make([]byte, 32) }
|
|
for _, tc := range []struct {
|
|
name string
|
|
edit func(c map[uint64]any)
|
|
want error
|
|
}{
|
|
{"type tag of another schema and version 2", func(c map[uint64]any) { c[0], c[1] = "datekeycap", uint64(2) }, datekeys.ErrNonCanonicalCBOR},
|
|
{"version 2, unknown key and invalid provider", func(c map[uint64]any) { c[1], c[11], c[3] = uint64(2), uint64(0), "Drand" }, datekeys.ErrUnsupportedVersion},
|
|
{"period above the implementation limit and invalid provider", func(c map[uint64]any) { c[7], c[3] = uint64(86401), "Drand" }, datekeys.ErrNonCanonicalCBOR},
|
|
{"network of another CBOR type", func(c map[uint64]any) { c[4] = uint64(7) }, datekeys.ErrNonCanonicalCBOR},
|
|
{"public key of another CBOR type and chain hash", func(c map[uint64]any) { c[6] = "key"; chain(c) }, datekeys.ErrNonCanonicalCBOR},
|
|
{"invalid provider and chain hash", func(c map[uint64]any) { c[3] = "Drand"; chain(c) }, datekeys.ErrUnknownProfile},
|
|
{"public key above 1024 bytes and chain hash", func(c map[uint64]any) { c[6] = make([]byte, 1025); chain(c) }, datekeys.ErrUnknownProfile},
|
|
{"genesis_time 0 and chain hash", func(c map[uint64]any) { c[8] = uint64(0); chain(c) }, datekeys.ErrUnknownProfile},
|
|
{"genesis_time 9999-12-31T23:59:59Z and chain hash", func(c map[uint64]any) { c[8] = uint64(profile.MaxUnixTime); chain(c) }, datekeys.ErrUnknownProfile},
|
|
{"scheme without tlock support and chain hash", func(c map[uint64]any) { c[9] = "pedersen-bls-chained"; chain(c) }, datekeys.ErrUnknownProfile},
|
|
{"public key not a point and chain hash", func(c map[uint64]any) { c[6] = bytes.Repeat([]byte{0xff}, 96); chain(c) }, datekeys.ErrUnknownProfile},
|
|
{"public key the identity and chain hash", func(c map[uint64]any) { k := make([]byte, 96); k[0] = 0xc0; c[6] = k; chain(c) }, datekeys.ErrUnknownProfile},
|
|
// Spec §12.1 rule 2: the key is a point of the prime-order subgroup.
|
|
// pedersen-bls-unchained has its keys on G1 (48 bytes); its generator
|
|
// passes rule 2 and only the chain hash fails, while a point of the
|
|
// curve outside the subgroup fails rule 2.
|
|
{"G1 generator and chain hash", func(c map[uint64]any) { c[9], c[6] = "pedersen-bls-unchained", g1Generator(t); chain(c) }, datekeys.ErrProfileMismatch},
|
|
{"G1 point outside the prime-order subgroup and chain hash", func(c map[uint64]any) { c[9], c[6] = "pedersen-bls-unchained", offSubgroupG1(t); chain(c) }, datekeys.ErrUnknownProfile},
|
|
{"chain hash alone", chain, datekeys.ErrProfileMismatch},
|
|
} {
|
|
if _, err := profile.Decode(encode(tc.edit)); !errors.Is(err, tc.want) {
|
|
t.Errorf("%s: got %v, want %v", tc.name, err, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Spec §12.1: chain_hash is the drand chain-info hash (drand specification,
|
|
// Root of trust):
|
|
//
|
|
// SHA-256(uint32_be(period) || int64_be(genesis_time) || public_key ||
|
|
// genesis_seed || network)
|
|
//
|
|
// with network left out when it is "default". Computed here without drand.
|
|
func TestChainHashFormula(t *testing.T) {
|
|
sum := chainHashFormula
|
|
q := profile.Quicknet()
|
|
if got := hex.EncodeToString(sum(q)); got != profile.QuicknetChainHash {
|
|
t.Fatalf("formula gives %s, Quicknet chain_hash is %s", got, profile.QuicknetChainHash)
|
|
}
|
|
if err := q.Validate(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// The network "default" is not hashed.
|
|
d := profile.Quicknet()
|
|
d.Network = "default"
|
|
copy(d.ChainHash[:], sum(d))
|
|
if err := d.Validate(); err != nil {
|
|
t.Fatalf("network default: %v", err)
|
|
}
|
|
d.Network = "defaults"
|
|
if err := d.Validate(); !errors.Is(err, datekeys.ErrProfileMismatch) {
|
|
t.Fatalf("network defaults hashed like default: %v", err)
|
|
}
|
|
}
|
|
|
|
// chainHashFormula is the chain_hash of p by the formula of spec §12.1,
|
|
// computed without drand, over the exact bytes of its public key.
|
|
func chainHashFormula(p *profile.Profile) []byte {
|
|
var n [12]byte
|
|
binary.BigEndian.PutUint32(n[:4], uint32(p.Period/time.Second))
|
|
binary.BigEndian.PutUint64(n[4:], uint64(p.GenesisTime))
|
|
h := sha256.New()
|
|
h.Write(n[:])
|
|
h.Write(p.PublicKey)
|
|
h.Write(p.GenesisSeed[:])
|
|
if p.Network != "default" {
|
|
h.Write([]byte(p.Network))
|
|
}
|
|
return h.Sum(nil)
|
|
}
|
|
|
|
// g1Generator is the compressed generator of G1, in the encoding of drand.
|
|
func g1Generator(t *testing.T) []byte {
|
|
t.Helper()
|
|
s, err := crypto.SchemeFromName(crypto.UnchainedSchemeID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
b, err := s.KeyGroup.Point().Base().MarshalBinary()
|
|
if err != nil || len(b) != 48 {
|
|
t.Fatalf("G1 generator: %d bytes, %v", len(b), err)
|
|
}
|
|
return b
|
|
}
|
|
|
|
// offSubgroupG1 returns the compressed encoding (48 bytes, flag 0x80) of the
|
|
// point of the BLS12-381 curve y^2 = x^3 + 4 over Fp with the smallest x >= 1.
|
|
// It is on the curve but not in the prime-order subgroup, whose cofactor is
|
|
// about 2^126; the decoder of drand says so.
|
|
func offSubgroupG1(t *testing.T) []byte {
|
|
t.Helper()
|
|
p, _ := new(big.Int).SetString("1a0111ea397fe69a4b1ba7b6434bacd764774b84f38512bf6730d2a0f6b0f6241eabfffeb153ffffb9feffffffffaaab", 16)
|
|
half := new(big.Int).Rsh(new(big.Int).Sub(p, big.NewInt(1)), 1)
|
|
for x := int64(1); x < 100; x++ {
|
|
rhs := new(big.Int).Exp(big.NewInt(x), big.NewInt(3), p)
|
|
rhs.Add(rhs, big.NewInt(4)).Mod(rhs, p)
|
|
if new(big.Int).Exp(rhs, half, p).Cmp(big.NewInt(1)) != 0 {
|
|
continue // x^3 + 4 is not a square: no point with this x
|
|
}
|
|
b := make([]byte, 48)
|
|
big.NewInt(x).FillBytes(b)
|
|
b[0] |= 0x80
|
|
s, err := crypto.SchemeFromName(crypto.UnchainedSchemeID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.KeyGroup.Point().UnmarshalBinary(b); err == nil || !strings.Contains(err.Error(), "subgroup") {
|
|
t.Fatalf("x = %d: want a curve point outside the subgroup, got %v", x, err)
|
|
}
|
|
return b
|
|
}
|
|
t.Fatal("no curve point with a small x")
|
|
return nil
|
|
}
|
|
|
|
// Spec §12.1, §69.1: a profile gets the same code when it is pinned
|
|
// (NewRegistry), validated as a value (Validate) or decoded from its
|
|
// encoding (Decode): the schema first, with the period limit of the
|
|
// reference, then the field rules, then the chain hash, and last the pinned
|
|
// profile_hash.
|
|
func TestPinPathMatchesDecode(t *testing.T) {
|
|
chain := func(p *profile.Profile) { p.ChainHash = [32]byte{} }
|
|
for _, tc := range []struct {
|
|
name string
|
|
edit func(p *profile.Profile)
|
|
want error
|
|
}{
|
|
{"period of one day and one second", func(p *profile.Profile) { p.Period = 86401 * time.Second }, datekeys.ErrNonCanonicalCBOR},
|
|
{"period of one day and one second and provider Drand", func(p *profile.Profile) { p.Period, p.Provider = 86401*time.Second, "Drand" }, datekeys.ErrNonCanonicalCBOR},
|
|
{"sub-second period and provider Drand", func(p *profile.Profile) { p.Period, p.Provider = 1500*time.Millisecond, "Drand" }, datekeys.ErrNonCanonicalCBOR},
|
|
{"genesis_time 2^53 and provider Drand", func(p *profile.Profile) { p.GenesisTime, p.Provider = 1<<53, "Drand" }, datekeys.ErrNonCanonicalCBOR},
|
|
{"negative genesis_time", func(p *profile.Profile) { p.GenesisTime = -1 }, datekeys.ErrNonCanonicalCBOR},
|
|
{"network not valid UTF-8 and chain hash", func(p *profile.Profile) { p.Network = "quick\xffnet"; chain(p) }, datekeys.ErrNonCanonicalCBOR},
|
|
{"provider Drand and chain hash", func(p *profile.Profile) { p.Provider = "Drand"; chain(p) }, datekeys.ErrUnknownProfile},
|
|
{"genesis_time 9999-12-31T23:59:59Z and chain hash", func(p *profile.Profile) { p.GenesisTime = profile.MaxUnixTime; chain(p) }, datekeys.ErrUnknownProfile},
|
|
{"chain hash alone", chain, datekeys.ErrProfileMismatch},
|
|
} {
|
|
p := profile.Quicknet()
|
|
tc.edit(p)
|
|
if err := p.Validate(); !errors.Is(err, tc.want) {
|
|
t.Errorf("%s: Validate: got %v, want %v", tc.name, err, tc.want)
|
|
}
|
|
if _, err := profile.NewRegistry(profile.Pin{Profile: p}); !errors.Is(err, tc.want) {
|
|
t.Errorf("%s: NewRegistry: got %v, want %v", tc.name, err, tc.want)
|
|
}
|
|
if b, err := p.CanonicalCBOR(); err != nil {
|
|
if !errors.Is(err, tc.want) {
|
|
t.Errorf("%s: CanonicalCBOR: got %v, want %v", tc.name, err, tc.want)
|
|
}
|
|
} else if _, err := profile.Decode(b); !errors.Is(err, tc.want) {
|
|
t.Errorf("%s: Decode: got %v, want %v", tc.name, err, tc.want)
|
|
}
|
|
}
|
|
// Rule 4 comes last: a valid profile with another pinned hash.
|
|
if _, err := profile.NewRegistry(profile.Pin{Profile: profile.Quicknet()}); !errors.Is(err, datekeys.ErrProfileMismatch) {
|
|
t.Errorf("another pinned profile_hash: %v", err)
|
|
}
|
|
}
|