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/provider/release.go

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) }

Powered by TurnKey Linux.