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