Files
galaxy-game/SECURITY.md
T
2026-04-02 19:18:42 +02:00

9.5 KiB

Secure Exchange Architecture

Purpose

This document fixes the transport-level secure exchange model between client and server. It is the starting point for implementing authenticated device sessions, signed requests/responses, and anti-replay protection.

Main Principles

  • No browser cookies are used.
  • Authentication is device-session based.
  • Each device/session is unique and independently revocable.
  • There are no short-lived access tokens or refresh-token flows in the main design.
  • Requests are authenticated by client-side signatures.
  • Responses are authenticated by server-side signatures.
  • Transport integrity and freshness are verified before payload is processed.
sequenceDiagram
    participant Client
    participant Gateway
    participant SessionCache
    participant ReplayStore
    participant Business

    Client->>Gateway: ExecuteCommand / SubscribeEvents\n(protocol_version, device_session_id,\nmessage_type, timestamp_ms, request_id,\npayload_hash, signature)
    Gateway->>SessionCache: lookup(device_session_id)
    SessionCache-->>Gateway: user_id, client_public_key, status
    Gateway->>Gateway: verify payload_hash, signature,\nfreshness window
    Gateway->>ReplayStore: reserve(device_session_id, request_id, ttl)
    ReplayStore-->>Gateway: accepted / duplicate
    Gateway->>Business: verified command context
    Business-->>Gateway: response payload
    Gateway-->>Client: signed response
    Gateway-->>Client: signed push events on SubscribeEvents

Device Session Model

After successful login through e-mail code:

  1. client generates an asymmetric key pair
  2. private key remains on the client device
  3. public key is registered on the server as the standard base64-encoded raw 32-byte Ed25519 public key
  4. server creates a persistent device_session
  5. client stores:
    • device_session_id
    • private key

The server stores at least:

  • device_session_id
  • user_id
  • base64-encoded raw 32-byte Ed25519 client public key
  • session status
  • revoke metadata

Key Storage

Native Clients

Private key should be stored in platform secure storage.

Browser / WASM Clients

Private key should be created and used through WebCrypto. Non-exportable key storage is preferred. Loss of browser storage is acceptable and means re-login is required.

Request Structure

Each authenticated request logically contains:

  • payload_bytes
  • request_envelope
  • signature

Request Envelope

Minimal required fields:

  • protocol_version
  • device_session_id
  • message_type
  • timestamp_ms
  • request_id
  • payload_hash

The supported request protocol_version literal for the v1 gateway transport is v1. The v1 authenticated request signature scheme is Ed25519. The stored client public key is the standard base64-encoded raw 32-byte Ed25519 public key, and the request signature field carries the raw 64-byte Ed25519 signature bytes.

Request Signing Input

The client signs canonical bytes built from:

  • request domain marker galaxy-request-v1
  • protocol_version
  • device_session_id
  • message_type
  • timestamp_ms
  • request_id
  • payload_hash

The canonical v1 request signing input uses this binary encoding:

  • each string and bytes field is encoded as uvarint(len(field_bytes)) followed by raw bytes
  • timestamp_ms is encoded as an 8-byte big-endian unsigned integer
  • fields are appended in the exact order listed above

payload_hash is the raw 32-byte SHA-256 digest computed from raw payload_bytes. Empty payloads still use the SHA-256 digest of the empty byte slice.

The goal is to bind the signature to:

  • the concrete device session
  • the concrete message type
  • the concrete payload
  • a fresh request instance

Response Structure

Each server response logically contains:

  • payload_bytes
  • response_envelope
  • signature

Response Envelope

Minimal required fields:

  • protocol_version
  • request_id
  • timestamp_ms
  • result_code
  • payload_hash

Response Signing Input

The server signs canonical bytes built from:

  • response domain marker galaxy-response-v1
  • protocol_version
  • request_id
  • timestamp_ms
  • result_code
  • payload_hash

The current gateway v1 response signature scheme is Ed25519. The canonical v1 response signing input uses this binary encoding:

  • each string and bytes field is encoded as uvarint(len(field_bytes)) followed by raw bytes
  • timestamp_ms is encoded as an 8-byte big-endian unsigned integer
  • fields are appended in the exact order listed above

The gateway server loads the response signing key from a PKCS#8 PEM-encoded Ed25519 private key. The client verifies the signature using a trusted server public key.

Event Structure

Each server push event logically contains:

  • payload_bytes
  • event_envelope
  • signature

Event Envelope

Minimal required fields:

  • event_type
  • event_id
  • timestamp_ms
  • payload_hash

Optional fields:

  • request_id
  • trace_id

The current gateway v1 stream-event signature scheme is Ed25519. The gateway currently signs unary responses and stream events with the same PKCS#8 PEM-encoded Ed25519 private key. The bootstrap event implemented for SubscribeEvents uses event_type = gateway.server_time, reuses the opening subscribe request_id as event_id, and encodes server_time_ms in a FlatBuffers gateway.ServerTimeEvent payload. Later client-facing push events are sourced from internal pub/sub with target metadata user_id and optional device_session_id, plus event_type, event_id, payload_bytes, and optional request_id / trace_id. The gateway derives timestamp_ms, recomputes payload_hash, signs the event at delivery time, and only then forwards it to the matching active streams.

Event Signing Input

The server signs canonical bytes built from:

  • event domain marker galaxy-event-v1
  • event_type
  • event_id
  • timestamp_ms
  • request_id
  • trace_id
  • payload_hash

The canonical v1 event signing input uses this binary encoding:

  • each string and bytes field is encoded as uvarint(len(field_bytes)) followed by raw bytes
  • timestamp_ms is encoded as an 8-byte big-endian unsigned integer
  • fields are appended in the exact order listed above

Verification Order on Server

Before processing payload, the server/gateway must:

  1. verify that the transport envelope is present and supported
  2. resolve device_session_id
  3. reject unknown or revoked sessions
  4. verify client signature using stored public key
  5. verify timestamp freshness window
  6. verify anti-replay constraints using request_id
  7. only then pass payload to business processing

Verification Order on Client

Before accepting response payload, the client must:

  1. verify server signature
  2. verify request_id matches the corresponding request
  3. verify payload_hash
  4. verify timestamp freshness if applicable
  5. only then accept the response payload

Before accepting push-event payload, the client must:

  1. verify server event signature
  2. verify payload_hash
  3. verify request_id when the event is correlated to the opening request
  4. verify timestamp freshness if applicable
  5. only then accept the event payload

Anti-Replay Model

Transport anti-replay uses:

  • timestamp_ms
  • request_id

The server accepts requests only inside an allowed time window. The current gateway v1 freshness window is symmetric ±5 minutes around server time. Recently seen request_id values must be tracked for the corresponding session and rejected on reuse. Replay reservations should remain active until timestamp_ms + freshness_window so future-skewed but still valid requests stay protected after acceptance.

This protects transport freshness. It does not replace business idempotency.

Server Time Offset

Clients use server time offset instead of trusting local clock directly.

Expected approach:

  • client establishes an authenticated SubscribeEvents gRPC stream
  • server provides current server time
  • client computes local offset
  • subsequent signed requests use adjusted time

No extra sync request is required when the authenticated push stream is already open.

TLS and MITM Considerations

Native Clients notes

Native clients should use TLS pinning in addition to signed request/response exchange. Pinning should be based on public key / SPKI rather than leaf certificate whenever possible.

Browser / WASM Clients notes

Real TLS pinning is not available in the browser in the same way as in native clients. Browser clients still use the signed request/response model, but browser-managed TLS remains the platform limitation.

Threat Model Boundaries

This design protects against:

  • request/response tampering in transit
  • replay of previously seen transport messages inside the protected window
  • use of unknown or revoked device sessions
  • forged server responses without server signing key
  • forged client requests without client signing key

This design does not guarantee that a legitimate user cannot generate their own valid requests from their own client environment. That is handled by server-side business validation and authorization.

Architectural Notes

  • Transport authentication and business authorization are separate concerns.
  • Signed transport proves message origin and integrity.
  • Business services must still validate command correctness, ownership, permissions, and state transitions.
  • Transport request_id is not the same as business idempotency key.

The system should treat the secure exchange layer as the mandatory outer contract for all authenticated traffic. Only after successful transport validation may payload be routed to business logic.