الانتقال إلى المحتوى الرئيسي

Wire Protocol

Framing

[ u32_be frame_length ][ u8 frame_type ][ frame_payload ]

Max frame size: 65 535 bytes (mirrors ZSP_MAX_BYTES).

CodeNameDirection
0x01SERVER_HELLOserver → client
0x02CLIENT_HELLOclient → server
0x03SERVER_FINISHEDserver → client
0x10APP_DATAboth
0xfeALERTboth

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:

  1. Lookup peer machine + kid via /api/ssl/verify (or local DB).
  2. Confirm peer_kid fingerprint is valid at zeqond_number.
  3. Confirm |zeqond_number - currentZeqond()| ≤ SSL_CLOCK_SKEW_ZEQONDS = 2.
  4. Derive shared_secret_handshake via deriveSharedSecret(...).
  5. Re-sign the payload, crypto.timingSafeEqual against auth_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.