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

372 lines
12 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"
"unicode/utf8"
"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, the last representable instant of
// spec §15. 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. maxPeriod also keeps period within
// the 32 bits the chain-info hash gives it, the normative bound of spec
// §12.1.
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 applies rules 1 to 3 of spec §12.1 to a Profile value, in their
// order, so that it reports the code Decode reports for the encoding of the
// value (spec §69.1):
//
// 1. the schema rules a value can break (ErrNonCanonicalCBOR): a period
// that is not a whole number of seconds in 1..86400, the implementation
// limit of spec §74; a genesis time outside 0..2^53-1; a name that is not
// valid UTF-8;
// 2. the rules of each field (ErrUnknownProfile): the name alphabets and
// lengths, the public key length, genesis_time in 1..253402300798, the
// provider drand, a scheme tlock supports and a public key in the key
// group of the scheme;
// 3. the chain-hash self-check (ErrProfileMismatch): the chain hash is the
// drand chain-info hash of the other parameters, so that a profile whose
// parameters do not produce its own chain hash is rejected.
func (p *Profile) Validate() error {
if p.Period <= 0 || p.Period > maxPeriod || p.Period%time.Second != 0 {
return fmt.Errorf("profile %q: period %s is not a whole number of seconds in 1..%d: %w", p.ID, p.Period, int64(maxPeriod/time.Second), datekeys.ErrNonCanonicalCBOR)
}
if p.GenesisTime < 0 || p.GenesisTime > codec.MaxSafeUint {
return fmt.Errorf("profile %q: genesis time %d outside 0..%d: %w", p.ID, p.GenesisTime, int64(codec.MaxSafeUint), datekeys.ErrNonCanonicalCBOR)
}
for _, s := range []string{p.ID, p.Provider, p.Network, p.Scheme} {
if !utf8.ValidString(s) {
return fmt.Errorf("profile %q: name %q is not valid UTF-8: %w", p.ID, s, datekeys.ErrNonCanonicalCBOR)
}
}
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.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 the canonical encoding of a point of the key group of %s: %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: %q is not a drand scheme: %w", p.ID, p.Scheme, 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
// (spec §15), or 0 when there is none.
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.