This commit is contained in:
Ilia Denisov
2026-05-07 09:40:37 +02:00
parent 63cccdc958
commit dc1c9b109c
29 changed files with 1991 additions and 20 deletions
+227
View File
@@ -0,0 +1,227 @@
package authn_test
import (
"crypto/ed25519"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"testing"
"galaxy/core/canon"
"galaxy/core/keypair"
"galaxy/gateway/authn"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func sha256Of(payload []byte) []byte {
sum := sha256.Sum256(payload)
return sum[:]
}
// TestParityWithUICoreCanonicalBytes proves that the gateway-side
// authn package and the client-side ui/core canon package produce the
// exact same canonical signing input for every v1 envelope. Any drift
// here means a client signature would be silently rejected by the
// gateway (or vice versa).
func TestParityWithUICoreCanonicalBytes(t *testing.T) {
t.Parallel()
t.Run("request", func(t *testing.T) {
t.Parallel()
gatewayFields := authn.RequestSigningFields{
ProtocolVersion: "v1",
DeviceSessionID: "device-session-parity",
MessageType: "user.games.command",
TimestampMS: 1_700_000_000_000,
RequestID: "request-parity",
PayloadHash: sha256Of([]byte("payload")),
}
clientFields := canon.RequestSigningFields{
ProtocolVersion: gatewayFields.ProtocolVersion,
DeviceSessionID: gatewayFields.DeviceSessionID,
MessageType: gatewayFields.MessageType,
TimestampMS: gatewayFields.TimestampMS,
RequestID: gatewayFields.RequestID,
PayloadHash: gatewayFields.PayloadHash,
}
assert.Equal(t,
authn.BuildRequestSigningInput(gatewayFields),
canon.BuildRequestSigningInput(clientFields))
})
t.Run("response", func(t *testing.T) {
t.Parallel()
gatewayFields := authn.ResponseSigningFields{
ProtocolVersion: "v1",
RequestID: "request-parity",
TimestampMS: 1_700_000_000_500,
ResultCode: "ok",
PayloadHash: sha256Of([]byte("response-payload")),
}
clientFields := canon.ResponseSigningFields{
ProtocolVersion: gatewayFields.ProtocolVersion,
RequestID: gatewayFields.RequestID,
TimestampMS: gatewayFields.TimestampMS,
ResultCode: gatewayFields.ResultCode,
PayloadHash: gatewayFields.PayloadHash,
}
assert.Equal(t,
authn.BuildResponseSigningInput(gatewayFields),
canon.BuildResponseSigningInput(clientFields))
})
t.Run("event", func(t *testing.T) {
t.Parallel()
gatewayFields := authn.EventSigningFields{
EventType: "gateway.server_time",
EventID: "evt-parity",
TimestampMS: 1_700_000_001_000,
RequestID: "request-parity",
TraceID: "trace-parity",
PayloadHash: sha256Of([]byte("event-payload")),
}
clientFields := canon.EventSigningFields{
EventType: gatewayFields.EventType,
EventID: gatewayFields.EventID,
TimestampMS: gatewayFields.TimestampMS,
RequestID: gatewayFields.RequestID,
TraceID: gatewayFields.TraceID,
PayloadHash: gatewayFields.PayloadHash,
}
assert.Equal(t,
authn.BuildEventSigningInput(gatewayFields),
canon.BuildEventSigningInput(clientFields))
})
}
// TestParityRequestSignedByUICoreAcceptedByGateway proves that a
// request the client signs with `keypair.Sign` is accepted by the
// gateway's `authn.VerifyRequestSignature`. This is the acceptance
// criterion from `ui/PLAN.md` Phase 3.
func TestParityRequestSignedByUICoreAcceptedByGateway(t *testing.T) {
t.Parallel()
privateKey, publicKey, err := keypair.Generate(rand.Reader)
require.NoError(t, err)
clientFields := canon.RequestSigningFields{
ProtocolVersion: "v1",
DeviceSessionID: "device-session-parity",
MessageType: "user.account.get",
TimestampMS: 1_700_000_000_000,
RequestID: "request-parity",
PayloadHash: sha256Of([]byte("payload")),
}
signature, err := keypair.Sign(privateKey, canon.BuildRequestSigningInput(clientFields))
require.NoError(t, err)
encodedKey, err := keypair.MarshalPublicKey(publicKey)
require.NoError(t, err)
gatewayFields := authn.RequestSigningFields{
ProtocolVersion: clientFields.ProtocolVersion,
DeviceSessionID: clientFields.DeviceSessionID,
MessageType: clientFields.MessageType,
TimestampMS: clientFields.TimestampMS,
RequestID: clientFields.RequestID,
PayloadHash: clientFields.PayloadHash,
}
require.NoError(t,
authn.VerifyRequestSignature(encodedKey, signature, gatewayFields))
}
// TestParityResponseSignedByGatewayAcceptedByUICore proves that a
// response signed by the gateway's `Ed25519ResponseSigner` is
// accepted by the client's `canon.VerifyResponseSignature`. The
// reverse acceptance criterion from `ui/PLAN.md` Phase 3.
func TestParityResponseSignedByGatewayAcceptedByUICore(t *testing.T) {
t.Parallel()
_, privateKey, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
signer, err := authn.NewEd25519ResponseSigner(privateKey)
require.NoError(t, err)
gatewayFields := authn.ResponseSigningFields{
ProtocolVersion: "v1",
RequestID: "request-parity",
TimestampMS: 1_700_000_000_500,
ResultCode: "ok",
PayloadHash: sha256Of([]byte("response-payload")),
}
signature, err := signer.SignResponse(gatewayFields)
require.NoError(t, err)
clientFields := canon.ResponseSigningFields{
ProtocolVersion: gatewayFields.ProtocolVersion,
RequestID: gatewayFields.RequestID,
TimestampMS: gatewayFields.TimestampMS,
ResultCode: gatewayFields.ResultCode,
PayloadHash: gatewayFields.PayloadHash,
}
require.NoError(t,
canon.VerifyResponseSignature(signer.PublicKey(), signature, clientFields))
}
// TestParityEventSignedByGatewayAcceptedByUICore proves that a
// stream event signed by the gateway's response signer (which signs
// both responses and events with the same key) is accepted by the
// client's `canon.VerifyEventSignature`.
func TestParityEventSignedByGatewayAcceptedByUICore(t *testing.T) {
t.Parallel()
_, privateKey, err := ed25519.GenerateKey(rand.Reader)
require.NoError(t, err)
signer, err := authn.NewEd25519ResponseSigner(privateKey)
require.NoError(t, err)
gatewayFields := authn.EventSigningFields{
EventType: "gateway.server_time",
EventID: "evt-parity",
TimestampMS: 1_700_000_001_000,
RequestID: "request-parity",
TraceID: "trace-parity",
PayloadHash: sha256Of([]byte("event-payload")),
}
signature, err := signer.SignEvent(gatewayFields)
require.NoError(t, err)
clientFields := canon.EventSigningFields{
EventType: gatewayFields.EventType,
EventID: gatewayFields.EventID,
TimestampMS: gatewayFields.TimestampMS,
RequestID: gatewayFields.RequestID,
TraceID: gatewayFields.TraceID,
PayloadHash: gatewayFields.PayloadHash,
}
require.NoError(t,
canon.VerifyEventSignature(signer.PublicKey(), signature, clientFields))
}
// TestParityClientPublicKeyEncodingMatchesBackend proves that the
// base64 encoding `keypair.MarshalPublicKey` produces is the exact
// string form `authn.VerifyRequestSignature` expects when the
// gateway reads a client public key out of session cache.
func TestParityClientPublicKeyEncodingMatchesBackend(t *testing.T) {
t.Parallel()
_, publicKey, err := keypair.Generate(rand.Reader)
require.NoError(t, err)
encoded, err := keypair.MarshalPublicKey(publicKey)
require.NoError(t, err)
expected := base64.StdEncoding.EncodeToString(publicKey)
require.Equal(t, expected, encoded)
}
+3
View File
@@ -5,6 +5,7 @@ go 1.26.1
require (
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1
buf.build/go/protovalidate v1.1.3
galaxy/core v0.0.0-00010101000000-000000000000
galaxy/redisconn v0.0.0-00010101000000-000000000000
github.com/alicebob/miniredis/v2 v2.37.0
github.com/getkin/kin-openapi v0.135.0
@@ -102,3 +103,5 @@ require (
)
replace galaxy/redisconn => ../pkg/redisconn
replace galaxy/core => ../ui/core