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/archive.go

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

Powered by TurnKey Linux.