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