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

260 lines
8.9 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"
"time"
"github.com/drand/drand/v2/common/chain"
"github.com/drand/drand/v2/crypto"
datekeys "github.com/datekeys/datekeys-go"
"github.com/datekeys/datekeys-go/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. Every key is required.
type wire struct {
Type string `cbor:"0,keyasint"`
Version uint64 `cbor:"1,keyasint"`
ID string `cbor:"2,keyasint"`
Provider string `cbor:"3,keyasint"`
Network string `cbor:"4,keyasint"`
ChainHash []byte `cbor:"5,keyasint"`
PublicKey []byte `cbor:"6,keyasint"`
Period uint64 `cbor:"7,keyasint"`
GenesisTime int64 `cbor:"8,keyasint"`
Scheme string `cbor:"9,keyasint"`
GenesisSeed []byte `cbor:"10,keyasint"`
}
// 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", p.Period)
}
return codec.Marshal(wire{
Type: TypeTag,
Version: SchemaVersion,
ID: p.ID,
Provider: p.Provider,
Network: p.Network,
ChainHash: p.ChainHash[:],
PublicKey: p.PublicKey,
Period: uint64(p.Period / time.Second),
GenesisTime: p.GenesisTime,
Scheme: p.Scheme,
GenesisSeed: p.GenesisSeed[:],
})
}
// 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); err != nil {
return nil, fmt.Errorf("profile: %w", err)
}
if len(w.ChainHash) != 32 || len(w.GenesisSeed) != 32 {
return nil, fmt.Errorf("profile: chain hash and genesis seed must be 32 bytes: %w", datekeys.ErrNonCanonicalCBOR)
}
if w.Period == 0 || 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: 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.