// Package provider defines conditions, releases and release sources (spec §9, // §45-§52) and the local verification every release must pass. // // A release is never trusted because of where it came from: the DateKeys API, // a cache or a relay are untrusted transports (spec §3, §48, §52). A remote // "verified: true" has no security value (spec §51). package provider import ( "bytes" "context" "crypto/sha256" "encoding/hex" "fmt" "strings" "github.com/drand/drand/v2/common" datekeys "g.activething.com/go/DateKeys" "g.activething.com/go/DateKeys/profile" ) // Condition is the time condition of a Quicknet-style profile: a round. type Condition struct { Round uint64 } // Release is the material that satisfies a condition. For drand it is the // BLS signature of the round. type Release struct { Round uint64 Signature []byte // ChainHash is the chain the release names, key 2 of a release object // (spec v0.15, §47.1), or nil when it names none, as the answer of a // relay and drand's JSON. Verify compares it with the pinned profile. ChainHash []byte } // ReleaseSource fetches the release of a condition. Callers always verify the // release, with Verify (spec §63 step 10). // // A source that fetches releases over a network (a relay, the Release API or // a cache) must also verify each response with Verify and discard the one // that fails, and report datekeys.ErrReleaseUnavailable when no response // passes (spec §63 step 9), as provider/drand.Client does: an invalid release // from the network is then reported at step 9, not with the codes of step 10. // A source that hands over a release the caller supplies directly need not // verify it; its release gets the codes of step 10. // // Errors should wrap datekeys.ErrReleaseUnavailable, and no other normative // error, when the release cannot be obtained, for example because the round // is not published yet. capsule.Open reports any error of a source at step 9 // with ErrReleaseUnavailable as its only code, the one spec §63 gives that // step, and keeps only the text of an error that carries another code. type ReleaseSource interface { Fetch(ctx context.Context, p *profile.Profile, c Condition) (Release, error) } // ReleaseSourceFunc adapts a function to ReleaseSource. type ReleaseSourceFunc func(ctx context.Context, p *profile.Profile, c Condition) (Release, error) // Fetch calls f. func (f ReleaseSourceFunc) Fetch(ctx context.Context, p *profile.Profile, c Condition) (Release, error) { return f(ctx, p, c) } // Verify checks a release locally against the pinned profile (spec §17, §51), // in the order of spec §63 step 10: the chain hash it names, if any, which // must be that of the pinned profile (ErrProfileMismatch, spec v0.15); the // expected round (ErrRoundMismatch), // then the signature (ErrReleaseInvalid), which must be the canonical // encoding of a point of the signature group of the scheme other than the // point at infinity (spec §12.2), with the length of that group, and a valid // BLS signature of the round under the pinned public key. The chain hash is // covered too because the pinned public key is bound to it by // profile.Validate. // // The BLS verification of drand decodes the signature with the canonical // decoder of kilic/bls12-381, which rejects every other encoding, and the // point at infinity never verifies because the pinned public key is not the // point at infinity. The error of drand is not copied. func Verify(p *profile.Profile, c Condition, r Release) error { if c.Round == 0 || c.Round > p.MaxRound() { return fmt.Errorf("provider: round %d outside the range of %s: %w", c.Round, p.ID, datekeys.ErrDateKeyInvalid) } if r.ChainHash != nil && !bytes.Equal(r.ChainHash, p.ChainHash[:]) { return fmt.Errorf("provider: release of chain %s, the pinned profile %s is chain %s: %w", chainHashHex(r.ChainHash), p.ID, p.ChainHashHex(), datekeys.ErrProfileMismatch) } if r.Round != c.Round { return fmt.Errorf("provider: release for round %d, expected %d: %w", r.Round, c.Round, datekeys.ErrRoundMismatch) } scheme, err := p.DrandScheme() if err != nil { return err } if want := scheme.SigGroup.PointLen(); len(r.Signature) != want { return fmt.Errorf("provider: signature is %d bytes, %s uses %d: %w", len(r.Signature), scheme.Name, want, datekeys.ErrReleaseInvalid) } key := scheme.KeyGroup.Point() if err := key.UnmarshalBinary(p.PublicKey); err != nil { return fmt.Errorf("provider: pinned public key of %s is not the canonical encoding of a point of the key group: %w", p.ID, datekeys.ErrUnknownProfile) } beacon := &common.Beacon{Round: r.Round, Signature: r.Signature} if err := scheme.VerifyBeacon(beacon, key); err != nil { return fmt.Errorf("provider: the signature is not a canonical point encoding, or does not verify as the BLS signature of round %d under %s: %w", r.Round, p.ID, datekeys.ErrReleaseInvalid) } return nil } // randomnessMatches reports whether the randomness of a drand answer, in // hexadecimal, is SHA-256 of its signature, as drand defines it. func randomnessMatches(randomness string, signature []byte) bool { sum := sha256.Sum256(signature) return strings.EqualFold(randomness, hex.EncodeToString(sum[:])) }