ALL

Solo para leer y verificar. No puedes copiar, usar, modificar ni redistribuir este código. Aviso completo en la página principal.

tunnel/auth.go

SHA-256: cc86de6463bd448aabdaf0c247685b1a359f7776c7ffab011df87d1eff46bb70
479 líneas · 17252 bytes · Ver en GitHub

1package main
2
3import (
4 "context"
5 "crypto/ed25519"
6 "crypto/sha256"
7 "fmt"
8 "strconv"
9 "strings"
10 "sync"
11 "time"
12)
13
14// Device authentication of protocol v2 (the contract, section 3): the node verifies the device's session JWT offline against the control
15// plane's keys, makes the device prove it holds the session's key (a DPoP proof over the NODE's URL and a nonce the node just made), and then asks
16// the control plane what the device is allowed: the host, the status and how long. Nothing a device sends names a host.
17
18// A token problem is one closed word. The words and their order are those of the contracts' verifiers (tests run every golden vector through
19// the functions below).
20const (
21 tokMalformed = "malformed_token"
22 tokBadAlg = "bad_alg"
23 tokUnknownKID = "unknown_kid"
24 tokBadSignature = "bad_signature"
25 tokWrongType = "wrong_type"
26 tokWrongIssuer = "wrong_issuer"
27 tokWrongAudience = "wrong_audience"
28 tokWrongKey = "wrong_key"
29 tokWrongSubject = "wrong_subject"
30 tokWrongHTM = "wrong_htm"
31 tokWrongHTU = "wrong_htu"
32 tokWrongATH = "wrong_ath"
33 tokWrongNonce = "wrong_nonce"
34 tokBadClaims = "bad_claims"
35 tokBadLifetime = "bad_lifetime"
36 tokNotYetValid = "not_yet_valid"
37 tokExpired = "expired"
38 tokStaleProof = "stale_proof"
39 tokReplayedJTI = "replayed_jti"
40)
41
42type tokenError struct{ word string }
43
44func (e *tokenError) Error() string { return e.word }
45
46func tokFail(word string) error { return &tokenError{word} }
47
48// tokenWord is the closed word of a token error ("" when err is nil).
49func tokenWord(err error) string {
50 if err == nil {
51 return ""
52 }
53 if te, ok := err.(*tokenError); ok {
54 return te.word
55 }
56 return tokMalformed
57}
58
59// errFor turns a token word into the ERR a refused HELLO, RENEW or PROOF is answered with, as the reference node of the contracts does: a clock that
60// is off is clock_skew (the device shows it), a malformed token is bad_hello, a token for another audience or an expired one is auth, and every other
61// word is the fallback (auth for a session, bad_proof for a proof) because nothing else may be told to a device that failed.
62func errFor(word, fallback string) string {
63 switch word {
64 case tokMalformed:
65 return codeBadHello
66 case tokNotYetValid, tokStaleProof:
67 return codeClockSkew
68 case tokWrongAudience, tokExpired:
69 return codeAuth
70 }
71 return fallback
72}
73
74type jws struct {
75 header, claims map[string]any
76 signature []byte
77 signingInput []byte
78}
79
80// splitJWS splits and decodes a compact JWS. Any structural fault is malformed_token: ASCII only, three dot-separated base64url segments with no
81// padding and canonical encoding, header and claims JSON objects without a repeated key and without a fraction, none of the headers that change
82// how a signature is read (crit, b64, jku, x5u, x5c). An empty signature part is allowed here (alg none then fails as bad_alg, the more useful word).
83func splitJWS(token string, maxBytes int) (*jws, error) {
84 if len(token) > maxBytes {
85 return nil, tokFail(tokMalformed)
86 }
87 for i := 0; i < len(token); i++ {
88 if token[i] > 0x7f {
89 return nil, tokFail(tokMalformed)
90 }
91 }
92 parts := strings.Split(token, ".")
93 if len(parts) != 3 {
94 return nil, tokFail(tokMalformed)
95 }
96 decode := func(text string) (map[string]any, bool) {
97 raw, err := b64Decode(text)
98 if err != nil {
99 return nil, false
100 }
101 value, err := parseJSON(string(raw))
102 if err != nil {
103 return nil, false
104 }
105 object, ok := value.(map[string]any)
106 return object, ok
107 }
108 header, ok1 := decode(parts[0])
109 claims, ok2 := decode(parts[1])
110 if !ok1 || !ok2 {
111 return nil, tokFail(tokMalformed)
112 }
113 var signature []byte
114 if parts[2] != "" {
115 raw, err := b64Decode(parts[2])
116 if err != nil {
117 return nil, tokFail(tokMalformed)
118 }
119 signature = raw
120 }
121 for _, name := range []string{"crit", "b64", "jku", "x5u", "x5c"} {
122 if _, present := header[name]; present {
123 return nil, tokFail(tokMalformed)
124 }
125 }
126 return &jws{header, claims, signature, []byte(parts[0] + "." + parts[1])}, nil
127}
128
129func verifyEd25519(key ed25519.PublicKey, signature, input []byte) bool {
130 return len(signature) == ed25519.SignatureSize && len(key) == ed25519.PublicKeySize && ed25519.Verify(key, input, signature)
131}
132
133func claimInt(claims map[string]any, key string) (int64, bool) {
134 f, ok := claims[key].(float64)
135 if !ok || f != float64(int64(f)) {
136 return 0, false
137 }
138 return int64(f), true
139}
140
141func claimString(claims map[string]any, key string) (string, bool) {
142 s, ok := claims[key].(string)
143 return s, ok
144}
145
146// epochClaim reads an epoch-seconds claim in the contracts' range.
147func epochClaim(claims map[string]any, key string) (int64, bool) {
148 n, ok := epochOK(claims[key])
149 return n, ok
150}
151
152// SessionClaims are the claims of a verified device session the node keeps for the life of a connection.
153type SessionClaims struct {
154 Subject string // the device id
155 JTI string
156 Generation int64
157 IssuedAt int64
158 ExpiresAt int64
159 KeyX string // the session's bound key (cnf.jwk.x): the key a proof must be signed with
160}
161
162func epochSeconds(t time.Time) float64 { return float64(t.UnixNano()) / 1e9 }
163
164// publicJWKOK is the contracts' public JWK: OKP, Ed25519, an x of 43 canonical characters, no private part.
165func publicJWKX(value any) (string, bool) {
166 jwk, ok := value.(map[string]any)
167 if !ok {
168 return "", false
169 }
170 x, hasX := jwk["x"].(string)
171 if _, private := jwk["d"]; private || jwk["kty"] != "OKP" || jwk["crv"] != "Ed25519" || !hasX {
172 return "", false
173 }
174 if _, ok := publicKeyX(x); !ok {
175 return "", false
176 }
177 return x, true
178}
179
180// verifySession verifies a device session offline: the signature before any claim is trusted, then the audience (the node accepts only
181// all-tunnel: the relay's and the control plane's audiences and a missing audience are refused), the lifetime (exactly the policy's session TTL) and the clock within skew.
182// The words, in the order the checks fail: malformed_token, bad_alg, unknown_kid, bad_signature, wrong_type, wrong_issuer, wrong_audience,
183// bad_claims, bad_lifetime, not_yet_valid, expired.
184func verifySession(token string, now time.Time, keys KeySource, skew time.Duration) (*SessionClaims, error) {
185 if len(token) > policyData.Protocol.JWTMaxBytes {
186 return nil, tokFail(tokMalformed)
187 }
188 j, err := splitJWS(token, policyData.Protocol.JWTMaxBytes)
189 if err != nil {
190 return nil, err
191 }
192 if j.header["alg"] != "EdDSA" {
193 return nil, tokFail(tokBadAlg)
194 }
195 kid, isString := j.header["kid"].(string)
196 if !isString || !reKID.MatchString(kid) {
197 return nil, tokFail(tokUnknownKID)
198 }
199 key, known := keys.Key(kid)
200 if !known {
201 return nil, tokFail(tokUnknownKID)
202 }
203 if !verifyEd25519(key, j.signature, j.signingInput) {
204 return nil, tokFail(tokBadSignature)
205 }
206 c := j.claims
207 if j.header["typ"] != "device-session" || c["typ"] != "device-session" {
208 return nil, tokFail(tokWrongType)
209 }
210 if ver, ok := claimInt(c, "ver"); !ok || ver != int64(policyData.Protocol.Version) {
211 return nil, tokFail(tokWrongType)
212 }
213 if c["iss"] != policyData.Identity.Issuer {
214 return nil, tokFail(tokWrongIssuer)
215 }
216 if c["aud"] != policyData.Identity.AudTunnel {
217 return nil, tokFail(tokWrongAudience)
218 }
219 sub, subOK := claimString(c, "sub")
220 jti, jtiOK := claimString(c, "jti")
221 iat, iatOK := epochClaim(c, "iat")
222 exp, expOK := epochClaim(c, "exp")
223 generation, genOK := claimInt(c, "generation")
224 x, cnfOK := "", false
225 if cnf, ok := c["cnf"].(map[string]any); ok && len(cnf) == 1 {
226 if jwk, ok := cnf["jwk"].(map[string]any); ok && len(jwk) == 3 {
227 x, cnfOK = publicJWKX(jwk)
228 }
229 }
230 if !subOK || !reDeviceID.MatchString(sub) || !jtiOK || !reJTI.MatchString(jti) || !iatOK || !expOK || !genOK || generation < 1 || generation > 1<<53-1 || !cnfOK {
231 return nil, tokFail(tokBadClaims)
232 }
233 if exp-iat != int64(policyData.Authorization.SessionTTLS) {
234 return nil, tokFail(tokBadLifetime)
235 }
236 t := epochSeconds(now)
237 if float64(iat) > t+skew.Seconds() {
238 return nil, tokFail(tokNotYetValid)
239 }
240 if float64(exp) <= t {
241 return nil, tokFail(tokExpired)
242 }
243 return &SessionClaims{Subject: sub, JTI: jti, Generation: generation, IssuedAt: iat, ExpiresAt: exp, KeyX: x}, nil
244}
245
246func sha256B64(data []byte) string {
247 sum := sha256.Sum256(data)
248 return b64Encode(sum[:])
249}
250
251// nodeProofURL is the htu a proof must carry: https://<node host>[:<port> when it is not 443]/all-tunnel/2.
252func nodeProofURL(host string, port int) string {
253 u := "https://" + host
254 if port != 443 {
255 u += ":" + strconv.Itoa(port)
256 }
257 return u + policyData.Protocol.ProofHTUPath
258}
259
260// proofClaims is what a verified proof leaves the node: the jti, to be spent once.
261type proofClaims struct {
262 JTI string
263 IssuedAt int64
264}
265
266// verifyProof verifies the DPoP proof of the session the connection said it holds: signed by the session's bound key, for this session (ath),
267// this node's URL (htu), this exchange (the nonce the node made) and this moment (iat within the policy's proof TTL). The jti is NOT spent here: the
268// caller spends it (replayCache.spend) so that a replay is refused even between two connections. Words, in order: malformed_token, bad_alg,
269// wrong_type, wrong_key, bad_signature, wrong_subject, wrong_audience, wrong_htm, wrong_htu, wrong_ath, wrong_nonce, bad_claims, not_yet_valid,
270// stale_proof.
271func verifyProof(proof string, now time.Time, sessionToken string, session *SessionClaims, nonce, htu string, skew time.Duration) (*proofClaims, error) {
272 if len(proof) > policyData.Protocol.JWTMaxBytes {
273 return nil, tokFail(tokMalformed)
274 }
275 j, err := splitJWS(proof, policyData.Protocol.JWTMaxBytes)
276 if err != nil {
277 return nil, err
278 }
279 if j.header["alg"] != "EdDSA" {
280 return nil, tokFail(tokBadAlg)
281 }
282 if j.header["typ"] != "dpop+jwt" {
283 return nil, tokFail(tokWrongType)
284 }
285 x, ok := publicJWKX(j.header["jwk"])
286 if !ok {
287 return nil, tokFail(tokMalformed)
288 }
289 if x != session.KeyX {
290 return nil, tokFail(tokWrongKey)
291 }
292 key, _ := publicKeyX(x)
293 if !verifyEd25519(key, j.signature, j.signingInput) {
294 return nil, tokFail(tokBadSignature)
295 }
296 c := j.claims
297 switch {
298 case c["sub"] != session.Subject:
299 return nil, tokFail(tokWrongSubject)
300 case c["aud"] != policyData.Identity.AudTunnel:
301 return nil, tokFail(tokWrongAudience)
302 case c["htm"] != policyData.Protocol.ProofHTM:
303 return nil, tokFail(tokWrongHTM)
304 case c["htu"] != htu:
305 return nil, tokFail(tokWrongHTU)
306 case c["ath"] != sha256B64([]byte(sessionToken)):
307 return nil, tokFail(tokWrongATH)
308 case c["nonce"] != nonce:
309 return nil, tokFail(tokWrongNonce)
310 }
311 jti, jtiOK := claimString(c, "jti")
312 iat, iatOK := epochClaim(c, "iat")
313 if !jtiOK || !reJTI.MatchString(jti) || !iatOK {
314 return nil, tokFail(tokBadClaims)
315 }
316 t := epochSeconds(now)
317 if float64(iat) > t+skew.Seconds() {
318 return nil, tokFail(tokNotYetValid)
319 }
320 if float64(iat+int64(policyData.Authorization.ProofTTLS)) <= t {
321 return nil, tokFail(tokStaleProof)
322 }
323 return &proofClaims{JTI: jti, IssuedAt: iat}, nil
324}
325
326// replayCache remembers the proofs the node has accepted until they could not verify any more (a proof is single use for the policy's proof TTL,
327// plus the skew a fast clock is allowed). A session's jti is NOT single use: a cached session reconnects. The cache is bounded: when it is full of
328// live entries a new proof is refused (the connection gets bad_proof), never admitted unchecked.
329type replayCache struct {
330 mu sync.Mutex
331 seen map[string]time.Time // jti -> when it can be forgotten
332 limit int
333}
334
335func newReplayCache(limit int) *replayCache {
336 return &replayCache{seen: map[string]time.Time{}, limit: limit}
337}
338
339// spend records a proof jti issued at iat. It reports false when the jti was already spent (or the cache is full of live entries).
340func (r *replayCache) spend(jti string, iat int64, now time.Time, skew time.Duration) bool {
341 forget := time.Unix(iat, 0).Add(secs(policyData.Authorization.ProofTTLS) + skew)
342 r.mu.Lock()
343 defer r.mu.Unlock()
344 if until, dup := r.seen[jti]; dup && now.Before(until) {
345 return false
346 }
347 if len(r.seen) >= r.limit {
348 for id, until := range r.seen {
349 if !now.Before(until) {
350 delete(r.seen, id)
351 }
352 }
353 if len(r.seen) >= r.limit {
354 return false
355 }
356 }
357 r.seen[jti] = forget
358 return true
359}
360
361func (r *replayCache) size() int {
362 r.mu.Lock()
363 defer r.mu.Unlock()
364 return len(r.seen)
365}
366
367// ControlPlane is the node's side of POST /v1/tunnel/authorize: what the control plane decides for a device whose session the node verified. The
368// request never carries a host name; the answer's host, status and valid_until are what the node relays. The real client (node assertion, retry,
369// concurrency cap, grace) is the control-plane piece; the node core is written against this interface and tested with a fake.
370type ControlPlane interface {
371 Authorize(ctx context.Context, req AuthorizeRequest) (AuthorizeResponse, error)
372}
373
374// AuthorizeRequest is authorize-request.schema.json.
375type AuthorizeRequest struct {
376 DeviceID string
377 SessionJTI string
378 Reason string // "hello" or "renew"
379 Node string
380}
381
382func (r AuthorizeRequest) toMap() map[string]any {
383 return map[string]any{"v": 1, "device_id": r.DeviceID, "session_jti": r.SessionJTI, "reason": r.Reason, "node": r.Node}
384}
385
386// AuthorizeResponse is the 200 answer of authorize (authorize-response.schema.json): the node uses Host, Status, ValidUntil, ServerTime and the
387// Policy windows; the rest travels to the pieces that need it.
388type AuthorizeResponse struct {
389 DeviceID string
390 Label string
391 Host string
392 Status string // active, gated or paused
393 Plan string // paid or trial
394 ValidUntil int64 // epoch seconds
395 ServerTime int64
396 Policy okPolicy
397 Access string // the signed access statement, when the answer has one
398 Nodes any // the signed list of nodes, opaque to this piece
399 E2EWave int
400 E2EKill bool
401 CopyE2EE bool
402}
403
404func (a AuthorizeResponse) toMap() map[string]any {
405 m := map[string]any{"ok": true, "v": 1, "device_id": a.DeviceID, "label": a.Label, "host": a.Host, "status": a.Status, "plan": a.Plan,
406 "valid_until": a.ValidUntil, "server_time": a.ServerTime, "policy": a.Policy.toMap(), "e2e_wave": a.E2EWave, "e2e_kill": a.E2EKill,
407 "copy_e2ee": a.CopyE2EE}
408 if a.Nodes != nil {
409 m["nodes"] = a.Nodes
410 }
411 if a.Access != "" {
412 m["access"] = a.Access
413 }
414 return m
415}
416
417// ControlError is a refusal of authorize: the HTTP status and the closed word of the error body (license_inactive, device_revoked, ...). A transport
418// failure or a timeout is any other error: the node answers control_unavailable.
419type ControlError struct {
420 Status int
421 Code string
422}
423
424func (e *ControlError) Error() string { return fmt.Sprintf("authorize: %d %s", e.Status, e.Code) }
425
426// errForControl is the ERR word of a failed authorize.
427func errForControl(err error) string {
428 ce, ok := err.(*ControlError)
429 if !ok {
430 return codeControlUnavailable
431 }
432 if ce.Status >= 500 || ce.Status == 429 || ce.Status < 400 {
433 return codeControlUnavailable
434 }
435 switch ce.Code {
436 case codeLicenseInactive, codeDeviceRevoked, codePaused:
437 return ce.Code
438 case "unauthorized", "invalid_authorize_request", "request_too_large", "bad_response":
439 // The control plane refused the NODE (its assertion, its request), not the device: nothing the device can fix, so "try later", never "auth".
440 return codeControlUnavailable
441 }
442 return codeAuth
443}
444
445// checkAuthorize refuses an answer that is not what was asked: another device's, a host that is not exactly one label under a zone or does not start
446// with the answer's own label, an unknown status or plan, windows out of range. The node never relays an answer it cannot vouch for.
447func checkAuthorize(a AuthorizeResponse, req AuthorizeRequest) error {
448 switch {
449 case a.DeviceID != req.DeviceID:
450 return fmt.Errorf("authorize answer is for another device")
451 case !reLabel.MatchString(a.Label), !validFQDN(a.Host), !strings.HasPrefix(a.Host, a.Label+"."):
452 return fmt.Errorf("authorize answer has a bad host")
453 case !validStatus(a.Status):
454 return fmt.Errorf("authorize answer has a bad status")
455 case a.Plan != "paid" && a.Plan != "trial":
456 return fmt.Errorf("authorize answer has a bad plan")
457 case a.ValidUntil < 1 || a.ValidUntil > epochMax || a.ServerTime < 1 || a.ServerTime > epochMax:
458 return fmt.Errorf("authorize answer has a bad time")
459 }
460 for _, v := range []int64{a.Policy.PingIntervalS, a.Policy.DeadPeerS, a.Policy.StreamIdleS, a.Policy.StreamMaxAgeS, a.Policy.RenewBeforeValidUntilS} {
461 if v < 1 || v > 86400 {
462 return fmt.Errorf("authorize answer has a bad policy window")
463 }
464 }
465 return nil
466}
467
468// boundWindows bounds the windows of the answer to policy.json: the node never offers a device a window longer than the table's own. (The control
469// plane builds them from the same table, so this changes nothing for an honest answer.)
470func boundWindows(p okPolicy) okPolicy {
471 t := policyData.Timing
472 return okPolicy{
473 PingIntervalS: min(p.PingIntervalS, int64(t.PingIntervalS)),
474 DeadPeerS: min(p.DeadPeerS, int64(t.DeadPeerS)),
475 StreamIdleS: min(p.StreamIdleS, int64(t.StreamIdleS)),
476 StreamMaxAgeS: min(p.StreamMaxAgeS, int64(t.StreamMaxAgeS)),
477 RenewBeforeValidUntilS: min(p.RenewBeforeValidUntilS, int64(t.RenewBeforeValidUntilS)),
478 }
479}