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.
237 lines
9.0 KiB
237 lines
9.0 KiB
package provider
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/hex"
|
|
"fmt"
|
|
|
|
datekeys "g.activething.com/go/DateKeys"
|
|
"g.activething.com/go/DateKeys/codec"
|
|
"g.activething.com/go/DateKeys/profile"
|
|
)
|
|
|
|
// Schema constants of the release object (spec v0.15, §47.1): the answer of
|
|
// the Release API, an entry of a Release Cache, and a release the caller
|
|
// gives from a file or from an archive.
|
|
const (
|
|
ReleaseTypeTag = "datekeys-release"
|
|
ReleaseSchemaVersion = 1
|
|
)
|
|
|
|
// Limits of the release object (spec v0.15, §47.1). The object has no frame:
|
|
// a file or an input of more than MaxReleaseObjectSize bytes is rejected
|
|
// before it is decoded, with ErrNonCanonicalCBOR, and no valid encoding comes
|
|
// close to it. MaxSignatureLen is the longest compressed point of
|
|
// BLS12-381, one of G2; Quicknet signs with 48 bytes, a point of G1.
|
|
const (
|
|
MaxReleaseObjectSize = 1024
|
|
MaxSignatureLen = 96
|
|
)
|
|
|
|
// MaxReleaseJSONSize bounds drand's JSON, which a reader accepts too as the
|
|
// input of the caller (spec v0.15, §47.1). It is the bound of a relay
|
|
// response in provider/drand.
|
|
const MaxReleaseJSONSize = 8 << 10
|
|
|
|
// releaseKeys is the number of keys of the release object, all required.
|
|
const releaseKeys = 5
|
|
|
|
// releaseWire is the CBOR map of the release object, keys 2 to 4; keys 0 and
|
|
// 1 are the constants ReleaseTypeTag and ReleaseSchemaVersion.
|
|
type releaseWire struct {
|
|
ChainHash []byte // key 2, 32 bytes
|
|
Round uint64 // key 3, 1..2^53-1
|
|
Signature []byte // key 4, 1..MaxSignatureLen bytes
|
|
}
|
|
|
|
func (w *releaseWire) encode(e *codec.Encoder) {
|
|
e.Map(releaseKeys)
|
|
e.Uint(0)
|
|
e.Text(ReleaseTypeTag)
|
|
e.Uint(1)
|
|
e.Uint(ReleaseSchemaVersion)
|
|
e.Uint(2)
|
|
e.Bstr(w.ChainHash)
|
|
e.Uint(3)
|
|
e.Uint(w.Round)
|
|
e.Uint(4)
|
|
e.Bstr(w.Signature)
|
|
}
|
|
|
|
// decode reads the map with every CDDL rule of the release object, all of
|
|
// them ErrNonCanonicalCBOR: what each field means against the pinned profile
|
|
// and the DateKey is checked by Verify, at step 10.
|
|
func (w *releaseWire) decode(d *codec.Decoder) error {
|
|
pairs, err := d.Map(releaseKeys)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if pairs != releaseKeys {
|
|
return fmt.Errorf("%d keys, want all %d: %w", pairs, releaseKeys, datekeys.ErrNonCanonicalCBOR)
|
|
}
|
|
for want := range uint64(releaseKeys) {
|
|
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(ReleaseTypeTag))
|
|
case 1:
|
|
_, err = d.Uint(ReleaseSchemaVersion)
|
|
case 2:
|
|
w.ChainHash, err = d.Bstr(32, 32)
|
|
case 3:
|
|
if w.Round, err = d.Uint(codec.MaxSafeUint); err == nil && w.Round == 0 {
|
|
err = fmt.Errorf("round 0: %w", datekeys.ErrNonCanonicalCBOR)
|
|
}
|
|
case 4:
|
|
w.Signature, err = d.Bstr(1, MaxSignatureLen)
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("key %d: %w", k, err)
|
|
}
|
|
}
|
|
return d.EndMap()
|
|
}
|
|
|
|
// EncodeRelease returns the release object of r (spec v0.15, §47.1): its
|
|
// chain hash, its round and its signature. It does
|
|
// not verify the release: Verify does, against the pinned profile.
|
|
func EncodeRelease(r Release) ([]byte, error) {
|
|
switch {
|
|
case len(r.ChainHash) != 32:
|
|
return nil, fmt.Errorf("provider: release object: chain hash of %d bytes, want 32: %w", len(r.ChainHash), datekeys.ErrNonCanonicalCBOR)
|
|
case r.Round == 0 || r.Round > codec.MaxSafeUint:
|
|
return nil, fmt.Errorf("provider: release object: round %d outside 1..%d: %w", r.Round, uint64(codec.MaxSafeUint), datekeys.ErrNonCanonicalCBOR)
|
|
case len(r.Signature) == 0 || len(r.Signature) > MaxSignatureLen:
|
|
return nil, fmt.Errorf("provider: release object: signature of %d bytes outside 1..%d: %w", len(r.Signature), MaxSignatureLen, datekeys.ErrNonCanonicalCBOR)
|
|
}
|
|
w := releaseWire{ChainHash: r.ChainHash, Round: r.Round, Signature: r.Signature}
|
|
var e codec.Encoder
|
|
w.encode(&e)
|
|
return e.Out()
|
|
}
|
|
|
|
// DecodeRelease decodes a release object (spec v0.15, §47.1) with the layers
|
|
// of spec §69.1 that it has: its size, at most MaxReleaseObjectSize bytes;
|
|
// its type and schema version (ErrNonCanonicalCBOR, then
|
|
// ErrUnsupportedVersion); its encoding and schema (ErrNonCanonicalCBOR). The
|
|
// release it returns names its chain, and Verify checks it against the pinned
|
|
// profile at step 10 of spec §63: the chain hash, the round, the signature.
|
|
func DecodeRelease(b []byte) (Release, error) {
|
|
if len(b) == 0 || len(b) > MaxReleaseObjectSize {
|
|
return Release{}, fmt.Errorf("provider: release object of %d bytes, outside 1..%d: %w", len(b), MaxReleaseObjectSize, datekeys.ErrNonCanonicalCBOR)
|
|
}
|
|
if err := codec.CheckSchema(b, ReleaseTypeTag, ReleaseSchemaVersion); err != nil {
|
|
return Release{}, fmt.Errorf("provider: release object: %w", err)
|
|
}
|
|
var w releaseWire
|
|
if err := codec.Unmarshal(b, w.decode, w.encode); err != nil {
|
|
return Release{}, fmt.Errorf("provider: release object: %w", err)
|
|
}
|
|
return Release{Round: w.Round, Signature: w.Signature, ChainHash: w.ChainHash}, nil
|
|
}
|
|
|
|
// ParseRelease reads a release that the caller supplies: drand's JSON, when
|
|
// its first byte other than a JSON space is "{", or else a release object
|
|
// (spec v0.15, §47.1), with DecodeRelease. drand's JSON is the answer of a
|
|
// relay, {"round": …, "signature": "…"}, with an optional "randomness" that
|
|
// must be SHA-256 of the signature; it does not name its chain, so the
|
|
// release has no chain hash, and any failure to read it is
|
|
// ErrReleaseInvalid. It is accepted as input, never written.
|
|
func ParseRelease(b []byte) (Release, error) {
|
|
if t := bytes.TrimLeft(b, " \t\r\n"); len(t) > 0 && t[0] == '{' {
|
|
return ParseDrandJSON(b)
|
|
}
|
|
return DecodeRelease(b)
|
|
}
|
|
|
|
// ParseDrandJSON reads the JSON of a drand relay with the strict rules of
|
|
// spec v0.16, §47.1: at most MaxReleaseJSONSize bytes of JSON whose value is
|
|
// an object, with no repeated name and names compared exactly once their
|
|
// escapes are decoded; "round" a number without sign, fraction or exponent,
|
|
// from 1 to 2^53 - 1; "signature" a string of hexadecimal, in lower or upper
|
|
// case; and "randomness", when present, a string with SHA-256 of the
|
|
// signature in hexadecimal. Other members are ignored. Any failure is
|
|
// ErrReleaseInvalid. provider/drand reads the answers of the relays with it.
|
|
func ParseDrandJSON(b []byte) (Release, error) {
|
|
if len(b) > MaxReleaseJSONSize {
|
|
return Release{}, fmt.Errorf("provider: drand JSON of %d bytes, larger than %d: %w", len(b), MaxReleaseJSONSize, datekeys.ErrReleaseInvalid)
|
|
}
|
|
malformed := fmt.Errorf("provider: drand JSON: malformed, or without round or signature: %w", datekeys.ErrReleaseInvalid)
|
|
members, ok := strictJSON(b)
|
|
if !ok {
|
|
return Release{}, malformed
|
|
}
|
|
var round uint64
|
|
var signature, randomness *string
|
|
for _, m := range members {
|
|
switch m.name {
|
|
case "round":
|
|
if round, ok = jsonRound(m); !ok {
|
|
return Release{}, malformed
|
|
}
|
|
case "signature", "randomness":
|
|
if m.kind != '"' {
|
|
return Release{}, malformed
|
|
}
|
|
v := m.str
|
|
if m.name == "signature" {
|
|
signature = &v
|
|
} else {
|
|
randomness = &v
|
|
}
|
|
}
|
|
}
|
|
if round == 0 || signature == nil {
|
|
return Release{}, malformed
|
|
}
|
|
sig, err := hex.DecodeString(*signature)
|
|
if err != nil {
|
|
return Release{}, fmt.Errorf("provider: drand JSON: signature is not hex: %w", datekeys.ErrReleaseInvalid)
|
|
}
|
|
if randomness != nil && !randomnessMatches(*randomness, sig) {
|
|
return Release{}, fmt.Errorf("provider: drand JSON: randomness does not match the signature: %w", datekeys.ErrReleaseInvalid)
|
|
}
|
|
return Release{Round: round, Signature: sig}, nil
|
|
}
|
|
|
|
// Supplier hands over a release that the caller has in hand (spec v0.15,
|
|
// §49, §63 step 9.c): a release object read from a file, drand's JSON that
|
|
// the person saved, or an entry of a local archive. It makes no network
|
|
// request, so capsule.Open asks it for the release without comparing its
|
|
// clock with the round time: a valid signature proves that the round was
|
|
// published.
|
|
//
|
|
// Supply returns the encoding of the release of c, as it is: a release
|
|
// object or drand's JSON, which capsule.Open decodes and verifies at step 10
|
|
// with the codes of that step. Without a release for c it returns an error
|
|
// that wraps datekeys.ErrReleaseUnavailable, the code of step 9.
|
|
type Supplier interface {
|
|
Supply(p *profile.Profile, c Condition) ([]byte, error)
|
|
}
|
|
|
|
// Encoded is a release in hand, already read: the bytes of a release object
|
|
// or of drand's JSON. It supplies itself whatever the condition; step 10 compares
|
|
// its round with the DateKey.
|
|
type Encoded []byte
|
|
|
|
// Supply implements Supplier.
|
|
func (e Encoded) Supply(*profile.Profile, Condition) ([]byte, error) { return e, nil }
|
|
|
|
// NewReleaseObject returns the release object of a release of the profile p,
|
|
// with the chain hash of p: what a Release Cache or an archive stores, or
|
|
// the Release API serves, after verifying the release (spec v0.15, §45,
|
|
// §47, §47.1).
|
|
func NewReleaseObject(p *profile.Profile, r Release) ([]byte, error) {
|
|
r.ChainHash = p.ChainHash[:]
|
|
return EncodeRelease(r)
|
|
}
|
|
|
|
// chainHashHex is the chain hash of a release in the text of an error.
|
|
func chainHashHex(b []byte) string { return hex.EncodeToString(b) }
|