carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package dbreads
2
3import (
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.
15var 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.
20type 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.
38func (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.
41func (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.
49var ops = map[string]bool{OpQuery: true, OpPull: true}
50
51func (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.
82func 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.
96func 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.
114func 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