320 lines
9.5 KiB
Markdown
320 lines
9.5 KiB
Markdown
# 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.
|
|
|
|
```mermaid
|
|
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.
|
|
|
|
## Recommended Outcome
|
|
|
|
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.
|