phase 3
This commit is contained in:
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user