ALL

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

tunnel/protocol_test.go

SHA-256: 59a7b338bacbc5cb415cb786c48d5ac1c1374c6670b0b92806002865d064efef
343 líneas · 13842 bytes · Ver en GitHub

1package main
2
3// ALL 2.0, WP05: the node's own protocol code (policy.go, proto.go, auth.go, jwks.go) against the contracts' golden vectors. The Go reference verifier of
4// WP01 (contracts_verify_test.go) and the node are two independent implementations; each vector must get the same verdict and the same error word from
5// both. Vectors "nobody has seen" run the same way: ALL_CONTRACT_VECTORS_DIR points ctVectors at a set made with another salt.
6
7import (
8 "encoding/json"
9 "os"
10 "path/filepath"
11 "reflect"
12 "strings"
13 "testing"
14 "time"
15)
16
17// tunnel/policy.json is the node's PROJECTION of contracts/policy.json (the contract lint table `copies`: the contract generator's sync writes it): only the sections the node
18// reads, each one as it is in the table. So the test compares section by section, and the sections the node's code reads must all be there.
19func TestPolicyCopyIsTheNodesProjectionOfTheContracts(t *testing.T) {
20 want, err := os.ReadFile(filepath.Join(ctContracts, "policy.json"))
21 if err != nil {
22 t.Fatal(err)
23 }
24 var full, mine map[string]any
25 if err := json.Unmarshal(want, &full); err != nil {
26 t.Fatal(err)
27 }
28 if err := json.Unmarshal(policyJSON, &mine); err != nil {
29 t.Fatal(err)
30 }
31 for _, section := range []string{"protocol", "timing", "node_limits", "authorization", "gated", "identity", "jwks", "enums"} {
32 if _, ok := mine[section]; !ok {
33 t.Errorf("tunnel/policy.json has no %q section: the node reads it (the contract lint table `copies`, then the contract generator's sync)", section)
34 }
35 }
36 for name, section := range mine {
37 if name == "$comment" || name == "provisional" {
38 continue
39 }
40 whole, ok := full[name]
41 if !ok {
42 t.Errorf("section %q of tunnel/policy.json is not in contracts/policy.json", name)
43 continue
44 }
45 mineMap, mineIsMap := section.(map[string]any)
46 wholeMap, _ := whole.(map[string]any)
47 if !mineIsMap {
48 if !reflect.DeepEqual(section, whole) {
49 t.Errorf("%s of tunnel/policy.json differs from contracts/policy.json: run the contract generator's sync", name)
50 }
51 continue
52 }
53 for key, value := range mineMap {
54 if !reflect.DeepEqual(value, wholeMap[key]) {
55 t.Errorf("%s.%s of tunnel/policy.json differs from contracts/policy.json: run the contract generator's sync", name, key)
56 }
57 }
58 }
59}
60
61func TestPolicyDefaultsOfTheNodeAreThePolicysNumbers(t *testing.T) {
62 cfg := DefaultConfig()
63 for name, got := range map[string]time.Duration{
64 "HelloTimeout": cfg.HelloTimeout, "AttachTimeout": cfg.AttachTimeout, "FirstLineTimeout": cfg.FirstLineTimeout, "DeadPeer": cfg.DeadPeer,
65 "StreamIdle": cfg.StreamIdle, "StreamMaxAge": cfg.StreamMaxAge, "HalfCloseTimeout": cfg.HalfCloseTimeout, "ClockSkew": cfg.ClockSkew, "MinRenewGap": cfg.MinRenewGap,
66 } {
67 want := map[string]string{
68 "HelloTimeout": "node_limits.client_hello_deadline_s", "AttachTimeout": "protocol.attach_timeout_s", "FirstLineTimeout": "protocol.first_line_timeout_s",
69 "DeadPeer": "timing.dead_peer_s", "StreamIdle": "timing.stream_idle_s", "StreamMaxAge": "timing.stream_max_age_s",
70 "HalfCloseTimeout": "timing.half_close_s", "ClockSkew": "authorization.node_skew_s", "MinRenewGap": "timing.backoff_min_s",
71 }[name]
72 if got != time.Duration(ctPolInt(want))*time.Second {
73 t.Errorf("%s = %s, policy %s = %d", name, got, want, ctPolInt(want))
74 }
75 }
76 if cfg.MaxStreams != ctPolInt("node_limits.global_streams_max") || cfg.MaxStreamsPerHost != ctPolInt("node_limits.label_streams_max") ||
77 cfg.MaxUnauth != ctPolInt("node_limits.max_unauthenticated_control") || cfg.AuthorizeMax != ctPolInt("authorization.authorize_max_concurrent") {
78 t.Errorf("limits %d %d %d %d", cfg.MaxStreams, cfg.MaxStreamsPerHost, cfg.MaxUnauth, cfg.AuthorizeMax)
79 }
80}
81
82func TestWireVectorsGetTheSameVerdictFromTheNode(t *testing.T) {
83 for _, v := range ctList(t, ctVectors(t, "lines.json"), "lines") {
84 line, direction := v["line"].(string), v["dir"].(string)
85 parsed, err := parseLine(line, direction)
86 if v["ok"] == true {
87 if err != nil {
88 t.Errorf("%v: %v", v["id"], err)
89 continue
90 }
91 if !ctEqual(map[string]any(parsed), v["parsed"]) {
92 t.Errorf("%v: parsed %v, want %v", v["id"], parsed, v["parsed"])
93 }
94 if v["canonical"] == true && formatLine(parsed) != line {
95 t.Errorf("%v: canonical form %q, want %q", v["id"], formatLine(parsed), line)
96 }
97 continue
98 }
99 if err == nil {
100 t.Errorf("%v: accepted %q", v["id"], truncate(line))
101 continue
102 }
103 if code := errCode(err); code != v["error"] {
104 t.Errorf("%v: error %q, want %q", v["id"], code, v["error"])
105 }
106 for _, suffix := range []string{"\n", "\x00", "\r"} {
107 if _, err := parseLine(line+suffix, direction); err == nil {
108 t.Errorf("%v: accepted with suffix %q", v["id"], suffix)
109 }
110 }
111 }
112}
113
114func TestAddressVectorsGetTheSameVerdictFromTheNode(t *testing.T) {
115 for _, v := range ctList(t, ctVectors(t, "lines.json"), "ips") {
116 if got := validIP(v["value"].(string)); got != (v["ok"] == true) {
117 t.Errorf("%v: %q gave %v", v["id"], v["value"], got)
118 }
119 }
120}
121
122func TestLabelAndHostVectorsGetTheSameVerdictFromTheNode(t *testing.T) {
123 doc := ctVectors(t, "labels.json")
124 for _, v := range ctList(t, doc, "labels") {
125 if got := reLabel.MatchString(v["value"].(string)); got != (v["ok"] == true) {
126 t.Errorf("%v: label %q gave %v", v["id"], v["value"], got)
127 }
128 }
129 for _, v := range ctList(t, doc, "fqdns") {
130 if got := validFQDN(v["value"].(string)); got != (v["ok"] == true) && v["zones"] == nil {
131 t.Errorf("%v: host %q gave %v", v["id"], v["value"], got)
132 }
133 }
134}
135
136// nodeKeys turns the verifier's kid -> x map into the node's key set.
137func nodeKeys(t *testing.T, doc any) StaticKeys {
138 t.Helper()
139 keys := StaticKeys{}
140 m, code := ctParseJWKS(doc)
141 if code != "" {
142 t.Fatalf("jwks: %s", code)
143 }
144 for kid, x := range m {
145 key, ok := publicKeyX(x)
146 if !ok {
147 t.Fatalf("key %s", kid)
148 }
149 keys[kid] = key
150 }
151 return keys
152}
153
154func verdictOf(err error) map[string]any {
155 if err != nil {
156 return map[string]any{"ok": false, "error": tokenWord(err)}
157 }
158 return map[string]any{"ok": true}
159}
160
161func atSeconds(s float64) time.Time { return time.Unix(0, int64(s*1e9)) }
162
163func TestSessionVectorsGetTheSameVerdictFromTheNode(t *testing.T) {
164 for _, v := range ctList(t, ctVectors(t, "tokens.json"), "sessions") {
165 ctx := v["ctx"].(map[string]any)
166 token, isString := v["token"].(string)
167 var err error
168 if !isString {
169 err = tokFail(tokMalformed)
170 } else {
171 _, err = verifySession(token, atSeconds(ctx["now"].(float64)), nodeKeys(t, ctx["jwks"]), time.Duration(ctx["skew"].(float64))*time.Second)
172 }
173 ctCheckVerdict(t, v, verdictOf(err))
174 }
175}
176
177func TestProofVectorsGetTheSameVerdictFromTheNode(t *testing.T) {
178 for _, v := range ctList(t, ctVectors(t, "tokens.json"), "proofs") {
179 ctx := v["ctx"].(map[string]any)
180 now, skew := atSeconds(ctx["now"].(float64)), time.Duration(ctx["skew"].(float64))*time.Second
181 sessionToken := ctx["session"].(string)
182 session, err := verifySession(sessionToken, now, nodeKeys(t, ctx["jwks"]), skew)
183 if err != nil {
184 t.Fatalf("%v: session %v", v["id"], err)
185 }
186 pc, err := verifyProof(v["proof"].(string), now, sessionToken, session, ctx["nonce"].(string), ctx["htu"].(string), skew)
187 if err == nil {
188 // the replay check belongs to the caller: spend against the vector's set of used jtis
189 cache := newReplayCache(16)
190 for _, id := range ctStrings(ctx["seen"]) {
191 cache.spend(id, pc.IssuedAt, now, skew)
192 }
193 if !cache.spend(pc.JTI, pc.IssuedAt, now, skew) {
194 err = tokFail(tokReplayedJTI)
195 }
196 }
197 ctCheckVerdict(t, v, verdictOf(err))
198 }
199}
200
201func TestKeySetVectorsGetTheSameVerdictFromTheNode(t *testing.T) {
202 for _, v := range ctList(t, ctVectors(t, "tokens.json"), "key_sets") {
203 if v["kind"] != "jwks" {
204 continue
205 }
206 _, err := ParseJWKS([]byte(canonicalJSON(v["doc"])))
207 got := map[string]any{"ok": true}
208 if err != nil {
209 got = map[string]any{"ok": false, "error": err.Error()}
210 }
211 ctCheckVerdict(t, v, got)
212 }
213}
214
215func TestOKBodyBoundsTheWindowsToThePolicy(t *testing.T) {
216 big := okPolicy{PingIntervalS: 86400, DeadPeerS: 86400, StreamIdleS: 86400, StreamMaxAgeS: 86400, RenewBeforeValidUntilS: 86400}
217 got := boundWindows(big)
218 want := okPolicy{int64(ctPolInt("timing.ping_interval_s")), int64(ctPolInt("timing.dead_peer_s")), int64(ctPolInt("timing.stream_idle_s")),
219 int64(ctPolInt("timing.stream_max_age_s")), int64(ctPolInt("timing.renew_before_valid_until_s"))}
220 if got != want {
221 t.Fatalf("bounded %+v, want %+v", got, want)
222 }
223 small := okPolicy{PingIntervalS: 5, DeadPeerS: 9, StreamIdleS: 30, StreamMaxAgeS: 60, RenewBeforeValidUntilS: 10}
224 if boundWindows(small) != small {
225 t.Fatalf("a window below the policy's must stay as it is: %+v", boundWindows(small))
226 }
227 line := okBody{Node: "t1", Host: strings.Repeat("a", 20) + "." + testZone, Status: statusActive, ValidUntil: 1798766600, ServerTime: 1798761600, Policy: got}.line()
228 if m, err := parseLine(line, dirN2D); err != nil || formatLine(m) != line {
229 t.Fatalf("the OK line is not canonical: %q %v", line, err)
230 }
231}
232
233func TestAuthorizeAnswerIsCheckedBeforeItIsRelayed(t *testing.T) {
234 req := AuthorizeRequest{DeviceID: randomHex(16), SessionJTI: randomHex(12), Reason: "hello", Node: "t1"}
235 label := label20()
236 good := AuthorizeResponse{DeviceID: req.DeviceID, Label: label, Host: label + "." + testZone, Status: statusActive, Plan: "paid", ValidUntil: 1798766600,
237 ServerTime: 1798761600, Policy: okPolicy{20, 45, 1800, 14400, 300}}
238 if err := checkAuthorize(good, req); err != nil {
239 t.Fatal(err)
240 }
241 for name, mutate := range map[string]func(a *AuthorizeResponse){
242 "another device": func(a *AuthorizeResponse) { a.DeviceID = randomHex(16) },
243 "host without label": func(a *AuthorizeResponse) { a.Host = "evil." + testZone },
244 "host of another": func(a *AuthorizeResponse) { a.Host = label20() + "." + testZone },
245 "two labels deep": func(a *AuthorizeResponse) { a.Host = "x." + a.Host },
246 "unknown status": func(a *AuthorizeResponse) { a.Status = "free" },
247 "unknown plan": func(a *AuthorizeResponse) { a.Plan = "lifetime" },
248 "no valid_until": func(a *AuthorizeResponse) { a.ValidUntil = 0 },
249 "window of a week": func(a *AuthorizeResponse) { a.Policy.DeadPeerS = 7 * 86400 },
250 "zero window": func(a *AuthorizeResponse) { a.Policy.PingIntervalS = 0 },
251 "upper case host": func(a *AuthorizeResponse) { a.Host = strings.ToUpper(a.Host) },
252 "trailing dot host": func(a *AuthorizeResponse) { a.Host += "." },
253 "label not base32": func(a *AuthorizeResponse) { a.Label = strings.Repeat("0", 20); a.Host = a.Label + "." + testZone },
254 } {
255 a := good
256 mutate(&a)
257 if checkAuthorize(a, req) == nil {
258 t.Errorf("%s: accepted", name)
259 }
260 }
261}
262
263func TestReplayCacheRefusesAReusedJTIForTheProofsLifeAndIsBounded(t *testing.T) {
264 now := time.Unix(1798766600, 0)
265 skew := 5 * time.Second
266 c := newReplayCache(3)
267 if !c.spend("jti-aaaaaaaaaaaaaaaa", now.Unix(), now, skew) || c.spend("jti-aaaaaaaaaaaaaaaa", now.Unix(), now, skew) {
268 t.Fatal("a jti is single use")
269 }
270 // after the proof can no longer verify (its TTL plus the skew) the jti is forgotten and the cache makes room
271 later := now.Add(secs(ctPolInt("authorization.proof_ttl_s")) + skew + time.Second)
272 if !c.spend("jti-aaaaaaaaaaaaaaaa", later.Unix(), later, skew) {
273 t.Fatal("a forgotten jti is spendable again")
274 }
275 // full of live entries: a new proof is refused, never admitted unchecked
276 c = newReplayCache(2)
277 for _, id := range []string{"jti-1aaaaaaaaaaaaaaa", "jti-2aaaaaaaaaaaaaaa"} {
278 if !c.spend(id, now.Unix(), now, skew) {
279 t.Fatal(id)
280 }
281 }
282 if c.spend("jti-3aaaaaaaaaaaaaaa", now.Unix(), now, skew) {
283 t.Fatal("a full cache admitted a proof")
284 }
285 if !c.spend("jti-3aaaaaaaaaaaaaaa", later.Unix(), later, skew) {
286 t.Fatal("expired entries must free the room")
287 }
288 if c.size() != 1 {
289 t.Fatalf("size %d", c.size())
290 }
291}
292
293func FuzzParseLine(f *testing.F) {
294 for _, v := range []string{"HELLO 2 a.b.c", "PROOF a.b.c", "PING", "PONG", "HINT 192.0.2.4 ::1", "CLOSE", "CLOSE expired", "ERR auth", "CHALLENGE AAAAAAAAAAAAAAAAAAAAAA",
295 "ATTACH 00000000000000000000000000000000 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", "DRAIN 20", "OK {}", "OPEN x y z w"} {
296 f.Add(v, true)
297 f.Add(v, false)
298 }
299 f.Fuzz(func(t *testing.T, line string, toNode bool) {
300 dir := dirN2D
301 if toNode {
302 dir = dirD2N
303 }
304 m, err := parseLine(line, dir)
305 if err != nil {
306 if code := errCode(err); !inList(policyData.Protocol.ErrorCodes, code) {
307 t.Fatalf("%q: error word %q is not a protocol code", line, code)
308 }
309 return
310 }
311 // what the node accepts, it can write again unchanged in its canonical form, and that form parses to the same message
312 if canonical := formatLine(m); canonical != "" {
313 again, err := parseLine(canonical, dir)
314 if err != nil || !ctEqual(map[string]any(again), map[string]any(m)) {
315 t.Fatalf("%q: canonical %q reparses to %v (%v)", line, canonical, again, err)
316 }
317 }
318 })
319}
320
321func FuzzTokens(f *testing.F) {
322 doc, err := ctLoadFile(filepath.Join(ctVectorsDir(), "tokens.json"))
323 if err != nil {
324 f.Fatal(err)
325 }
326 for _, item := range doc.(map[string]any)["sessions"].([]any) {
327 if s, ok := item.(map[string]any)["token"].(string); ok {
328 f.Add(s)
329 }
330 }
331 for _, item := range doc.(map[string]any)["proofs"].([]any) {
332 f.Add(item.(map[string]any)["proof"].(string))
333 }
334 keys := StaticKeys{}
335 f.Fuzz(func(t *testing.T, token string) {
336 // no token, however broken, may panic the verifiers; a token no key signed never verifies
337 if _, err := verifySession(token, time.Unix(1798766600, 0), keys, 5*time.Second); err == nil {
338 t.Fatalf("a session verified against an empty key set: %q", token)
339 }
340 _, _ = splitJWS(token, 1<<16)
341 _, _ = verifyProof(token, time.Unix(1798766600, 0), "a.b.c", &SessionClaims{KeyX: strings.Repeat("A", 43)}, "n", "h", 5*time.Second)
342 })
343}