package dbreads import ( "crypto/ed25519" "errors" "time" "unicode/utf8" "github.com/carlosframework/platform/internal/provision" "github.com/carlosframework/platform/internal/release" ) // ErrEnvelope is a request this package will not sign or a box will not // accept: well signed, perhaps, but not for this id, region or moment. var ErrEnvelope = errors.New("dbreads: request refused") // Request is db-reads//request.json's payload (spec §5.5). The box // decodes it with release.VerifyRootEnvelope, which requires canonical JSON // for this exact type, so the field order here is the wire's. type Request struct { Purpose string `json:"purpose"` V int `json:"v"` ID string `json:"id"` Account string `json:"account"` App string `json:"app"` Host string `json:"host"` Incarnation string `json:"incarnation"` Region string `json:"region"` Op string `json:"op"` SQL string `json:"sql"` At string `json:"at"` ReplyKey string `json:"reply_key"` By string `json:"by"` IssuedAt string `json:"issued_at"` } // Issued is IssuedAt as a time. func (r Request) Issued() (time.Time, error) { return time.Parse(time.RFC3339, r.IssuedAt) } // AtTime is At as a time; zero for the newest copy. func (r Request) AtTime() (time.Time, error) { if r.At == "" { return time.Time{}, nil } return time.Parse(time.RFC3339, r.At) } // ops are the operations this build serves. var ops = map[string]bool{OpQuery: true, OpPull: true} func (r Request) valid() bool { if r.Purpose != Purpose || r.V != 1 || !ValidID(r.ID) || !ValidRegion(r.Region) || !release.ValidAccount(r.Account) || !release.ValidApp(r.App) || !provision.ValidHostShape(r.Host) || r.Incarnation == "" || !ops[r.Op] || r.By == "" { return false } switch r.Op { case OpQuery: if r.SQL == "" || len(r.SQL) > MaxSQLBytes || !utf8.ValidString(r.SQL) { return false } case OpPull: // A pull runs nothing: SQL in one is a request nobody built. if r.SQL != "" { return false } } if _, err := r.AtTime(); err != nil { return false } if _, err := r.Issued(); err != nil { return false } _, err := ParseReplyKey(r.ReplyKey) return err == nil } // Sign is the console's half: it fills Purpose and V, refuses a request it // could not carry intact (invalid UTF-8 would be rewritten by encoding/json, // so the box would run SQL nobody typed), and signs with the root. // release.SignRootEnvelope refuses a scoped key. func Sign(sg release.Signer, req Request) ([]byte, error) { req.Purpose, req.V = Purpose, 1 if !req.valid() { return nil, ErrEnvelope } return release.SignRootEnvelope(sg, req) } // Verify is the box's half (spec §5.5): the root signature over canonical // JSON, this package's purpose, the id the object was read under, this box's // own region, and an issue time no more than PickupWindow old and Skew in the // future. The age bound is PickupWindow, not PickupWindow+Skew: InWindow // already widened the listing by Skew, and the envelope is checked against // this box's own clock. func Verify(pub ed25519.PublicKey, wire []byte, id, region string, now time.Time) (Request, error) { var req Request if err := release.VerifyRootEnvelope(pub, wire, &req); err != nil { return Request{}, err } if !req.valid() || req.ID != id || req.Region != region { return Request{}, ErrEnvelope } issued, _ := req.Issued() if issued.Before(now.Add(-PickupWindow)) || issued.After(now.Add(Skew)) { return Request{}, ErrEnvelope } return req, nil } // QueueMatches is the queue entry check (spec §5.9): the entry's region, // hour, time and id are all the envelope's, so a box's forged entry for a // real request names nothing the request does not. func QueueMatches(e QueueEntry, req Request) bool { issued, err := req.Issued() return err == nil && e.Region == req.Region && e.ID == req.ID && e.Issued.Equal(issued) && e.Hour == issued.UTC().Format(hourLayout) }