Protocol and Crypto¶
The Management Protocol channel between C3PO and an agent is an encrypted, authenticated, protobuf-framed line protocol over TCP. C3PO's implementation in c3po/c2/ is byte-for-byte compatible with the firmware's transport.
Silent handshake¶
When an agent connects, identity is established without ever sending the device id in plaintext (c3po/c2/handshake.py):
sequenceDiagram
participant D as Device
participant S as Server (C3PO)
S->>D: base64(server_nonce[32])
D->>S: base64(nonce12 + ChaCha(key, device_id + server_nonce))
Note over S: Try each known key until AEAD decrypt succeeds -> identify device
S->>D: base64(nonce12 + ChaCha(key, "OK:" + server_nonce))
- The server sends a random 32-byte nonce.
- The device replies with its device id and the server nonce, encrypted under its session key.
- The server tries each registered key until one decrypts the frame, which identifies the device and verifies the nonce.
- The server returns an authenticated acknowledgement.
Each step has a 10-second timeout. An eavesdropper without the key cannot tell which device is connecting or correlate sessions.
Per-device crypto context¶
Each device has its own AEAD context (c3po/c2/crypto.py:CryptoContext) derived from the device master_key with HKDF-SHA256 salted by the device id:
| Property | Value |
|---|---|
| Cipher | ChaCha20-Poly1305 (AEAD) |
| Key | 32 bytes, derived per device |
| Nonce | 12 bytes, random per message |
| Tag | 16 bytes |
Wire framing¶
Every message is encrypted and base64-encoded, one frame per line:
A frame that fails authentication is dropped silently; the connection is not torn down.
Protobuf messages¶
The payload is a protobuf message (c2.proto). Commands go to the device, agent messages come back:
message Command {
string device_id = 1;
string command_name = 2;
repeated string argv = 3;
string request_id = 4;
bytes binary_data = 5; // module chunks
uint32 binary_offset = 6;
uint32 binary_total = 7;
}
message AgentMessage {
string device_id = 1;
AgentMsgType type = 2; // INFO, ERROR, DATA, LOG, CMD_RESULT
string source = 3; // module name
string request_id = 4; // correlates to Command.request_id
bytes payload = 5;
bool eof = 6;
}
request_id correlates a command with its responses; eof marks the final frame of a response.
Command dispatch path¶
flowchart LR
CMD["Command"] --> ENC["serialize -> encrypt -> base64"]
ENC -->|"TCP :2626"| RX["device RX"]
RX --> DEC["base64 -> decrypt -> verify"]
DEC --> PB["protobuf decode"]
PB --> DISP["static or dynamic dispatch"]
DISP --> RESP["msg_info / msg_result / ..."]
RESP -->|"reverse path"| C3PO["C3PO router"]
Server internals¶
c3po/c2/server.py:C2Server is an asyncio TCP server. Per connection it performs the handshake, registers the device, and reads newline-delimited frames. Relevant timing:
| Parameter | Value | Meaning |
|---|---|---|
HEARTBEAT_INTERVAL_S |
10 | Timeout check interval |
PROBE_THRESHOLD_S |
1200 (20 min) | Send a keep-alive probe after silence |
DEVICE_TIMEOUT_S |
1500 (25 min) | Mark a device inactive |
| Command stale cleanup | 120 s | Drop in-flight commands with no EOF |
The C2 layer never touches the UI directly; it emits Qt signals (C2Events) that the views subscribe to.