package server import ( "strings" "testing" "time" "jarl/internal/dnssec" "jarl/internal/model" "jarl/internal/nscheck" ) func TestDNSSECEndpoints(t *testing.T) { e := newAPIEnv(t) // Nothing answers DNS here, so the parent can't be checked. e.srv.nsChecker = &nscheck.Checker{Timeout: 200 * time.Millisecond, Resolver: "127.0.0.1:1"} e.do("POST", "/zones", e.token, map[string]any{"name": "example.com", "records": []map[string]any{{"name": "ns1", "type": "A", "data": "192.0.2.1"}}}, 201) e.do("GET", "/zones/example.com/dnssec?view=", e.token, nil, 200) e.do("PATCH", "/zones/example.com", e.token, map[string]any{"dnssec_policy": "default"}, 200) got := e.do("GET", "/zones/example.com/dnssec", e.token, nil, 200) if got["policy"] != "default" || got["phase"].(map[string]any)["code"] != dnssec.PhaseStarting || !strings.HasSuffix(got["key_dir"].(string), "zones/dnssec-keys/_default") { t.Errorf("status: %v", got) } // Switching off at once is refused; insecure is the way. out := e.do("PATCH", "/zones/example.com", e.token, map[string]any{"dnssec_policy": "off"}, 422) if !strings.Contains(out["error"].(map[string]any)["message"].(string), "switching DNSSEC off at once") { t.Errorf("switching off: %v", out) } cd := e.do("POST", "/zones/example.com/dnssec/check-ds", e.token, nil, 200) if cd["parent"].(map[string]any)["error"] == "" || len(cd["marked"].([]any)) != 0 { t.Errorf("check-ds without a resolver: %v", cd) } // Wipe: refused while the parent can't be checked, unless forced. out = e.do("POST", "/zones/example.com/dnssec/wipe", e.token, nil, 409) if !strings.Contains(out["error"].(map[string]any)["message"].(string), "wipe with force") { t.Errorf("wipe: %v", out) } e.do("POST", "/zones/example.com/dnssec/wipe", e.token, map[string]any{"force": true, "dry_run": true}, 200) if e.do("GET", "/zones/example.com", e.token, nil, 200)["dnssec_policy"] != "default" { t.Error("a dry run changed the zone") } e.do("POST", "/zones/example.com/dnssec/wipe", e.token, map[string]any{"force": true}, 200) if p, ok := e.do("GET", "/zones/example.com", e.token, nil, 200)["dnssec_policy"]; ok && p != "" { t.Errorf("after wipe: %v", p) } e.do("POST", "/zones/example.com/dnssec/wipe", e.token, map[string]any{"force": true}, 409) // nothing left // Secondary zones aren't signed by Jarl. e.do("POST", "/zones", e.token, map[string]any{"name": "copy.example", "type": "secondary", "options": []map[string]any{{"name": "primaries", "value": "{ 192.0.2.1; }"}}}, 201) e.do("GET", "/zones/copy.example/dnssec", e.token, nil, 400) e.do("PATCH", "/zones/copy.example", e.token, map[string]any{"dnssec_policy": "default"}, 422) } func TestDSChanges(t *testing.T) { ds := dnssec.DS{KeyTag: 1, Algorithm: 13, DigestType: 2, Digest: "AA"} key := func(goal, dsState string) *dnssec.Key { d := ds return &dnssec.Key{Tag: 1, Role: "CSK", DS: &d, Goal: goal, DSState: dsState} } with := &nscheck.ParentDSReport{Servers: []nscheck.ParentAnswer{{Status: "delegated", DS: []dnssec.DS{ds}}, {Status: "delegated", DS: []dnssec.DS{ds}}}, DS: []dnssec.DS{ds}, Consistent: true} half := &nscheck.ParentDSReport{Servers: []nscheck.ParentAnswer{{Status: "delegated", DS: []dnssec.DS{ds}}, {Status: "delegated"}}, DS: []dnssec.DS{ds}} without := &nscheck.ParentDSReport{Servers: []nscheck.ParentAnswer{{Status: "delegated"}}, Consistent: true} signed := &model.Zone{DNSSECPolicy: "default"} off := &model.Zone{DNSSECPolicy: "insecure"} if p, w := dsChanges(signed, []*dnssec.Key{key("omnipresent", "rumoured")}, with); len(p) != 1 || len(w) != 0 { t.Errorf("DS added: %v %v", p, w) } if p, _ := dsChanges(signed, []*dnssec.Key{key("omnipresent", "rumoured")}, half); len(p) != 0 { t.Error("published while not every parent server has it") } if p, w := dsChanges(signed, []*dnssec.Key{key("omnipresent", "rumoured")}, without); len(p)+len(w) != 0 { t.Error("a signed zone without a DS yet is just waiting") } if p, w := dsChanges(off, []*dnssec.Key{key("hidden", "unretentive")}, without); len(p) != 0 || len(w) != 1 { t.Errorf("DS removed: %v %v", p, w) } if _, w := dsChanges(off, []*dnssec.Key{key("hidden", "unretentive")}, half); len(w) != 0 { t.Error("withdrawn while a parent server still has it") } }