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/profile/precedence_test.go

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)
}
}

Powered by TurnKey Linux.