| 1 | package dbreads |
| 2 | |
| 3 | import ( |
| 4 | "crypto/ed25519" |
| 5 | "errors" |
| 6 | "time" |
| 7 | "unicode/utf8" |
| 8 | |
| 9 | "github.com/carlosframework/platform/internal/provision" |
| 10 | "github.com/carlosframework/platform/internal/release" |
| 11 | ) |
| 12 | |
| 13 | // ErrEnvelope is a request this package will not sign or a box will not |
| 14 | // accept: well signed, perhaps, but not for this id, region or moment. |
| 15 | var ErrEnvelope = errors.New("dbreads: request refused") |
| 16 | |
| 17 | // Request is db-reads/<id>/request.json's payload (spec §5.5). The box |
| 18 | // decodes it with release.VerifyRootEnvelope, which requires canonical JSON |
| 19 | // for this exact type, so the field order here is the wire's. |
| 20 | type Request struct { |
| 21 | Purpose string `json:"purpose"` |
| 22 | V int `json:"v"` |
| 23 | ID string `json:"id"` |
| 24 | Account string `json:"account"` |
| 25 | App string `json:"app"` |
| 26 | Host string `json:"host"` |
| 27 | Incarnation string `json:"incarnation"` |
| 28 | Region string `json:"region"` |
| 29 | Op string `json:"op"` |
| 30 | SQL string `json:"sql"` |
| 31 | At string `json:"at"` |
| 32 | ReplyKey string `json:"reply_key"` |
| 33 | By string `json:"by"` |
| 34 | IssuedAt string `json:"issued_at"` |
| 35 | } |
| 36 | |
| 37 | // Issued is IssuedAt as a time. |
| 38 | func (r Request) Issued() (time.Time, error) { return time.Parse(time.RFC3339, r.IssuedAt) } |
| 39 | |
| 40 | // AtTime is At as a time; zero for the newest copy. |
| 41 | func (r Request) AtTime() (time.Time, error) { |
| 42 | if r.At == "" { |
| 43 | return time.Time{}, nil |
| 44 | } |
| 45 | return time.Parse(time.RFC3339, r.At) |
| 46 | } |
| 47 | |
| 48 | // ops are the operations this build serves. |
| 49 | var ops = map[string]bool{OpQuery: true, OpPull: true} |
| 50 | |
| 51 | func (r Request) valid() bool { |
| 52 | if r.Purpose != Purpose || r.V != 1 || !ValidID(r.ID) || !ValidRegion(r.Region) || |
| 53 | !release.ValidAccount(r.Account) || !release.ValidApp(r.App) || !provision.ValidHostShape(r.Host) || |
| 54 | r.Incarnation == "" || !ops[r.Op] || r.By == "" { |
| 55 | return false |
| 56 | } |
| 57 | switch r.Op { |
| 58 | case OpQuery: |
| 59 | if r.SQL == "" || len(r.SQL) > MaxSQLBytes || !utf8.ValidString(r.SQL) { |
| 60 | return false |
| 61 | } |
| 62 | case OpPull: |
| 63 | // A pull runs nothing: SQL in one is a request nobody built. |
| 64 | if r.SQL != "" { |
| 65 | return false |
| 66 | } |
| 67 | } |
| 68 | if _, err := r.AtTime(); err != nil { |
| 69 | return false |
| 70 | } |
| 71 | if _, err := r.Issued(); err != nil { |
| 72 | return false |
| 73 | } |
| 74 | _, err := ParseReplyKey(r.ReplyKey) |
| 75 | return err == nil |
| 76 | } |
| 77 | |
| 78 | // Sign is the console's half: it fills Purpose and V, refuses a request it |
| 79 | // could not carry intact (invalid UTF-8 would be rewritten by encoding/json, |
| 80 | // so the box would run SQL nobody typed), and signs with the root. |
| 81 | // release.SignRootEnvelope refuses a scoped key. |
| 82 | func Sign(sg release.Signer, req Request) ([]byte, error) { |
| 83 | req.Purpose, req.V = Purpose, 1 |
| 84 | if !req.valid() { |
| 85 | return nil, ErrEnvelope |
| 86 | } |
| 87 | return release.SignRootEnvelope(sg, req) |
| 88 | } |
| 89 | |
| 90 | // Verify is the box's half (spec §5.5): the root signature over canonical |
| 91 | // JSON, this package's purpose, the id the object was read under, this box's |
| 92 | // own region, and an issue time no more than PickupWindow old and Skew in the |
| 93 | // future. The age bound is PickupWindow, not PickupWindow+Skew: InWindow |
| 94 | // already widened the listing by Skew, and the envelope is checked against |
| 95 | // this box's own clock. |
| 96 | func Verify(pub ed25519.PublicKey, wire []byte, id, region string, now time.Time) (Request, error) { |
| 97 | var req Request |
| 98 | if err := release.VerifyRootEnvelope(pub, wire, &req); err != nil { |
| 99 | return Request{}, err |
| 100 | } |
| 101 | if !req.valid() || req.ID != id || req.Region != region { |
| 102 | return Request{}, ErrEnvelope |
| 103 | } |
| 104 | issued, _ := req.Issued() |
| 105 | if issued.Before(now.Add(-PickupWindow)) || issued.After(now.Add(Skew)) { |
| 106 | return Request{}, ErrEnvelope |
| 107 | } |
| 108 | return req, nil |
| 109 | } |
| 110 | |
| 111 | // QueueMatches is the queue entry check (spec §5.9): the entry's region, |
| 112 | // hour, time and id are all the envelope's, so a box's forged entry for a |
| 113 | // real request names nothing the request does not. |
| 114 | func QueueMatches(e QueueEntry, req Request) bool { |
| 115 | issued, err := req.Issued() |
| 116 | return err == nil && e.Region == req.Region && e.ID == req.ID && e.Issued.Equal(issued) && |
| 117 | e.Hour == issued.UTC().Format(hourLayout) |
| 118 | } |
| 119 | |