Skip to content
File

Blob: tun/server/delegation_rpc.go

go214 lines
1package server
2 
3import (
4 "bytes"
5 "context"
6 "crypto/rand"
7 "errors"
8 "strings"
9 "time"
10 
11 "go.miragespace.co/specter/spec/acme"
12 "go.miragespace.co/specter/spec/protocol"
13 "go.miragespace.co/specter/spec/tun"
14 "go.miragespace.co/specter/util/promise"
15 
16 "github.com/twitchtv/twirp"
17)
18 
19var (
20 errDelegationNotFound = errors.New("unknown or revoked grant")
21 errDelegationInvalid = errors.New("invalid grant record")
22 errDelegationNotOwned = errors.New("hostname is not registered to the grant owner")
23)
24 
25func (s *Server) readDelegation(ctx context.Context, id string) (*protocol.DelegationRecord, error) {
26 if !tun.IsDelegationID(id) {
27 return nil, errDelegationInvalid
28 }
29 data, err := s.Chord.Get(ctx, []byte(tun.DelegationKey(id)))
30 if err != nil {
31 return nil, err
32 }
33 if len(data) == 0 {
34 return nil, errDelegationNotFound
35 }
36 rec := &protocol.DelegationRecord{}
37 if rec.UnmarshalVT(data) != nil || rec.GetVersion() != tun.DelegationRecordVersion || rec.GetId() != id || len(rec.GetOwner().GetToken()) == 0 || rec.GetHostname() == "" || rec.GetExpiresAt() < 0 {
38 return nil, errDelegationInvalid
39 }
40 return rec, nil
41}
42 
43func (s *Server) verifyDelegatedAuthority(ctx context.Context, rec *protocol.DelegationRecord) error {
44 owned, err := s.Chord.PrefixContains(ctx, []byte(tun.ClientHostnamesPrefix(rec.GetOwner())), []byte(rec.GetHostname()))
45 if err != nil {
46 return err
47 }
48 if !owned {
49 return errDelegationNotOwned
50 }
51 if strings.Contains(rec.GetHostname(), ".") {
52 custom, err := tun.FindCustomHostname(ctx, s.Chord, rec.GetHostname())
53 if errors.Is(err, tun.ErrHostnameNotFound) {
54 return errDelegationNotOwned
55 }
56 if err != nil {
57 return err
58 }
59 if !bytes.Equal(custom.GetClientToken().GetToken(), rec.GetOwner().GetToken()) {
60 return errDelegationNotOwned
61 }
62 }
63 return nil
64}
65 
66func delegationAuthorityError(err error) error {
67 if errors.Is(err, errDelegationNotOwned) {
68 return twirp.PermissionDenied.Error(err.Error())
69 }
70 return twirp.Unavailable.Error(err.Error())
71}
72 
73func normalizeDelegationHostname(hostname string) (string, error) {
74 if strings.Contains(hostname, ".") {
75 return acme.Normalize(hostname)
76 }
77 // Short labels must match a server-generated owner registration exactly.
78 return hostname, nil
79}
80 
81func grantFromRecord(rec *protocol.DelegationRecord) *protocol.DelegationGrant {
82 return &protocol.DelegationGrant{
83 Id: rec.GetId(),
84 Hostname: rec.GetHostname(),
85 ExpiresAt: rec.GetExpiresAt(),
86 }
87}
88 
89func (s *Server) MintDelegation(ctx context.Context, req *protocol.MintDelegationRequest) (*protocol.MintDelegationResponse, error) {
90 owner, _, err := extractAuthenticated(ctx)
91 if err != nil {
92 return nil, err
93 }
94 hostname, err := normalizeDelegationHostname(req.GetHostname())
95 if err != nil {
96 return nil, twirp.InvalidArgument.Error(err.Error())
97 }
98 if req.GetExpiresAt() != 0 && req.GetExpiresAt() <= time.Now().Unix() {
99 return nil, twirp.InvalidArgument.Error("expires_at must be in the future")
100 }
101 rec := &protocol.DelegationRecord{
102 Version: tun.DelegationRecordVersion,
103 Owner: owner,
104 Hostname: hostname,
105 ExpiresAt: req.GetExpiresAt(),
106 }
107 if err := s.verifyDelegatedAuthority(ctx, rec); err != nil {
108 return nil, delegationAuthorityError(err)
109 }
110 index := []byte(tun.ClientDelegationsPrefix(owner))
111 ids, err := s.Chord.PrefixList(ctx, index)
112 if err != nil {
113 return nil, twirp.Unavailable.Error(err.Error())
114 }
115 if len(ids) >= tun.MaxDelegationsPerOwner {
116 return nil, twirp.ResourceExhausted.Error("delegation limit reached; revoke unused or incomplete grants")
117 }
118 var secret [32]byte
119 if _, err := rand.Read(secret[:]); err != nil {
120 return nil, twirp.InternalErrorWith(err)
121 }
122 rec.Id = tun.DelegationID(secret)
123 data, err := rec.MarshalVT()
124 if err != nil {
125 return nil, twirp.InternalErrorWith(err)
126 }
127 if err := s.Chord.PrefixAppend(ctx, index, []byte(rec.Id)); err != nil {
128 return nil, twirp.Unavailable.Error(err.Error())
129 }
130 if err := s.Chord.Put(ctx, []byte(tun.DelegationKey(rec.Id)), data); err != nil {
131 return nil, twirp.Unavailable.Errorf("grant %s may be incomplete: %v", rec.Id, err)
132 }
133 return &protocol.MintDelegationResponse{
134 Grant: grantFromRecord(rec),
135 Token: tun.EncodeDelegationToken(secret),
136 }, nil
137}
138 
139func (s *Server) ListDelegations(ctx context.Context, _ *protocol.ListDelegationsRequest) (*protocol.ListDelegationsResponse, error) {
140 owner, _, err := extractAuthenticated(ctx)
141 if err != nil {
142 return nil, err
143 }
144 ids, err := s.Chord.PrefixList(ctx, []byte(tun.ClientDelegationsPrefix(owner)))
145 if err != nil {
146 return nil, twirp.Unavailable.Error(err.Error())
147 }
148 if len(ids) > tun.MaxDelegationsPerOwner {
149 ids = ids[:tun.MaxDelegationsPerOwner]
150 }
151 resp := &protocol.ListDelegationsResponse{}
152 for offset := 0; offset < len(ids); offset += 8 {
153 batch := ids[offset:min(offset+8, len(ids))]
154 jobs := make([]func(context.Context) (*protocol.DelegationGrant, error), len(batch))
155 for i, raw := range batch {
156 id := string(raw)
157 jobs[i] = func(ctx context.Context) (*protocol.DelegationGrant, error) {
158 rec, err := s.readDelegation(ctx, id)
159 if errors.Is(err, errDelegationNotFound) || errors.Is(err, errDelegationInvalid) || (err == nil && !bytes.Equal(rec.GetOwner().GetToken(), owner.GetToken())) {
160 return &protocol.DelegationGrant{
161 Id: id,
162 Incomplete: true,
163 }, nil
164 }
165 if err != nil {
166 return nil, err
167 }
168 return grantFromRecord(rec), nil
169 }
170 }
171 grants, errs := promise.All(ctx, jobs...)
172 for _, err := range errs {
173 if err != nil {
174 return nil, twirp.Unavailable.Error(err.Error())
175 }
176 }
177 resp.Grants = append(resp.Grants, grants...)
178 }
179 return resp, nil
180}
181 
182func (s *Server) RevokeDelegation(ctx context.Context, req *protocol.RevokeDelegationRequest) (*protocol.RevokeDelegationResponse, error) {
183 owner, _, err := extractAuthenticated(ctx)
184 if err != nil {
185 return nil, err
186 }
187 id := req.GetId()
188 if !tun.IsDelegationID(id) {
189 return nil, twirp.InvalidArgument.Error("invalid grant ID")
190 }
191 rec, err := s.readDelegation(ctx, id)
192 if err != nil && !errors.Is(err, errDelegationNotFound) && !errors.Is(err, errDelegationInvalid) {
193 return nil, twirp.Unavailable.Error(err.Error())
194 }
195 index := []byte(tun.ClientDelegationsPrefix(owner))
196 if rec == nil || !bytes.Equal(rec.GetOwner().GetToken(), owner.GetToken()) {
197 indexed, err := s.Chord.PrefixContains(ctx, index, []byte(id))
198 if err != nil {
199 return nil, twirp.Unavailable.Error(err.Error())
200 }
201 if !indexed {
202 return nil, twirp.NotFound.Error("grant is not owned by this client")
203 }
204 }
205 if err := s.Chord.Delete(ctx, []byte(tun.DelegationKey(id))); err != nil {
206 return nil, twirp.Unavailable.Error(err.Error())
207 }
208 resp := &protocol.RevokeDelegationResponse{Revoked: true}
209 if err := s.Chord.PrefixRemove(ctx, index, []byte(id)); err != nil {
210 resp.IndexError = err.Error()
211 }
212 return resp, nil
213}