Wire Protocol
Framing
[ u32_be frame_length ][ u8 frame_type ][ frame_payload ]
Max frame size: 65 535 bytes (mirrors ZSP_MAX_BYTES).
| Code | Name | Direction |
|---|---|---|
0x01 | SERVER_HELLO | server → client |
0x02 | CLIENT_HELLO | client → server |
0x03 | SERVER_FINISHED | server → client |
0x10 | APP_DATA | both |
0xfe | ALERT | both |
Handshake (1 RTT happy path)
Frame 1 — SERVER_HELLO
Canonical JSON payload (sorted-key):
{
"protocol_version": 1,
"server_machine_id": "<uuid>",
"server_kid": "kid_xxxxxxxxxxxx",
"zeqond_number": 2287490000,
"nonce_s": "<32-hex>",
"cipher_suite": "ZEQ-SSL-AES256-GCM-HMAC-SHA256-V1"
}
No signature here — the server has not yet authenticated the client; signing would waste a derivation.
Frame 2 — CLIENT_HELLO
{
"protocol_version": 1,
"local_machine_id": "<client uuid>",
"local_kid": "kid_bravo",
"peer_machine_id": "<server uuid>",
"peer_kid": "kid_alpha",
"zeqond_number": 2287490000,
"nonce_self": "<client nonce>",
"nonce_peer": "<server nonce>",
"cipher_suite": "ZEQ-SSL-AES256-GCM-HMAC-SHA256-V1",
"mode": "A",
"auth_tag": "<64-hex HMAC-SHA256 over canonicalJson(rest)>"
}
The server, on receipt:
- Lookup peer machine + kid via
/api/ssl/verify(or local DB). - Confirm
peer_kidfingerprint is valid atzeqond_number. - Confirm
|zeqond_number - currentZeqond()| ≤ SSL_CLOCK_SKEW_ZEQONDS = 2. - Derive
shared_secret_handshakeviaderiveSharedSecret(...). - Re-sign the payload,
crypto.timingSafeEqualagainstauth_tag.
Mismatch on any step → ALERT with reason code.
Frame 3 — SERVER_FINISHED
{
"protocol_version": 1,
"session_id": "<32-hex>",
"server_auth_tag": "<64-hex>"
}
Both sides are now mutually authenticated and share a confirmed
shared_secret_handshake.
Bulk encryption — APP_DATA
Per-direction record key:
record_key = HKDF-SHA256(
IKM = shared_secret_handshake,
salt = session_id,
info = "zeq-ssl-record/" + direction + "/" + zeqond_window,
L = 32 bytes,
)
direction∈"c2s","s2c".zeqond_window = floor(zeqond_number / SSL_REKEY_INTERVAL_ZEQONDS).
Frame payload:
[ zeqond_window u32_be ][ iv 12B ][ gcm_tag 16B ][ ciphertext ]
The reference implementation in @zeq-os/zeq-ssl re-derives the per-window
key on every frame; cache the last 1–2 windows in production builds to amortise.
Cipher-suite enum
v1 = ZEQ-SSL-AES256-GCM-HMAC-SHA256-V1 (only valid value). Adding v2 means
issuing fresh credentials — same row scheme, new cipher_suite field. No
back-compat fallback in the handshake; mixed v1/v2 deployments are not
supported by design.
Parity with @zeq-os/zeq-ssl
deriveSharedSecret, signHandshake, deriveRecordKey, and canonicalJson
in @zeq-os/zeq-ssl/handshake are lifted verbatim from
shared/api-core/src/lib/sslHandshake.ts. If they ever drift, the parity
test src/__tests__/sslHandshake.test.ts will fail on the very first
mismatched-byte HMAC.