Secure UDP Protocol

This page describes the current secure UDP behavior used by AISMixer and its station-side proxy.

Packet Prefixes

Handshake

The station sends a text handshake packet:

NMEA-H|<station_id>|<unix_timestamp>|<base64(client_signature)>

The current client signing payload is not the text packet above. It is:

b"NMEA-H" + station_id.encode() + timestamp.to_bytes(8, "big")

The server verifies the client signature against the authorized public key for station_id. The server accepts only timestamps within 30 seconds of server time.

After signature verification, the server checks a process-local handshake replay cache. The replay key is based on the station id, timestamp, and exact client signature bytes. The cache TTL is 60 seconds and it is bounded to 100000 entries. Replayed verified handshakes are rejected before a session is created or an OK response is sent.

On success, the server signs the same current handshake payload and responds:

OK|<base64(server_signature)>

Session Key

The station and server derive a shared secret with ECDH. The AES session key is derived with SHA-256 over a protocol label, the ECDH shared secret, the client signature, and the server signature.

The server stores sessions by UDP peer address. Each session currently contains the station id, AES-GCM object, creation time, last-seen time, and a per-session data nonce cache.

Encrypted Data

The station encrypts each extracted AIS/NMEA sentence as JSON:

{
  "type": "nmea",
  "payload": "<NMEA sentence>",
  "timestamp": 1234567890,
  "source_id": "<station_id>"
}

The UDP packet layout is:

NMEA-D || 12-byte AES-GCM nonce || ciphertext || 16-byte GCM tag

AES-GCM uses AAD:

b"NMEA"

The server rejects malformed NMEA-D packets that are too short to contain the prefix, 12-byte nonce, and GCM tag-sized payload.

Data Nonce Replay Protection

Data nonce tracking is per session. The nonce key is the raw 12-byte AES-GCM nonce from the packet.

For an active session, the server checks whether the nonce is already recorded before decrypting. Duplicate nonces are rejected without decrypting, touching the session, or enqueueing data.

For a new nonce, the server decrypts, parses JSON, and verifies that source_id matches the authenticated session station id. Only after those checks pass does it record the nonce. The data nonce cache TTL currently matches the session TTL, 300 seconds, and is bounded to 100000 entries per session.

Failed decrypts, invalid JSON, malformed packets, and source_id mismatches do not record nonces.

Session TTL And Keepalive

Sessions expire when last_seen is more than 300 seconds old. Expired sessions are removed when looked up.

The station sends plaintext keepalive packets every 30 seconds:

KEEPALIVE|<station_id>|<unix_timestamp>

The server updates last_seen only when an active session exists for the UDP peer address and the keepalive station_id matches the session station id. Keepalive packets do not create sessions and do not enqueue AIS data.

Station Identity

The handshake station id selects the authorized public key. Encrypted data must carry a JSON source_id that matches the station id stored in the active session. A mismatch is rejected and does not refresh the session or record the data nonce.

When accepted data is queued, the secure input source identifier is the configured secure input id if provided, otherwise the station id.

Current Limitations