| 1 | package operator |
| 2 | |
| 3 | import ( |
| 4 | "encoding/base64" |
| 5 | "strings" |
| 6 | "time" |
| 7 | |
| 8 | family "amadan.net/rastrillo/rastrillo/crypto" |
| 9 | "github.com/sqids/sqids-go" |
| 10 | ) |
| 11 | |
| 12 | const OwnerLimit = 8 << 10 |
| 13 | |
| 14 | var teamCodec, teamCodecError = sqids.New() |
| 15 | |
| 16 | func TeamSQID(id int64) (string, error) { |
| 17 | if id <= 0 || teamCodecError != nil { |
| 18 | return "", ErrRefused |
| 19 | } |
| 20 | s, e := teamCodec.Encode([]uint64{uint64(id)}) |
| 21 | if e != nil || len(s) == 0 || len(s) > 64 { |
| 22 | return "", ErrRefused |
| 23 | } |
| 24 | return s, nil |
| 25 | } |
| 26 | func validTeam(id int64, s string) bool { |
| 27 | expected, e := TeamSQID(id) |
| 28 | return e == nil && s == expected |
| 29 | } |
| 30 | func PersonRef(s string) bool { |
| 31 | if len(s) != 29 || !strings.HasPrefix(s, "pr_") { |
| 32 | return false |
| 33 | } |
| 34 | for _, c := range s[3:] { |
| 35 | if !(c >= 'a' && c <= 'z' || c >= '2' && c <= '7') { |
| 36 | return false |
| 37 | } |
| 38 | } |
| 39 | return true |
| 40 | } |
| 41 | |
| 42 | type OwnerReceipt struct { |
| 43 | Version int `json:"version"` |
| 44 | Purpose string `json:"purpose"` |
| 45 | OperationID string `json:"operation_id"` |
| 46 | Issuer string `json:"issuer"` |
| 47 | Directory string `json:"directory"` |
| 48 | TeamID int64 `json:"team_id"` |
| 49 | TeamSQID string `json:"team_sqid"` |
| 50 | PersonRef string `json:"person_ref"` |
| 51 | Role string `json:"role"` |
| 52 | Issued int64 `json:"issued_at"` |
| 53 | Expires int64 `json:"expires_at"` |
| 54 | } |
| 55 | |
| 56 | func (p OwnerReceipt) valid() bool { |
| 57 | return p.Version == 1 && p.Purpose == "owner-seed" && CanonicalToken(p.OperationID) && Origin(p.Issuer) && Origin(p.Directory) && validTeam(p.TeamID, p.TeamSQID) && PersonRef(p.PersonRef) && p.Role == "owner" && p.Issued > 0 && p.Expires > p.Issued && p.Expires-p.Issued <= 300 |
| 58 | } |
| 59 | |
| 60 | func SignOwner(k *family.Keypair, p OwnerReceipt) ([]byte, error) { |
| 61 | if !signingKey(k) || !p.valid() { |
| 62 | return nil, ErrRefused |
| 63 | } |
| 64 | payload, e := encode(p) |
| 65 | if e != nil { |
| 66 | return nil, e |
| 67 | } |
| 68 | sig, e := family.Sign(k, OwnerContext, payload) |
| 69 | if e != nil { |
| 70 | return nil, ErrUnavailable |
| 71 | } |
| 72 | out, e := encode(envelope{1, keyID(k.SignPub()), base64.RawURLEncoding.EncodeToString(payload), base64.RawURLEncoding.EncodeToString(sig)}) |
| 73 | if e != nil || len(out) > OwnerLimit { |
| 74 | return nil, ErrRefused |
| 75 | } |
| 76 | return out, nil |
| 77 | } |
| 78 | |
| 79 | type OwnerVerifier struct { |
| 80 | key []byte |
| 81 | kid, issuer, directory string |
| 82 | } |
| 83 | |
| 84 | func NewOwnerVerifier(publicKey, issuer, directory string) (*OwnerVerifier, error) { |
| 85 | k, e := public(publicKey) |
| 86 | if e != nil || !Origin(issuer) || !Origin(directory) || issuer == directory { |
| 87 | return nil, ErrRefused |
| 88 | } |
| 89 | return &OwnerVerifier{k, keyID(k), issuer, directory}, nil |
| 90 | } |
| 91 | func (v *OwnerVerifier) Verify(raw []byte, now time.Time) (OwnerReceipt, error) { |
| 92 | if v == nil || len(raw) == 0 || len(raw) > OwnerLimit { |
| 93 | return OwnerReceipt{}, ErrRefused |
| 94 | } |
| 95 | var env envelope |
| 96 | if strictCanonical(raw, &env) != nil || env.Version != 1 || env.KeyID != v.kid { |
| 97 | return OwnerReceipt{}, ErrRefused |
| 98 | } |
| 99 | p, ok := canonical(env.Payload, -1) |
| 100 | sig, ok2 := canonical(env.Signature, 64) |
| 101 | if !ok || !ok2 || len(p) > OwnerLimit || !family.Verify(v.key, OwnerContext, p, sig) { |
| 102 | return OwnerReceipt{}, ErrRefused |
| 103 | } |
| 104 | var receipt OwnerReceipt |
| 105 | if strictCanonical(p, &receipt) != nil || !receipt.valid() || receipt.Issuer != v.issuer || receipt.Directory != v.directory || now.Unix() <= 0 || receipt.Issued > now.Unix() || now.Unix() >= receipt.Expires { |
| 106 | return OwnerReceipt{}, ErrRefused |
| 107 | } |
| 108 | return receipt, nil |
| 109 | } |
| 110 | |