package server_test import ( "bytes" "encoding/hex" "encoding/json" "fmt" "io" "net/http" "net/http/httptest" "strings" "testing" "time" "git.n1ko.dev/Niko/niko_trust/internal/server" "git.n1ko.dev/Niko/niko_trust/pkg/identity/signer" "git.n1ko.dev/Niko/niko_trust/pkg/protocol" "git.n1ko.dev/Niko/niko_trust/pkg/tce" "git.n1ko.dev/Niko/niko_trust/pkg/transport" "git.n1ko.dev/Niko/niko_trust/pkg/verify" ) func newTestServer(t *testing.T) *httptest.Server { t.Helper() srv := server.New(server.Config{Audience: "trust.n1ko.dev"}) return httptest.NewServer(srv.Handler()) } func postEnvelope(t *testing.T, url string, env *transport.Envelope) *http.Response { t.Helper() var buf bytes.Buffer if err := json.NewEncoder(&buf).Encode(env); err != nil { t.Fatal(err) } resp, err := http.Post(url, "application/json", &buf) if err != nil { t.Fatal(err) } return resp } // authToken performs the challenge/assert handshake and returns the session // token, for use by tests that need an authenticated read. func authToken(t *testing.T, baseURL string, key *signer.Signer) string { t.Helper() cr, err := http.Post(baseURL+"/v1/auth/challenge", "application/json", nil) if err != nil { t.Fatal(err) } var chRes struct { Challenge string `json:"challenge"` } _ = json.NewDecoder(cr.Body).Decode(&chRes) cr.Body.Close() ch, err := hex.DecodeString(chRes.Challenge) if err != nil { t.Fatal(err) } a := &protocol.AuthAssertion{ PubKey: key.Public(), Challenge: ch, Scope: "read", Audience: "trust.n1ko.dev", CreatedAt: uint64(time.Now().Unix()), } tceBytes, err := protocol.EncodeAuthAssertion(a) if err != nil { t.Fatal(err) } sig := key.Sign(tceBytes) resp := postEnvelope(t, baseURL+"/v1/auth/assert", &transport.Envelope{TCE: tceBytes, Signature: sig}) if resp.StatusCode != http.StatusOK { t.Fatalf("assert status %d", resp.StatusCode) } var out struct { SessionToken string `json:"session_token"` } _ = json.NewDecoder(resp.Body).Decode(&out) resp.Body.Close() if out.SessionToken == "" { t.Fatal("no session token") } return out.SessionToken } func TestPublishAndGetClaim(t *testing.T) { ts := newTestServer(t) defer ts.Close() issuer, _ := signer.Generate() subject, _ := signer.Generate() c := &protocol.Claim{ Issuer: issuer.Public(), Subject: subject.Public(), Claims: map[string]tce.Value{"flag.example": tce.Bool(true)}, CreatedAt: 1_700_000_000, ExpiresAt: 1_700_086_400, Serial: 1, Nonce: bytes.Repeat([]byte{0x01}, tce.NonceSize), } tceBytes, err := protocol.EncodeClaim(c) if err != nil { t.Fatal(err) } sig := issuer.Sign(tceBytes) // Publish. resp := postEnvelope(t, ts.URL+"/v1/objects", &transport.Envelope{TCE: tceBytes, Signature: sig}) if resp.StatusCode != http.StatusOK { t.Fatalf("publish status %d", resp.StatusCode) } var pub struct { ObjectID string `json:"object_id"` } _ = json.NewDecoder(resp.Body).Decode(&pub) resp.Body.Close() if pub.ObjectID == "" { t.Fatal("no object_id returned") } // The returned ID must equal SHA-256(tce). if pub.ObjectID != tce.ComputeID(tceBytes).String() { t.Fatal("object_id mismatch") } // Fetch it back. got, err := http.Get(ts.URL + "/v1/objects/" + pub.ObjectID) if err != nil { t.Fatal(err) } defer got.Body.Close() if got.StatusCode != http.StatusOK { t.Fatalf("get status %d", got.StatusCode) } var env transport.Envelope if err := json.NewDecoder(got.Body).Decode(&env); err != nil { t.Fatal(err) } // The verifier checks the signature over the exact TCE bytes. if typ, err := env.Verify(); err != nil { t.Fatalf("verify %s: %v", typ, err) } // Query by subject. The read endpoint now requires an authenticated // session; authenticate as the subject itself with scope read:claims. tok := authToken(t, ts.URL, subject) q, err := http.NewRequest(http.MethodGet, ts.URL+"/v1/claims?subject="+subject.Address().String(), nil) if err != nil { t.Fatal(err) } q.Header.Set("Authorization", "Bearer "+tok) qresp, err := http.DefaultClient.Do(q) if err != nil { t.Fatal(err) } defer qresp.Body.Close() if qresp.StatusCode != http.StatusOK { t.Fatalf("claims status %d", qresp.StatusCode) } var list struct { Claims []json.RawMessage `json:"claims"` } if err := json.NewDecoder(qresp.Body).Decode(&list); err != nil { t.Fatal(err) } if len(list.Claims) != 1 { t.Fatalf("expected 1 claim, got %d", len(list.Claims)) } } func TestAuthHandshake(t *testing.T) { ts := newTestServer(t) defer ts.Close() client, _ := signer.Generate() // Challenge. cr, err := http.Post(ts.URL+"/v1/auth/challenge", "application/json", nil) if err != nil { t.Fatal(err) } var chRes struct { Challenge string `json:"challenge"` } _ = json.NewDecoder(cr.Body).Decode(&chRes) cr.Body.Close() ch, err := hex.DecodeString(chRes.Challenge) if err != nil { t.Fatal(err) } // Assertion. a := &protocol.AuthAssertion{ PubKey: client.Public(), Challenge: ch, Scope: "read:claims", Audience: "trust.n1ko.dev", CreatedAt: uint64(time.Now().Unix()), } b, err := protocol.EncodeAuthAssertion(a) if err != nil { t.Fatal(err) } sig := client.Sign(b) resp := postEnvelope(t, ts.URL+"/v1/auth/assert", &transport.Envelope{TCE: b, Signature: sig}) if resp.StatusCode != http.StatusOK { t.Fatalf("assert status %d", resp.StatusCode) } var out struct { SessionToken string `json:"session_token"` Identity string `json:"identity"` } _ = json.NewDecoder(resp.Body).Decode(&out) resp.Body.Close() if out.SessionToken == "" { t.Fatal("no session token") } if out.Identity != client.Address().String() { t.Fatalf("identity mismatch: %s != %s", out.Identity, client.Address().String()) } } func TestRejectsBadObjectID(t *testing.T) { ts := newTestServer(t) defer ts.Close() issuer, _ := signer.Generate() c := &protocol.Claim{ Issuer: issuer.Public(), Subject: issuer.Public(), Claims: map[string]tce.Value{"x": tce.Bool(true)}, CreatedAt: 1_700_000_000, ExpiresAt: 1_700_086_400, Serial: 1, Nonce: bytes.Repeat([]byte{0x02}, tce.NonceSize), } tceBytes, _ := protocol.EncodeClaim(c) sig := issuer.Sign(tceBytes) // Lie about the object_id. resp := postEnvelope(t, ts.URL+"/v1/objects", &transport.Envelope{ TCE: tceBytes, Signature: sig, ObjectID: "deadbeef", }) if resp.StatusCode != http.StatusUnprocessableEntity { t.Fatalf("expected 422, got %d", resp.StatusCode) } resp.Body.Close() } func TestGetMissingReturns404(t *testing.T) { ts := newTestServer(t) defer ts.Close() resp, _ := http.Get(ts.URL + "/v1/objects/" + hex.EncodeToString(bytes.Repeat([]byte{0}, 32))) if resp.StatusCode != http.StatusNotFound { t.Fatalf("expected 404, got %d", resp.StatusCode) } resp.Body.Close() } func TestDuplicatePutIsIdempotent(t *testing.T) { ts := newTestServer(t) defer ts.Close() issuer, _ := signer.Generate() c := &protocol.Claim{ Issuer: issuer.Public(), Subject: issuer.Public(), Claims: map[string]tce.Value{"x": tce.Bool(true)}, CreatedAt: 1_700_000_000, ExpiresAt: 1_700_086_400, Serial: 1, Nonce: bytes.Repeat([]byte{0x03}, tce.NonceSize), } tceBytes, _ := protocol.EncodeClaim(c) sig := issuer.Sign(tceBytes) env := &transport.Envelope{TCE: tceBytes, Signature: sig} for i := 0; i < 2; i++ { resp := postEnvelope(t, ts.URL+"/v1/objects", env) if resp.StatusCode != http.StatusOK { t.Fatalf("put %d: expected 200, got %d", i, resp.StatusCode) } resp.Body.Close() } } func TestAssertRejectsBadSignature(t *testing.T) { ts := newTestServer(t) defer ts.Close() claimer, _ := signer.Generate() impostor, _ := signer.Generate() a := &protocol.AuthAssertion{ PubKey: claimer.Public(), Challenge: bytes.Repeat([]byte{0x07}, tce.ChallengeSize), Scope: "read:claims", Audience: "trust.n1ko.dev", CreatedAt: uint64(time.Now().Unix()), } tceBytes, _ := protocol.EncodeAuthAssertion(a) // Sign with the wrong key. sig := impostor.Sign(tceBytes) resp := postEnvelope(t, ts.URL+"/v1/auth/assert", &transport.Envelope{TCE: tceBytes, Signature: sig}) if resp.StatusCode != http.StatusUnauthorized { t.Fatalf("expected 401, got %d", resp.StatusCode) } resp.Body.Close() } func TestAssertRejectsUnissuedChallenge(t *testing.T) { ts := newTestServer(t) defer ts.Close() client, _ := signer.Generate() // Assertion bound to a challenge the server never issued. a := &protocol.AuthAssertion{ PubKey: client.Public(), Challenge: bytes.Repeat([]byte{0x09}, tce.ChallengeSize), Scope: "read:claims", Audience: "trust.n1ko.dev", CreatedAt: uint64(time.Now().Unix()), } tceBytes, _ := protocol.EncodeAuthAssertion(a) sig := client.Sign(tceBytes) resp := postEnvelope(t, ts.URL+"/v1/auth/assert", &transport.Envelope{TCE: tceBytes, Signature: sig}) if resp.StatusCode != http.StatusUnauthorized { t.Fatalf("expected 401 for unissued challenge, got %d", resp.StatusCode) } resp.Body.Close() } func TestAssertChallengeIsSingleUse(t *testing.T) { ts := newTestServer(t) defer ts.Close() client, _ := signer.Generate() chRes := struct { Challenge string `json:"challenge"` }{} resp, _ := http.Post(ts.URL+"/v1/auth/challenge", "application/json", nil) _ = json.NewDecoder(resp.Body).Decode(&chRes) resp.Body.Close() ch, _ := hex.DecodeString(chRes.Challenge) a := &protocol.AuthAssertion{ PubKey: client.Public(), Challenge: ch, Scope: "read:claims", Audience: "trust.n1ko.dev", CreatedAt: uint64(time.Now().Unix()), } tceBytes, _ := protocol.EncodeAuthAssertion(a) sig := client.Sign(tceBytes) env := &transport.Envelope{TCE: tceBytes, Signature: sig} // First use succeeds. r1 := postEnvelope(t, ts.URL+"/v1/auth/assert", env) if r1.StatusCode != http.StatusOK { t.Fatalf("first assert: expected 200, got %d", r1.StatusCode) } r1.Body.Close() // Replaying the same challenge (captured assertion) is rejected. r2 := postEnvelope(t, ts.URL+"/v1/auth/assert", env) if r2.StatusCode != http.StatusUnauthorized { t.Fatalf("replay: expected 401, got %d", r2.StatusCode) } r2.Body.Close() } func TestClaimsRequiresAuth(t *testing.T) { ts := newTestServer(t) defer ts.Close() resp, _ := http.Get(ts.URL + "/v1/claims?subject=" + hex.EncodeToString(bytes.Repeat([]byte{0}, 32))) if resp.StatusCode != http.StatusUnauthorized { t.Fatalf("expected 401, got %d", resp.StatusCode) } resp.Body.Close() } func TestClaimsInsufficientScope(t *testing.T) { ts := newTestServer(t) defer ts.Close() client, _ := signer.Generate() // Authenticate with a scope that does not cover read:claims. cr, _ := http.Post(ts.URL+"/v1/auth/challenge", "application/json", nil) var chRes struct { Challenge string `json:"challenge"` } _ = json.NewDecoder(cr.Body).Decode(&chRes) cr.Body.Close() ch, _ := hex.DecodeString(chRes.Challenge) a := &protocol.AuthAssertion{ PubKey: client.Public(), Challenge: ch, Scope: "read:requests", Audience: "trust.n1ko.dev", CreatedAt: uint64(time.Now().Unix()), } tceBytes, _ := protocol.EncodeAuthAssertion(a) sig := client.Sign(tceBytes) ar := postEnvelope(t, ts.URL+"/v1/auth/assert", &transport.Envelope{TCE: tceBytes, Signature: sig}) var out struct { SessionToken string `json:"session_token"` } _ = json.NewDecoder(ar.Body).Decode(&out) ar.Body.Close() req, _ := http.NewRequest(http.MethodGet, ts.URL+"/v1/claims?subject=x", nil) req.Header.Set("Authorization", "Bearer "+out.SessionToken) resp, _ := http.DefaultClient.Do(req) if resp.StatusCode != http.StatusForbidden { t.Fatalf("expected 403, got %d", resp.StatusCode) } resp.Body.Close() } func TestStorePersistsAcrossRestart(t *testing.T) { dir := t.TempDir() // Start one server, publish an object. s1 := server.New(server.Config{Audience: "trust.n1ko.dev", DataDir: dir}) issuer, _ := signer.Generate() c := &protocol.Claim{ Issuer: issuer.Public(), Subject: issuer.Public(), Claims: map[string]tce.Value{"x": tce.Bool(true)}, CreatedAt: 1_700_000_000, ExpiresAt: 1_700_086_400, Serial: 1, Nonce: bytes.Repeat([]byte{0x04}, tce.NonceSize), } tceBytes, _ := protocol.EncodeClaim(c) sig := issuer.Sign(tceBytes) id, _, err := s1.Store().Put(tceBytes, sig, "") if err != nil { t.Fatal(err) } // A fresh server over the same directory must recover the object. s2 := server.New(server.Config{Audience: "trust.n1ko.dev", DataDir: dir}) env, err := s2.Store().Get(id) if err != nil { t.Fatalf("object not recovered after restart: %v", err) } if _, err := env.Verify(); err != nil { t.Fatalf("recovered object failed verify: %v", err) } if got, _ := s2.Store().ClaimsBySubject(issuer.Address().String()); len(got) != 1 { t.Fatalf("expected 1 recovered claim, got %d", len(got)) } } func TestConfigReportsAudience(t *testing.T) { ts := newTestServer(t) defer ts.Close() resp, _ := http.Get(ts.URL + "/v1/config") if resp.StatusCode != http.StatusOK { t.Fatalf("config status %d", resp.StatusCode) } var cfg struct { Audience string `json:"audience"` } _ = json.NewDecoder(resp.Body).Decode(&cfg) resp.Body.Close() if cfg.Audience != "trust.n1ko.dev" { t.Fatalf("audience = %q", cfg.Audience) } } func TestReadEndpointsRequireAuth(t *testing.T) { ts := newTestServer(t) defer ts.Close() for _, ep := range []string{ "/v1/claims?subject=x", "/v1/requests?recipient=x", "/v1/responses?request=x", "/v1/revocations?claim=x", } { resp, _ := http.Get(ts.URL + ep) if resp.StatusCode != http.StatusUnauthorized { t.Fatalf("%s: expected 401, got %d", ep, resp.StatusCode) } resp.Body.Close() } } func TestApprovalFlow(t *testing.T) { ts := newTestServer(t) defer ts.Close() issuer, _ := signer.Generate() approver, _ := signer.Generate() subject, _ := signer.Generate() now := uint64(time.Now().Unix()) req := &protocol.ApprovalRequest{ Sender: issuer.Public(), Recipient: approver.Public(), Action: "admin", Message: "approve", CreatedAt: now, ExpiresAt: now + 30, Nonce: bytes.Repeat([]byte{0x11}, tce.NonceSize), } reqTCE, _ := protocol.EncodeApprovalRequest(req) reqID := tce.ComputeID(reqTCE).String() if _, err := sPut(ts, reqTCE, issuer.Sign(reqTCE)); err != nil { t.Fatal(err) } resp := &protocol.ApprovalResponse{ RequestHash: tce.ComputeID(reqTCE), Responder: approver.Public(), Decision: protocol.Allow, CreatedAt: now + 5, Nonce: bytes.Repeat([]byte{0x22}, tce.NonceSize), } respTCE, _ := protocol.EncodeApprovalResponse(resp) if _, err := sPut(ts, respTCE, approver.Sign(respTCE)); err != nil { t.Fatal(err) } c := &protocol.Claim{ Issuer: issuer.Public(), Subject: subject.Public(), Claims: map[string]tce.Value{"admin": tce.Bool(true)}, CreatedAt: now - 10, Serial: 1, Nonce: bytes.Repeat([]byte{0x33}, tce.NonceSize), } claimTCE, _ := protocol.EncodeClaim(c) claimID, err := sPut(ts, claimTCE, issuer.Sign(claimTCE)) if err != nil { t.Fatal(err) } rv := &protocol.Revocation{ Issuer: issuer.Public(), ClaimID: tce.ComputeID(claimTCE), Reason: "bad", CreatedAt: now, Nonce: bytes.Repeat([]byte{0x44}, tce.NonceSize), } rvTCE, _ := protocol.EncodeRevocation(rv) if _, err := sPut(ts, rvTCE, issuer.Sign(rvTCE)); err != nil { t.Fatal(err) } // Authenticate with a generic read token. tok := authToken(t, ts.URL, subject) check := func(ep string, want int) []json.RawMessage { r, _ := http.NewRequest(http.MethodGet, ts.URL+ep, nil) r.Header.Set("Authorization", "Bearer "+tok) resp, _ := http.DefaultClient.Do(r) if resp.StatusCode != want { t.Fatalf("%s: expected %d, got %d", ep, want, resp.StatusCode) } var wrap struct { Requests []json.RawMessage `json:"requests"` Responses []json.RawMessage `json:"responses"` Revocations []json.RawMessage `json:"revocations"` } _ = json.NewDecoder(resp.Body).Decode(&wrap) resp.Body.Close() switch { case wrap.Requests != nil: return wrap.Requests case wrap.Responses != nil: return wrap.Responses case wrap.Revocations != nil: return wrap.Revocations } return nil } if got := check("/v1/requests?recipient="+approver.Address().String(), http.StatusOK); len(got) != 1 { t.Fatalf("requests: expected 1, got %d", len(got)) } if got := check("/v1/responses?request="+reqID, http.StatusOK); len(got) != 1 { t.Fatalf("responses: expected 1, got %d", len(got)) } if got := check("/v1/revocations?claim="+claimID, http.StatusOK); len(got) != 1 { t.Fatalf("revocations: expected 1, got %d", len(got)) } } // sPut publishes a raw envelope to the relay. func sPut(ts *httptest.Server, tceBytes, sig []byte) (string, error) { var buf bytes.Buffer _ = json.NewEncoder(&buf).Encode(&transport.Envelope{TCE: tceBytes, Signature: sig}) resp, err := http.Post(ts.URL+"/v1/objects", "application/json", &buf) if err != nil { return "", err } if resp.StatusCode != http.StatusOK { resp.Body.Close() return "", fmt.Errorf("put status %d", resp.StatusCode) } var out struct { ObjectID string `json:"object_id"` } _ = json.NewDecoder(resp.Body).Decode(&out) resp.Body.Close() return out.ObjectID, nil } func TestPutStoresUnverifiedEnvelope(t *testing.T) { // The relay is a dumb store: it accepts any well-formed envelope regardless // of signature validity. Signature checking is deferred to the verifier // (verify.Graph.Add), which is what decides "should I believe it". ts := newTestServer(t) defer ts.Close() issuer, _ := signer.Generate() subject, _ := signer.Generate() now := uint64(time.Now().Unix()) c := &protocol.Claim{ Issuer: issuer.Public(), Subject: subject.Public(), Claims: map[string]tce.Value{"x": tce.Bool(true)}, CreatedAt: now - 10, Serial: 1, Nonce: make([]byte, tce.NonceSize), } cb, _ := protocol.EncodeClaim(c) sig := issuer.Sign(cb) // Tamper with the signature so verification must fail downstream. bad := make([]byte, len(sig)) copy(bad, sig) bad[len(bad)-1] ^= 0xff env := &transport.Envelope{TCE: cb, Signature: bad} resp := postEnvelope(t, ts.URL+"/v1/objects", env) defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("relay should store unverified envelope, got %d", resp.StatusCode) } // The verifier must reject the tampered envelope. if err := verify.NewGraph().Add(env); err == nil { t.Fatal("expected verify.Graph.Add to reject tampered signature") } } func TestIdempotentPut(t *testing.T) { ts := newTestServer(t) defer ts.Close() issuer, _ := signer.Generate() subject, _ := signer.Generate() now := uint64(time.Now().Unix()) c := &protocol.Claim{ Issuer: issuer.Public(), Subject: subject.Public(), Claims: map[string]tce.Value{"x": tce.Bool(true)}, CreatedAt: now - 10, Serial: 1, Nonce: make([]byte, tce.NonceSize), } cb, _ := protocol.EncodeClaim(c) sig := issuer.Sign(cb) id1, err := sPut(ts, cb, sig) if err != nil { t.Fatal(err) } id2, err := sPut(ts, cb, sig) if err != nil { t.Fatal(err) } if id1 != id2 { t.Fatalf("expected identical object id on replay, got %q and %q", id1, id2) } // A replayed claim must not create a duplicate: only one claim for subject. tok := authToken(t, ts.URL, subject) r, _ := http.NewRequest(http.MethodGet, ts.URL+"/v1/claims?subject="+subject.Address().String(), nil) r.Header.Set("Authorization", "Bearer "+tok) resp, _ := http.DefaultClient.Do(r) var list struct { Claims []json.RawMessage `json:"claims"` } _ = json.NewDecoder(resp.Body).Decode(&list) resp.Body.Close() if len(list.Claims) != 1 { t.Fatalf("expected exactly 1 stored claim after replay, got %d", len(list.Claims)) } } func TestHealthMetricsEndpoints(t *testing.T) { ts := newTestServer(t) defer ts.Close() for _, ep := range []string{"/v1/healthz", "/v1/readyz"} { resp, _ := http.Get(ts.URL + ep) if resp.StatusCode != http.StatusOK { t.Fatalf("%s: expected 200, got %d", ep, resp.StatusCode) } resp.Body.Close() } resp, _ := http.Get(ts.URL + "/v1/metrics") if resp.StatusCode != http.StatusOK { t.Fatalf("metrics: expected 200, got %d", resp.StatusCode) } body, _ := io.ReadAll(resp.Body) resp.Body.Close() if !strings.Contains(string(body), "trust_objects_stored") { t.Fatalf("metrics missing objects_stored: %s", body) } } func TestPagination(t *testing.T) { ts := newTestServer(t) defer ts.Close() issuer, _ := signer.Generate() subject, _ := signer.Generate() now := uint64(time.Now().Unix()) // Publish three distinct claims about the same subject. for i := 0; i < 3; i++ { c := &protocol.Claim{ Issuer: issuer.Public(), Subject: subject.Public(), Claims: map[string]tce.Value{"x": tce.Number(fmt.Sprintf("%d", i))}, CreatedAt: now - 10, Serial: uint64(i + 1), Nonce: bytes.Repeat([]byte{byte(i + 1)}, tce.NonceSize), } cb, _ := protocol.EncodeClaim(c) if _, err := sPut(ts, cb, issuer.Sign(cb)); err != nil { t.Fatal(err) } } tok := authToken(t, ts.URL, subject) r, _ := http.NewRequest(http.MethodGet, ts.URL+"/v1/claims?subject="+subject.Address().String()+"&limit=1&offset=1", nil) r.Header.Set("Authorization", "Bearer "+tok) resp, _ := http.DefaultClient.Do(r) var list struct { Claims []json.RawMessage `json:"claims"` } _ = json.NewDecoder(resp.Body).Decode(&list) resp.Body.Close() if len(list.Claims) != 1 { t.Fatalf("expected 1 paginated claim, got %d", len(list.Claims)) } } func TestPerSubjectQuota(t *testing.T) { // Exercise handlePut directly (bypassing the HTTP rate limiter) so we can // publish enough distinct claims to reach the per-subject quota. srv := server.New(server.Config{Audience: "trust.n1ko.dev"}) issuer, _ := signer.Generate() subject, _ := signer.Generate() now := uint64(time.Now().Unix()) const n = 1000 for i := 0; i < n; i++ { c := &protocol.Claim{ Issuer: issuer.Public(), Subject: subject.Public(), Claims: map[string]tce.Value{"x": tce.Number(fmt.Sprintf("%d", i))}, CreatedAt: now - 10, Serial: uint64(i + 1), Nonce: bytes.Repeat([]byte{byte((i + 1) & 0xff)}, tce.NonceSize), } cb, _ := protocol.EncodeClaim(c) env := &transport.Envelope{TCE: cb, Signature: issuer.Sign(cb)} var buf bytes.Buffer _ = json.NewEncoder(&buf).Encode(env) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPut, "/v1/objects", &buf) srv.HandlePut(rec, req) if rec.Code != http.StatusOK { t.Fatalf("claim %d: expected 200, got %d", i, rec.Code) } } // The next distinct claim exceeds the per-subject quota. over := &protocol.Claim{ Issuer: issuer.Public(), Subject: subject.Public(), Claims: map[string]tce.Value{"x": tce.Number("9999")}, CreatedAt: now - 10, Serial: 9999, Nonce: bytes.Repeat([]byte{0xee}, tce.NonceSize), } ob, _ := protocol.EncodeClaim(over) env := &transport.Envelope{TCE: ob, Signature: issuer.Sign(ob)} var buf bytes.Buffer _ = json.NewEncoder(&buf).Encode(env) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodPut, "/v1/objects", &buf) srv.HandlePut(rec, req) if rec.Code != http.StatusUnprocessableEntity { t.Fatalf("expected 422 for quota exceeded, got %d", rec.Code) } } func TestOneResponsePerRequest(t *testing.T) { ts := newTestServer(t) defer ts.Close() issuer, _ := signer.Generate() approver, _ := signer.Generate() now := uint64(time.Now().Unix()) req := &protocol.ApprovalRequest{ Sender: issuer.Public(), Recipient: approver.Public(), Action: "admin", CreatedAt: now, ExpiresAt: now + 30, Nonce: make([]byte, tce.NonceSize), } reqb, _ := protocol.EncodeApprovalRequest(req) resp1 := &protocol.ApprovalResponse{ RequestHash: tce.ComputeID(reqb), Responder: approver.Public(), Decision: protocol.Allow, CreatedAt: now + 5, Nonce: bytes.Repeat([]byte{0x01}, tce.NonceSize), } r1, _ := protocol.EncodeApprovalResponse(resp1) if _, err := sPut(ts, r1, approver.Sign(r1)); err != nil { t.Fatal(err) } resp2 := &protocol.ApprovalResponse{ RequestHash: tce.ComputeID(reqb), Responder: approver.Public(), Decision: protocol.Allow, CreatedAt: now + 6, Nonce: bytes.Repeat([]byte{0x02}, tce.NonceSize), } r2, _ := protocol.EncodeApprovalResponse(resp2) if _, err := sPut(ts, r2, approver.Sign(r2)); err == nil { t.Fatal("expected second response to same request to be rejected") } } func TestRateLimit(t *testing.T) { ts := newTestServer(t) defer ts.Close() issuer, _ := signer.Generate() subject, _ := signer.Generate() now := uint64(time.Now().Unix()) c := &protocol.Claim{ Issuer: issuer.Public(), Subject: subject.Public(), Claims: map[string]tce.Value{"x": tce.Bool(true)}, CreatedAt: now - 10, Serial: 1, Nonce: make([]byte, tce.NonceSize), } cb, _ := protocol.EncodeClaim(c) env := &transport.Envelope{TCE: cb, Signature: issuer.Sign(cb)} got429 := false for i := 0; i < 65; i++ { var buf bytes.Buffer _ = json.NewEncoder(&buf).Encode(env) resp, _ := http.Post(ts.URL+"/v1/objects", "application/json", &buf) if resp.StatusCode == http.StatusTooManyRequests { got429 = true } resp.Body.Close() } if !got429 { t.Fatal("expected rate limit (429) after bursting PUT") } } func TestRejectsOversizeBody(t *testing.T) { ts := newTestServer(t) defer ts.Close() // An envelope whose base64 TCE exceeds the server body cap. env := &transport.Envelope{TCE: bytes.Repeat([]byte{0xff}, 10000)} var buf bytes.Buffer _ = json.NewEncoder(&buf).Encode(env) resp, _ := http.Post(ts.URL+"/v1/objects", "application/json", &buf) if resp.StatusCode != http.StatusRequestEntityTooLarge { t.Fatalf("expected 413, got %d", resp.StatusCode) } resp.Body.Close() }