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

349 lines
11 KiB

// Package profile implements Provider Profiles (spec §10-§13): their
// Deterministic CBOR encoding, profile_hash, validation and the locally
// pinned registry that forms the client's root of trust.
package profile
import (
"bytes"
"crypto/sha256"
"encoding/hex"
"fmt"
"math"
"time"
"github.com/drand/drand/v2/common/chain"
"github.com/drand/drand/v2/crypto"
datekeys "g.activething.com/go/DateKeys"
"g.activething.com/go/DateKeys/codec"
)
// Schema constants of the Provider Profile CBOR map (spec §11).
const (
TypeTag = "datekeys-provider-profile"
SchemaVersion = 1
)
// ProviderDrand is the only provider implemented by this module (spec §12).
const ProviderDrand = "drand"
// MaxUnixTime is 9999-12-31T23:59:59Z. Round times beyond it are rejected so
// that every effective time stays representable in RFC 3339 and every round
// computation stays within int64.
const MaxUnixTime int64 = 253402300799
// Field limits enforced by Validate. They are implementation limits; spec §74
// leaves the definitive field limits open.
const (
maxIDLen = 128
maxNameLen = 64
maxPublicKeyLen = 1024
maxPeriod = 24 * time.Hour
)
// Profile is an immutable Provider Profile (spec §10). Treat values as
// read-only; registries hand out copies.
type Profile struct {
ID string // key 2, profile_id, for example "datekeys:quicknet:v1"
Provider string // key 3, for example "drand"
Network string // key 4, provider network identifier, for example "quicknet"
ChainHash [32]byte // key 5
PublicKey []byte // key 6, provider group public key
Period time.Duration // key 7, encoded as whole seconds
GenesisTime int64 // key 8, Unix seconds
Scheme string // key 9, for example "bls-unchained-g1-rfc9380"
GenesisSeed [32]byte // key 10
}
// wire is the CBOR map of spec §11, keys 2 to 10; keys 0 and 1 are the
// constants TypeTag and SchemaVersion. Every key is required.
type wire struct {
ID string // key 2
Provider string // key 3
Network string // key 4
ChainHash []byte // key 5
PublicKey []byte // key 6
Period uint64 // key 7, 1..2^53-1
GenesisTime uint64 // key 8, 0..2^53-1
Scheme string // key 9
GenesisSeed []byte // key 10
}
// wireKeys is the number of keys of the map, all required.
const wireKeys = 11
// unbounded bounds a field only by the input: its rule carries its own error
// code (spec §57) and Validate checks it after decoding.
const unbounded = math.MaxInt
func (w *wire) encode(e *codec.Encoder) {
e.Map(wireKeys)
e.Uint(0)
e.Text(TypeTag)
e.Uint(1)
e.Uint(SchemaVersion)
e.Uint(2)
e.Text(w.ID)
e.Uint(3)
e.Text(w.Provider)
e.Uint(4)
e.Text(w.Network)
e.Uint(5)
e.Bstr(w.ChainHash)
e.Uint(6)
e.Bstr(w.PublicKey)
e.Uint(7)
e.Uint(w.Period)
e.Uint(8)
e.Uint(w.GenesisTime)
e.Uint(9)
e.Text(w.Scheme)
e.Uint(10)
e.Bstr(w.GenesisSeed)
}
// decode reads the map with every CDDL rule whose violation is
// ErrNonCanonicalCBOR; the names and the public key are left to Validate.
func (w *wire) decode(d *codec.Decoder) error {
pairs, err := d.Map(wireKeys)
if err != nil {
return err
}
if pairs != wireKeys {
return fmt.Errorf("%d keys, want all %d: %w", pairs, wireKeys, datekeys.ErrNonCanonicalCBOR)
}
for want := range uint64(wireKeys) {
k, err := d.Key()
if err != nil {
return err
}
if k != want {
return fmt.Errorf("key %d where key %d was expected: %w", k, want, datekeys.ErrNonCanonicalCBOR)
}
switch k {
case 0:
_, err = d.Text(len(TypeTag))
case 1:
_, err = d.Uint(SchemaVersion)
case 2:
w.ID, err = d.Text(unbounded)
case 3:
w.Provider, err = d.Text(unbounded)
case 4:
w.Network, err = d.Text(unbounded)
case 5:
w.ChainHash, err = d.Bstr(32, 32)
case 6:
w.PublicKey, err = d.Bstr(0, unbounded)
case 7:
// Spec §11: period in 1..2^53-1.
if w.Period, err = d.Uint(codec.MaxSafeUint); err == nil && w.Period == 0 {
err = fmt.Errorf("period 0: %w", datekeys.ErrNonCanonicalCBOR)
}
case 8:
// Spec §11: genesis_time in 0..2^53-1, unsigned.
w.GenesisTime, err = d.Uint(codec.MaxSafeUint)
case 9:
w.Scheme, err = d.Text(unbounded)
case 10:
w.GenesisSeed, err = d.Bstr(32, 32)
}
if err != nil {
return fmt.Errorf("key %d: %w", k, err)
}
}
return d.EndMap()
}
// Clone returns a deep copy of p.
func (p *Profile) Clone() *Profile {
c := *p
c.PublicKey = bytes.Clone(p.PublicKey)
return &c
}
// CanonicalCBOR returns the exact Deterministic CBOR bytes of spec §11.
func (p *Profile) CanonicalCBOR() ([]byte, error) {
if p.Period <= 0 || p.Period%time.Second != 0 {
return nil, fmt.Errorf("profile: period %s is not a positive whole number of seconds: %w", p.Period, datekeys.ErrNonCanonicalCBOR)
}
// Spec §11: genesis_time in 0..2^53-1. The period needs no such check:
// a time.Duration holds at most about 9.2e9 seconds.
if p.GenesisTime < 0 || p.GenesisTime > codec.MaxSafeUint {
return nil, fmt.Errorf("profile: genesis time %d outside 0..%d: %w", p.GenesisTime, int64(codec.MaxSafeUint), datekeys.ErrNonCanonicalCBOR)
}
w := wire{
ID: p.ID,
Provider: p.Provider,
Network: p.Network,
ChainHash: p.ChainHash[:],
PublicKey: p.PublicKey,
Period: uint64(p.Period / time.Second),
GenesisTime: uint64(p.GenesisTime),
Scheme: p.Scheme,
GenesisSeed: p.GenesisSeed[:],
}
var e codec.Encoder
w.encode(&e)
return e.Out()
}
// Hash returns profile_hash = SHA-256(exact_deterministic_cbor_bytes) (spec §11).
//
// A profile_hash declared by a remote party has no security value; security
// comes from the profile pinned locally (spec §11, §13).
func (p *Profile) Hash() ([32]byte, error) {
b, err := p.CanonicalCBOR()
if err != nil {
return [32]byte{}, err
}
return sha256.Sum256(b), nil
}
// Decode parses the Deterministic CBOR encoding of a Provider Profile and
// validates it. It does not make the profile trusted: only a Registry built by
// the caller does (spec §13).
func Decode(b []byte) (*Profile, error) {
if err := codec.CheckSchema(b, TypeTag, SchemaVersion); err != nil {
return nil, fmt.Errorf("profile: %w", err)
}
var w wire
if err := codec.Unmarshal(b, w.decode, w.encode); err != nil {
return nil, fmt.Errorf("profile: %w", err)
}
if w.Period > uint64(maxPeriod/time.Second) {
return nil, fmt.Errorf("profile: period %d s out of range: %w", w.Period, datekeys.ErrNonCanonicalCBOR)
}
p := &Profile{
ID: w.ID,
Provider: w.Provider,
Network: w.Network,
PublicKey: w.PublicKey,
Period: time.Duration(w.Period) * time.Second,
GenesisTime: int64(w.GenesisTime),
Scheme: w.Scheme,
}
copy(p.ChainHash[:], w.ChainHash)
copy(p.GenesisSeed[:], w.GenesisSeed)
if err := p.Validate(); err != nil {
return nil, err
}
return p, nil
}
// Validate checks the syntax of every field and, for drand profiles, that the
// scheme is supported, that the public key is a valid group element and that
// the chain hash is the drand chain-info hash of the other parameters. The
// last check is the self-verification kept from the prototype: a profile whose
// parameters do not produce its own chain hash is rejected.
func (p *Profile) Validate() error {
if !ValidID(p.ID) {
return fmt.Errorf("profile: invalid profile_id %q: %w", p.ID, datekeys.ErrUnknownProfile)
}
if !validName(p.Provider) || !validName(p.Network) || !validName(p.Scheme) {
return fmt.Errorf("profile %s: invalid provider, network or scheme name: %w", p.ID, datekeys.ErrUnknownProfile)
}
if len(p.PublicKey) == 0 || len(p.PublicKey) > maxPublicKeyLen {
return fmt.Errorf("profile %s: invalid public key length %d: %w", p.ID, len(p.PublicKey), datekeys.ErrUnknownProfile)
}
if p.Period <= 0 || p.Period > maxPeriod || p.Period%time.Second != 0 {
return fmt.Errorf("profile %s: invalid period %s: %w", p.ID, p.Period, datekeys.ErrUnknownProfile)
}
if p.GenesisTime <= 0 || p.GenesisTime >= MaxUnixTime {
return fmt.Errorf("profile %s: invalid genesis time %d: %w", p.ID, p.GenesisTime, datekeys.ErrUnknownProfile)
}
if p.Provider != ProviderDrand {
return fmt.Errorf("profile %s: unsupported provider %q: %w", p.ID, p.Provider, datekeys.ErrUnknownProfile)
}
return p.validateDrand()
}
func (p *Profile) validateDrand() error {
scheme, err := p.DrandScheme()
if err != nil {
return err
}
switch scheme.Name {
case crypto.SigsOnG1ID, crypto.UnchainedSchemeID, crypto.ShortSigSchemeID:
default:
return fmt.Errorf("profile %s: scheme %q is not supported by tlock: %w", p.ID, scheme.Name, datekeys.ErrUnknownProfile)
}
key := scheme.KeyGroup.Point()
if err := key.UnmarshalBinary(p.PublicKey); err != nil {
return fmt.Errorf("profile %s: public key is not a %s group element: %w", p.ID, scheme.Name, datekeys.ErrUnknownProfile)
}
if key.Equal(key.Null()) {
return fmt.Errorf("profile %s: public key is the identity element: %w", p.ID, datekeys.ErrUnknownProfile)
}
info := chain.Info{
PublicKey: key,
ID: p.Network,
Period: p.Period,
Scheme: p.Scheme,
GenesisTime: p.GenesisTime,
GenesisSeed: p.GenesisSeed[:],
}
if !bytes.Equal(info.Hash(), p.ChainHash[:]) {
return fmt.Errorf("profile %s: parameters hash to chain %s, not the pinned %s: %w",
p.ID, info.HashString(), hex.EncodeToString(p.ChainHash[:]), datekeys.ErrProfileMismatch)
}
return nil
}
// DrandScheme returns a fresh drand scheme object for p. Fresh objects avoid
// sharing mutable kyber state between callers.
func (p *Profile) DrandScheme() (*crypto.Scheme, error) {
if p.Provider != ProviderDrand {
return nil, fmt.Errorf("profile %s: provider %q is not drand: %w", p.ID, p.Provider, datekeys.ErrUnknownProfile)
}
scheme, err := crypto.SchemeFromName(p.Scheme)
if err != nil {
return nil, fmt.Errorf("profile %s: %v: %w", p.ID, err, datekeys.ErrUnknownProfile)
}
return scheme, nil
}
// ChainHashHex returns the lowercase hexadecimal chain hash, the form used in
// tlock stanzas and drand relay URLs.
func (p *Profile) ChainHashHex() string { return hex.EncodeToString(p.ChainHash[:]) }
// MaxRound is the last round whose round time is not after MaxUnixTime.
func (p *Profile) MaxRound() uint64 {
period := int64(p.Period / time.Second)
if period <= 0 || p.GenesisTime >= MaxUnixTime {
return 0
}
return uint64((MaxUnixTime-p.GenesisTime)/period) + 1
}
// ValidID reports whether s is a syntactically valid profile_id: 1 to 128
// characters from [a-z0-9:._-], starting with a letter or digit. The restricted
// alphabet keeps the dk1_ JSON form free of escapes (spec §18, §19).
func ValidID(s string) bool {
if len(s) == 0 || len(s) > maxIDLen || !alnum(s[0]) {
return false
}
for i := 0; i < len(s); i++ {
c := s[i]
if !alnum(c) && c != ':' && c != '.' && c != '_' && c != '-' {
return false
}
}
return true
}
func validName(s string) bool {
if len(s) == 0 || len(s) > maxNameLen || !alnum(s[0]) {
return false
}
for i := 0; i < len(s); i++ {
c := s[i]
if !alnum(c) && c != '.' && c != '_' && c != '-' {
return false
}
}
return true
}
func alnum(c byte) bool { return (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') }

Powered by TurnKey Linux.