package canon_test import ( "bytes" "crypto/ed25519" "crypto/sha256" "encoding/hex" "testing" "galaxy/core/canon" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestVerifyPayloadHash(t *testing.T) { t.Parallel() payloadSum := sha256.Sum256([]byte("payload")) emptySum := sha256.Sum256(nil) otherSum := sha256.Sum256([]byte("other")) tests := []struct { name string payload []byte payloadHash []byte wantErr error }{ { name: "matches non-empty payload", payload: []byte("payload"), payloadHash: payloadSum[:], }, { name: "matches empty payload", payload: nil, payloadHash: emptySum[:], }, { name: "rejects digest with invalid length", payload: []byte("payload"), payloadHash: []byte("short"), wantErr: canon.ErrInvalidPayloadHash, }, { name: "rejects digest mismatch", payload: []byte("payload"), payloadHash: otherSum[:], wantErr: canon.ErrPayloadHashMismatch, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() err := canon.VerifyPayloadHash(tt.payload, tt.payloadHash) if tt.wantErr == nil { require.NoError(t, err) return } require.ErrorIs(t, err, tt.wantErr) }) } } func TestBuildRequestSigningInputChangesWhenSignedFieldChanges(t *testing.T) { t.Parallel() base := canon.RequestSigningFields{ ProtocolVersion: "v1", DeviceSessionID: "device-session-123", MessageType: "user.games.command", TimestampMS: 123456789, RequestID: "request-123", PayloadHash: sha256Sum([]byte("payload")), } baseInput := canon.BuildRequestSigningInput(base) tests := []struct { name string mutate func(canon.RequestSigningFields) canon.RequestSigningFields }{ { name: "protocol version", mutate: func(fields canon.RequestSigningFields) canon.RequestSigningFields { fields.ProtocolVersion = "v2" return fields }, }, { name: "device session id", mutate: func(fields canon.RequestSigningFields) canon.RequestSigningFields { fields.DeviceSessionID = "device-session-456" return fields }, }, { name: "message type", mutate: func(fields canon.RequestSigningFields) canon.RequestSigningFields { fields.MessageType = "user.account.get" return fields }, }, { name: "timestamp", mutate: func(fields canon.RequestSigningFields) canon.RequestSigningFields { fields.TimestampMS++ return fields }, }, { name: "request id", mutate: func(fields canon.RequestSigningFields) canon.RequestSigningFields { fields.RequestID = "request-456" return fields }, }, { name: "payload hash", mutate: func(fields canon.RequestSigningFields) canon.RequestSigningFields { fields.PayloadHash = sha256Sum([]byte("other")) return fields }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() mutated := canon.BuildRequestSigningInput(tt.mutate(base)) assert.False(t, bytes.Equal(baseInput, mutated)) }) } } func TestRequestCanonicalBytesFixtures(t *testing.T) { t.Parallel() fixtures := []string{ "request_user_account_get.json", "request_lobby_my_games_list.json", "request_user_games_command.json", } for _, name := range fixtures { t.Run(name, func(t *testing.T) { t.Parallel() var fx requestFixture loadJSONFixture(t, name, &fx) fields := canon.RequestSigningFields{ ProtocolVersion: fx.ProtocolVersion, DeviceSessionID: fx.DeviceSessionID, MessageType: fx.MessageType, TimestampMS: fx.TimestampMS, RequestID: fx.RequestID, PayloadHash: mustHex(t, fx.PayloadHashHex), } require.NoError(t, canon.VerifyPayloadHash([]byte(fx.Payload), fields.PayloadHash), "payload hash must match payload bytes") gotInput := canon.BuildRequestSigningInput(fields) assert.Equal(t, fx.ExpectedCanonicalBytesHex, hex.EncodeToString(gotInput), "canonical bytes drift from fixture") seed := mustHex(t, fx.PrivateKeySeedHex) require.Len(t, seed, ed25519.SeedSize) privateKey := ed25519.NewKeyFromSeed(seed) signature := ed25519.Sign(privateKey, gotInput) assert.Equal(t, fx.ExpectedSignatureHex, hex.EncodeToString(signature), "signature drift from fixture") require.NoError(t, canon.VerifyRequestSignature(fx.PublicKeyBase64, signature, fields)) }) } }