package api import ( "context" "encoding/json" "strings" "gitea.dooplex.hu/admin/felhom-hub/internal/offsitekeys" "net/http" "net/http/httptest" "testing" "gitea.dooplex.hu/admin/felhom-hub/internal/store" ) // R-820 / decision 69: the consume endpoint is RETIRED — it answers 410 and its body carries no // password, even with a stored, unconsumed secret and the right key. Before v0.127.0 it returned the // sub-account password, which can rewrite authorized_keys and remove the append-only pin. func TestConsumePassword_RetiredReturnsNoPassword(t *testing.T) { h, st, _ := newTestHandler(t) st.SaveCustomerConfig(&store.CustomerConfig{CustomerID: "c1", APIKey: "ckey", RetrievalPassword: "pp"}) st.SaveCustomerConfig(&store.CustomerConfig{CustomerID: "c2", APIKey: "ckey2", RetrievalPassword: "pp2"}) if err := st.SaveOneTimeSecret("c1", "the-transient-pw"); err != nil { t.Fatal(err) } do := func(token string) *httptest.ResponseRecorder { req := httptest.NewRequest(http.MethodPost, "/api/v1/offsite/consume-password/c1", nil) if token != "" { req.Header.Set("Authorization", "Bearer "+token) } rr := httptest.NewRecorder() h.ServeHTTP(rr, req) return rr } if rr := do(""); rr.Code != http.StatusUnauthorized { t.Fatalf("no auth → %d, want 401", rr.Code) } if rr := do("ckey2"); rr.Code != http.StatusUnauthorized { t.Fatalf("cross-customer key → %d, want 401", rr.Code) } rr := do("ckey") if rr.Code != http.StatusGone { t.Fatalf("consume → %d, want 410 (retired)", rr.Code) } if strings.Contains(rr.Body.String(), "the-transient-pw") { t.Fatalf("the retired endpoint leaked the password: %q", rr.Body.String()) } // The stored secret is untouched (still usable by the HUB's registrar). if pw, err := st.OffsitePassword("c1"); err != nil || pw != "the-transient-pw" { t.Fatalf("stored credential changed: %v", err) } } type fakeKeySvc struct { gotPub string gotFP string err error } func (f *fakeKeySvc) RegisterKey(_ context.Context, _ string, pub string) (offsitekeys.InstallResult, error) { f.gotPub = pub return offsitekeys.InstallResult{Fingerprint: "SHA256:fake"}, f.err } func (f *fakeKeySvc) ConfirmKey(_ context.Context, _ string, fp string) (int, error) { f.gotFP = fp return 1, f.err } func (f *fakeKeySvc) OpenWindowFor(context.Context, string, int) (offsitekeys.WindowGrant, error) { return offsitekeys.WindowGrant{Granted: false, Reason: "not due"}, f.err } func (f *fakeKeySvc) CloseWindowFor(context.Context, string, offsitekeys.WindowResult) error { return f.err } func (f *fakeKeySvc) MoveAside(context.Context, string) (string, error) { return "/home/felhom-repo.orphaned-20261003", f.err } const testPub = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK436vIXGqfs6wz4Jv/GIIo3rQuW3oNnP7nMatI92gkA box" // Every box-facing off-site key response: auth enforced, and NO response body carries the stored // password (the decision-69 invariant, asserted on the consequence — the bytes the box receives). func TestOffsiteKeyEndpoints_AuthAndNoPasswordInAnyResponse(t *testing.T) { h, st, _ := newTestHandler(t) st.SaveCustomerConfig(&store.CustomerConfig{CustomerID: "c1", APIKey: "ckey", RetrievalPassword: "pp"}) st.SaveCustomerConfig(&store.CustomerConfig{CustomerID: "c2", APIKey: "ckey2", RetrievalPassword: "pp2"}) if err := st.SaveOneTimeSecret("c1", "the-transient-pw"); err != nil { t.Fatal(err) } f := &fakeKeySvc{} h.SetOffsiteKeyService(f) post := func(path, token, body string) *httptest.ResponseRecorder { req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(body)) if token != "" { req.Header.Set("Authorization", "Bearer "+token) } rr := httptest.NewRecorder() h.ServeHTTP(rr, req) return rr } reg := `{"public_key":"` + testPub + `"}` if rr := post("/api/v1/offsite/register-key/c1", "ckey2", reg); rr.Code != http.StatusUnauthorized { t.Fatalf("cross-customer register → %d, want 401", rr.Code) } if rr := post("/api/v1/offsite/register-key/c1", "ckey", `{"public_key":"command=\"x\" `+testPub+`"}`); rr.Code != http.StatusBadRequest { t.Fatalf("a key with options → %d, want 400 (the hub writes the options)", rr.Code) } for _, c := range []struct{ path, body string }{ {"/api/v1/offsite/register-key/c1", reg}, {"/api/v1/offsite/confirm-key/c1", `{"fingerprint":"SHA256:fake"}`}, {"/api/v1/offsite/move-aside/c1", ``}, {"/api/v1/offsite/window-open/c1", `{"count_before":3}`}, {"/api/v1/offsite/window-close/c1", `{"window_id":1,"count_after":3,"outcome":"nothing"}`}, } { rr := post(c.path, "ckey", c.body) if rr.Code != http.StatusOK { t.Fatalf("%s → %d (%s)", c.path, rr.Code, rr.Body.String()) } if strings.Contains(rr.Body.String(), "the-transient-pw") { t.Fatalf("%s leaked the password: %s", c.path, rr.Body.String()) } var m map[string]any if err := json.Unmarshal(rr.Body.Bytes(), &m); err != nil { t.Fatalf("%s: not JSON: %v", c.path, err) } if _, ok := m["password"]; ok { t.Fatalf("%s: a password field in the response", c.path) } } if f.gotPub != testPub || f.gotFP != "SHA256:fake" { t.Fatalf("service not reached: pub=%q fp=%q", f.gotPub, f.gotFP) } }