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.
209 lines
8.0 KiB
209 lines
8.0 KiB
package provider
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"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, the content of a .dkr file (spec
|
|
// v0.15, §47.1).
|
|
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, the content of a .dkr file
|
|
// (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.
|
|
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)
|
|
}
|
|
var wire struct {
|
|
Round *uint64 `json:"round"`
|
|
Signature *string `json:"signature"`
|
|
Randomness string `json:"randomness"`
|
|
}
|
|
if err := json.Unmarshal(b, &wire); err != nil || wire.Round == nil || wire.Signature == nil {
|
|
return Release{}, fmt.Errorf("provider: drand JSON: malformed, or without round or signature: %w", datekeys.ErrReleaseInvalid)
|
|
}
|
|
sig, err := hex.DecodeString(*wire.Signature)
|
|
if err != nil {
|
|
return Release{}, fmt.Errorf("provider: drand JSON: signature is not hex: %w", datekeys.ErrReleaseInvalid)
|
|
}
|
|
if wire.Randomness != "" && !randomnessMatches(wire.Randomness, sig) {
|
|
return Release{}, fmt.Errorf("provider: drand JSON: randomness does not match the signature: %w", datekeys.ErrReleaseInvalid)
|
|
}
|
|
return Release{Round: *wire.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 .dkr 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 .dkr file 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 reader saves as a .dkr after verifying the
|
|
// release (spec v0.15, §62.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) }
|