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.
176 lines
6.1 KiB
176 lines
6.1 KiB
package provider
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
|
|
datekeys "g.activething.com/go/DateKeys"
|
|
"g.activething.com/go/DateKeys/codec"
|
|
"g.activething.com/go/DateKeys/profile"
|
|
)
|
|
|
|
// Schema constants of the header of a release archive, an informative format
|
|
// (spec v0.15, §50).
|
|
const (
|
|
ArchiveTypeTag = "datekeys-release-archive"
|
|
ArchiveSchemaVersion = 1
|
|
)
|
|
|
|
// maxArchiveHeader bounds the header of an archive: its five keys take at
|
|
// most 1 + 26 + 2 + 35 + 9 + 9 bytes.
|
|
const maxArchiveHeader = 128
|
|
|
|
// archiveKeys is the number of keys of the header, all required.
|
|
const archiveKeys = 5
|
|
|
|
// archiveHeader is the CBOR map at the start of an archive.
|
|
type archiveHeader struct {
|
|
ChainHash []byte // key 2, 32 bytes
|
|
First uint64 // key 3, the first round, 1..2^53-1
|
|
Count uint64 // key 4, the number of rounds, 1..2^53-1
|
|
}
|
|
|
|
func (h *archiveHeader) encode(e *codec.Encoder) {
|
|
e.Map(archiveKeys)
|
|
e.Uint(0)
|
|
e.Text(ArchiveTypeTag)
|
|
e.Uint(1)
|
|
e.Uint(ArchiveSchemaVersion)
|
|
e.Uint(2)
|
|
e.Bstr(h.ChainHash)
|
|
e.Uint(3)
|
|
e.Uint(h.First)
|
|
e.Uint(4)
|
|
e.Uint(h.Count)
|
|
}
|
|
|
|
func (h *archiveHeader) decode(d *codec.Decoder) error {
|
|
pairs, err := d.Map(archiveKeys)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if pairs != archiveKeys {
|
|
return fmt.Errorf("%d keys, want all %d", pairs, archiveKeys)
|
|
}
|
|
for want := range uint64(archiveKeys) {
|
|
k, err := d.Key()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if k != want {
|
|
return fmt.Errorf("key %d where key %d was expected", k, want)
|
|
}
|
|
switch k {
|
|
case 0:
|
|
_, err = d.Text(len(ArchiveTypeTag))
|
|
case 1:
|
|
_, err = d.Uint(ArchiveSchemaVersion)
|
|
case 2:
|
|
h.ChainHash, err = d.Bstr(32, 32)
|
|
case 3:
|
|
h.First, err = d.Uint(codec.MaxSafeUint)
|
|
case 4:
|
|
h.Count, err = d.Uint(codec.MaxSafeUint)
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("key %d: %w", k, err)
|
|
}
|
|
}
|
|
return d.EndMap()
|
|
}
|
|
|
|
// EncodeArchiveHeader returns the header of an archive of count rounds of
|
|
// the chain chainHash from the round first (spec v0.15, §50). The signatures
|
|
// follow it, one after another, each with the length of a signature of the
|
|
// chain, and a round the archive lacks is written as zeros.
|
|
func EncodeArchiveHeader(chainHash []byte, first, count uint64) ([]byte, error) {
|
|
if len(chainHash) != 32 || first == 0 || count == 0 || first > codec.MaxSafeUint-count+1 {
|
|
return nil, fmt.Errorf("provider: archive header: chain hash of %d bytes, rounds %d to %d + %d - 1", len(chainHash), first, first, count)
|
|
}
|
|
h := archiveHeader{ChainHash: chainHash, First: first, Count: count}
|
|
var e codec.Encoder
|
|
h.encode(&e)
|
|
return e.Out()
|
|
}
|
|
|
|
// IsArchive reports whether b, the start of a file, is the start of a
|
|
// release archive: a map whose type tag is ArchiveTypeTag.
|
|
func IsArchive(b []byte) bool {
|
|
tag, _, err := codec.Peek(b)
|
|
return err == nil && tag == ArchiveTypeTag
|
|
}
|
|
|
|
// Archive is a local release archive, the informative format of spec v0.15,
|
|
// §50: a header in deterministic CBOR, {0: "datekeys-release-archive", 1: 1,
|
|
// 2: chain_hash, 3: first round, 4: number of rounds}, followed by the
|
|
// signatures, so that the one of round r starts at the end of the header plus
|
|
// (r - first)·n, with n the length of a signature of the chain, 48 bytes in
|
|
// Quicknet. A round written as zeros is missing.
|
|
//
|
|
// A local archive is a release in hand: it implements Supplier, and its
|
|
// entry is decoded and verified at step 10 of spec §63 like any release
|
|
// object. A round it lacks, a header it cannot read, an archive of another
|
|
// chain or of another length are failures to supply a release,
|
|
// ErrReleaseUnavailable at step 9: the format is informative and has no codes of its own.
|
|
type Archive struct {
|
|
r io.ReaderAt
|
|
size int64
|
|
}
|
|
|
|
var _ Supplier = (*Archive)(nil)
|
|
|
|
// NewArchive returns the archive of size bytes read from r. It reads nothing
|
|
// until Supply.
|
|
func NewArchive(r io.ReaderAt, size int64) *Archive { return &Archive{r: r, size: size} }
|
|
|
|
// Supply implements Supplier: it returns the release object of the round of
|
|
// c, with the chain hash of the header of the archive.
|
|
func (a *Archive) Supply(p *profile.Profile, c Condition) ([]byte, error) {
|
|
unavailable := func(format string, args ...any) error {
|
|
return fmt.Errorf("provider: release archive: "+format+": %w", append(args, datekeys.ErrReleaseUnavailable)...)
|
|
}
|
|
head := make([]byte, min(a.size, maxArchiveHeader))
|
|
if _, err := a.r.ReadAt(head, 0); err != nil && err != io.EOF {
|
|
return nil, unavailable("%v", err)
|
|
}
|
|
if err := codec.CheckSchema(head, ArchiveTypeTag, ArchiveSchemaVersion); err != nil {
|
|
return nil, unavailable("not an archive of version %d", ArchiveSchemaVersion)
|
|
}
|
|
var h archiveHeader
|
|
d := codec.NewDecoder(head)
|
|
if err := h.decode(d); err != nil {
|
|
return nil, unavailable("its header does not decode: %v", err)
|
|
}
|
|
// The header is the deterministic encoding of what it says: its length
|
|
// is that of the encoding, and the signatures follow it.
|
|
var e codec.Encoder
|
|
h.encode(&e)
|
|
enc, err := e.Out()
|
|
if err != nil || !bytes.Equal(enc, head[:min(len(enc), len(head))]) {
|
|
return nil, unavailable("its header is not the deterministic encoding of its value")
|
|
}
|
|
if !bytes.Equal(h.ChainHash, p.ChainHash[:]) {
|
|
return nil, unavailable("archive of chain %s, the pinned profile %s is chain %s", chainHashHex(h.ChainHash), p.ID, p.ChainHashHex())
|
|
}
|
|
if h.First == 0 || h.Count == 0 || c.Round < h.First || c.Round-h.First >= h.Count {
|
|
return nil, unavailable("round %d is not in the archive, which holds %d rounds from %d", c.Round, h.Count, h.First)
|
|
}
|
|
scheme, err := p.DrandScheme()
|
|
if err != nil {
|
|
return nil, unavailable("%v", err)
|
|
}
|
|
n := uint64(scheme.SigGroup.PointLen())
|
|
if want := uint64(len(enc)) + h.Count*n; h.Count > (1<<62)/n || uint64(a.size) != want {
|
|
return nil, unavailable("%d bytes, its header announces %d rounds of %d bytes", a.size, h.Count, n)
|
|
}
|
|
sig := make([]byte, n)
|
|
if _, err := a.r.ReadAt(sig, int64(uint64(len(enc))+(c.Round-h.First)*n)); err != nil {
|
|
return nil, unavailable("%v", err)
|
|
}
|
|
if bytes.Equal(sig, make([]byte, n)) {
|
|
return nil, unavailable("round %d is missing: its entry is zeros", c.Round)
|
|
}
|
|
return EncodeRelease(Release{ChainHash: h.ChainHash, Round: c.Round, Signature: sig})
|
|
}
|