feat: edge gateway service
This commit is contained in:
+319
@@ -0,0 +1,319 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user