package canon_test import ( "bytes" "crypto/ed25519" "encoding/hex" "testing" "galaxy/core/canon" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestBuildEventSigningInputChangesWhenSignedFieldChanges(t *testing.T) { t.Parallel() base := canon.EventSigningFields{ EventType: "gateway.server_time", EventID: "evt-123", TimestampMS: 123456789, RequestID: "request-123", TraceID: "trace-123", PayloadHash: sha256Sum([]byte("payload")), } baseInput := canon.BuildEventSigningInput(base) tests := []struct { name string mutate func(canon.EventSigningFields) canon.EventSigningFields }{ { name: "event type", mutate: func(fields canon.EventSigningFields) canon.EventSigningFields { fields.EventType = "gateway.other" return fields }, }, { name: "event id", mutate: func(fields canon.EventSigningFields) canon.EventSigningFields { fields.EventID = "evt-456" return fields }, }, { name: "timestamp", mutate: func(fields canon.EventSigningFields) canon.EventSigningFields { fields.TimestampMS++ return fields }, }, { name: "request id", mutate: func(fields canon.EventSigningFields) canon.EventSigningFields { fields.RequestID = "request-456" return fields }, }, { name: "trace id", mutate: func(fields canon.EventSigningFields) canon.EventSigningFields { fields.TraceID = "trace-456" return fields }, }, { name: "payload hash", mutate: func(fields canon.EventSigningFields) canon.EventSigningFields { fields.PayloadHash = sha256Sum([]byte("other")) return fields }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() mutated := canon.BuildEventSigningInput(tt.mutate(base)) assert.False(t, bytes.Equal(baseInput, mutated)) }) } } func TestSignAndVerifyEventSignature(t *testing.T) { t.Parallel() seed := bytes.Repeat([]byte{0xBB}, ed25519.SeedSize) privateKey := ed25519.NewKeyFromSeed(seed) publicKey, _ := privateKey.Public().(ed25519.PublicKey) fields := canon.EventSigningFields{ EventType: "gateway.server_time", EventID: "evt-123", TimestampMS: 123456789, RequestID: "request-123", TraceID: "trace-123", PayloadHash: sha256Sum([]byte("payload")), } signature := ed25519.Sign(privateKey, canon.BuildEventSigningInput(fields)) require.NoError(t, canon.VerifyEventSignature(publicKey, signature, fields)) t.Run("rejects mutated trace id", func(t *testing.T) { t.Parallel() mutated := fields mutated.TraceID = "trace-other" require.ErrorIs(t, canon.VerifyEventSignature(publicKey, signature, mutated), canon.ErrInvalidEventSignature) }) t.Run("rejects invalid signature length", func(t *testing.T) { t.Parallel() require.ErrorIs(t, canon.VerifyEventSignature(publicKey, signature[:1], fields), canon.ErrInvalidEventSignature) }) t.Run("rejects invalid public key length", func(t *testing.T) { t.Parallel() require.ErrorIs(t, canon.VerifyEventSignature(publicKey[:8], signature, fields), canon.ErrInvalidEventSignature) }) } func TestEventCanonicalBytesFixture(t *testing.T) { t.Parallel() var fx eventFixture loadJSONFixture(t, "event_gateway_server_time.json", &fx) fields := canon.EventSigningFields{ EventType: fx.EventType, EventID: fx.EventID, TimestampMS: fx.TimestampMS, RequestID: fx.RequestID, TraceID: fx.TraceID, PayloadHash: mustHex(t, fx.PayloadHashHex), } require.NoError(t, canon.VerifyPayloadHash([]byte(fx.Payload), fields.PayloadHash)) gotInput := canon.BuildEventSigningInput(fields) assert.Equal(t, fx.ExpectedCanonicalBytesHex, hex.EncodeToString(gotInput)) seed := mustHex(t, fx.PrivateKeySeedHex) require.Len(t, seed, ed25519.SeedSize) privateKey := ed25519.NewKeyFromSeed(seed) publicKey, _ := privateKey.Public().(ed25519.PublicKey) signature := ed25519.Sign(privateKey, gotInput) assert.Equal(t, fx.ExpectedSignatureHex, hex.EncodeToString(signature)) require.NoError(t, canon.VerifyEventSignature(publicKey, signature, fields)) }