fix(engine): enforce strict HMAC response signature verification per §4.4
Remove the lenient fallback that accepted bot responses missing the X-ACB-Signature header. Missing or invalid signatures now cause the response to be discarded and count toward the crash threshold (§4.5). Add tests for missing-header, bad-signature, and crash-after-10 cases. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
80733f673e
commit
5443e4d0ed
2 changed files with 135 additions and 8 deletions
|
|
@ -149,16 +149,15 @@ func (b *HTTPBot) GetMoves(state *VisibleState) ([]Move, error) {
|
|||
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
// Verify response signature
|
||||
// Verify response signature (strict — per §4.4)
|
||||
responseSig := resp.Header.Get("X-ACB-Signature")
|
||||
if responseSig == "" {
|
||||
// Missing signature - accept anyway for now (will be strict in production)
|
||||
// In production, this would be: b.recordFailure(); return nil, fmt.Errorf("missing response signature")
|
||||
} else {
|
||||
if err := VerifyResponse(b.auth.Secret, b.matchID, b.turn, responseSig, responseBody); err != nil {
|
||||
b.recordFailure()
|
||||
return nil, fmt.Errorf("response signature verification failed: %w", err)
|
||||
}
|
||||
b.recordFailure()
|
||||
return nil, fmt.Errorf("missing response signature")
|
||||
}
|
||||
if err := VerifyResponse(b.auth.Secret, b.matchID, b.turn, responseSig, responseBody); err != nil {
|
||||
b.recordFailure()
|
||||
return nil, fmt.Errorf("response signature verification failed: %w", err)
|
||||
}
|
||||
|
||||
// Parse response
|
||||
|
|
|
|||
|
|
@ -236,6 +236,134 @@ func TestHTTPBot_ValidateMoves(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestHTTPBot_MissingSignature(t *testing.T) {
|
||||
// Server that returns valid moves but omits X-ACB-Signature header
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := MoveResponse{Moves: []Move{
|
||||
{Position: Position{Row: 5, Col: 5}, Direction: DirN},
|
||||
}}
|
||||
body, _ := json.Marshal(resp)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
// Intentionally no X-ACB-Signature header
|
||||
w.Write(body)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
auth := AuthConfig{
|
||||
BotID: "b_test",
|
||||
Secret: "test-secret",
|
||||
MatchID: "m_test",
|
||||
}
|
||||
bot := NewHTTPBot(server.URL, auth)
|
||||
|
||||
state := &VisibleState{
|
||||
MatchID: "m_test",
|
||||
Turn: 1,
|
||||
Config: DefaultConfig(),
|
||||
You: struct {
|
||||
ID int `json:"id"`
|
||||
Energy int `json:"energy"`
|
||||
Score int `json:"score"`
|
||||
}{ID: 0},
|
||||
Bots: []VisibleBot{
|
||||
{Position: Position{Row: 5, Col: 5}, Owner: 0},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := bot.GetMoves(state)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing signature, got nil")
|
||||
}
|
||||
if bot.failCount != 1 {
|
||||
t.Errorf("failCount = %d, want 1", bot.failCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPBot_BadSignature(t *testing.T) {
|
||||
// Server that returns moves with a wrong-key signature
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var state VisibleState
|
||||
json.NewDecoder(r.Body).Decode(&state)
|
||||
|
||||
resp := MoveResponse{Moves: []Move{
|
||||
{Position: Position{Row: 5, Col: 5}, Direction: DirN},
|
||||
}}
|
||||
body, _ := json.Marshal(resp)
|
||||
|
||||
// Sign with wrong secret
|
||||
sig := SignResponse("wrong-secret", state.MatchID, state.Turn, body)
|
||||
w.Header().Set("X-ACB-Signature", sig)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(body)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
auth := AuthConfig{
|
||||
BotID: "b_test",
|
||||
Secret: "test-secret",
|
||||
MatchID: "m_test",
|
||||
}
|
||||
bot := NewHTTPBot(server.URL, auth)
|
||||
|
||||
state := &VisibleState{
|
||||
MatchID: "m_test",
|
||||
Turn: 1,
|
||||
Config: DefaultConfig(),
|
||||
You: struct {
|
||||
ID int `json:"id"`
|
||||
Energy int `json:"energy"`
|
||||
Score int `json:"score"`
|
||||
}{ID: 0},
|
||||
Bots: []VisibleBot{
|
||||
{Position: Position{Row: 5, Col: 5}, Owner: 0},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := bot.GetMoves(state)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for bad signature, got nil")
|
||||
}
|
||||
if bot.failCount != 1 {
|
||||
t.Errorf("failCount = %d, want 1", bot.failCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPBot_BadSignatureCrashes(t *testing.T) {
|
||||
// Verify that 10 consecutive bad-signature responses crashes the bot
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var state VisibleState
|
||||
json.NewDecoder(r.Body).Decode(&state)
|
||||
|
||||
resp := MoveResponse{Moves: []Move{}}
|
||||
body, _ := json.Marshal(resp)
|
||||
sig := SignResponse("wrong-secret", state.MatchID, state.Turn, body)
|
||||
w.Header().Set("X-ACB-Signature", sig)
|
||||
w.Write(body)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
auth := AuthConfig{
|
||||
BotID: "b_test",
|
||||
Secret: "test-secret",
|
||||
MatchID: "m_test",
|
||||
}
|
||||
bot := NewHTTPBot(server.URL, auth)
|
||||
|
||||
state := &VisibleState{
|
||||
MatchID: "m_test",
|
||||
Turn: 1,
|
||||
Config: DefaultConfig(),
|
||||
}
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
bot.GetMoves(state)
|
||||
}
|
||||
|
||||
if !bot.IsCrashed() {
|
||||
t.Error("bot should be crashed after 10 consecutive bad-signature failures")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPBot_Health(t *testing.T) {
|
||||
// Create a server with health endpoint
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue