Add register-by-signature, web fixes, bin scripts, docs
CI / test (push) Successful in 5s

- Register by signing service key: GET /v1/service-key, POST /v1/auth/register-by-signature
- Login auto-attempts register first for new users
- Web: default API URL momswap.produktor.duckdns.org, /libs/ static handler
- Docker: webbuild stage for geo-api-client, copy web+libs to runtime
- Bin scripts: test.sh, run.sh, up.sh, down.sh
- docs/ed25519-security-use-cases.md: use cases, message formats, examples
- SERVICE_PUBLIC_KEY env (defaults to ADMIN_PUBLIC_KEY)

Made-with: Cursor
This commit is contained in:
2026-03-01 12:58:44 +00:00
parent 978e0403eb
commit a5a97a0ad9
19 changed files with 405 additions and 41 deletions
+57 -1
View File
@@ -21,7 +21,7 @@ func newTestServer(adminPublicKey string) *httptest.Server {
svc := app.NewService(memory, app.Config{
ChallengeTTL: 5 * time.Minute,
SessionTTL: 24 * time.Hour,
})
}, adminPublicKey)
svc.BootstrapAdmin(adminPublicKey)
api := httpapi.NewAPI(svc)
return httptest.NewServer(api.Routes())
@@ -36,6 +36,22 @@ func mustJSON(t *testing.T, value interface{}) []byte {
return b
}
func getJSON(t *testing.T, client *http.Client, url string, token string) (*http.Response, map[string]interface{}) {
t.Helper()
req, _ := http.NewRequest(http.MethodGet, url, nil)
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("do request: %v", err)
}
defer resp.Body.Close()
out := map[string]interface{}{}
_ = json.NewDecoder(resp.Body).Decode(&out)
return resp, out
}
func postJSON(t *testing.T, client *http.Client, url string, body interface{}, token string) (*http.Response, map[string]interface{}) {
t.Helper()
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(mustJSON(t, body)))
@@ -107,6 +123,46 @@ func registerUserViaAdmin(t *testing.T, client *http.Client, baseURL, adminPub s
}
}
func TestRegisterBySignature(t *testing.T) {
adminPub, _, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("generate admin key: %v", err)
}
adminPubB64 := base64.RawURLEncoding.EncodeToString(adminPub)
server := newTestServer(adminPubB64)
defer server.Close()
client := server.Client()
svcKeyResp, svcKeyData := getJSON(t, client, server.URL+"/v1/service-key", "")
if svcKeyResp.StatusCode != http.StatusOK {
t.Fatalf("get service key status=%d body=%v", svcKeyResp.StatusCode, svcKeyData)
}
if svcKeyData["publicKey"] != adminPubB64 {
t.Fatalf("service key mismatch: got %v", svcKeyData["publicKey"])
}
userPub, userPriv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("generate user key: %v", err)
}
userPubB64 := base64.RawURLEncoding.EncodeToString(userPub)
sig := base64.RawURLEncoding.EncodeToString(ed25519.Sign(userPriv, []byte(adminPubB64)))
regResp, regData := postJSON(t, client, server.URL+"/v1/auth/register-by-signature", map[string]string{
"publicKey": userPubB64,
"signature": sig,
}, "")
if regResp.StatusCode != http.StatusCreated {
t.Fatalf("register-by-signature status=%d body=%v", regResp.StatusCode, regData)
}
userToken := loginUser(t, client, server.URL, userPubB64, userPriv)
if userToken == "" {
t.Fatal("login after register-by-signature failed")
}
}
func TestRegisterLoginAndProfile(t *testing.T) {
adminPub, adminPriv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {