One wire format. Three implementations that agree.
Swift, Go and Kotlin each implement the protocol independently and produce identical bytes against the same frozen vectors. A fourth client can be written from the document alone — if you need to read our code, that is a documentation bug.
Swift126tests, unit and end-to-end Go52golden vectors, byte-exact Kotlin70vectors, plus a JVM session smoke
The whole wire is four ideas.
Frames carry either canonical JSON or file chunks. Every control message wears the same envelope, identity is an Ed25519 key, and TLS is pinned to it.
Framing
Type-length-value, two kinds. An unknown kind is skipped by its length and logged, never fatal — that is the forward-compatibility rule.
frame = kind:u8 | length:u32be | payload
0x01 control → canonical JSON (≤ 1 MiB)
0x02 chunk → file_id uuid(16)
| seq u64be
| flags u8 (bit0 = last)
| data (≤ 2 MiB)
Envelope
Frozen shape, sorted keys, no added whitespace. Decoders must accept any valid JSON; encoders must emit the canonical form the vectors pin.
{"version":"0.2","type":"HELLO",
"session_id":"<opaque>","seq":0,
"payload":{…}}
Identity
The device ID is the hash of its public key, so an identity cannot be claimed without the key. The TLS key is signed into the same identity, which is what defeats a TLS-terminating middle-man.
identity = hex(SHA256(ed25519_pubkey))
tls_pin = SHA256(cert pubkey, X9.63)
binding_sig = Ed25519_sign(
"conduit-tls-binding-v1"
‖ tls_pin)
Pairing
Both sides derive the same six digits and word pair from both public keys, and a human confirms on both screens. Substituted keys produce different codes; a substituted TLS key fails the binding check before anyone is asked anything.
material = SHA256("conduit-pairing-v1"
‖ min(pubA,pubB)
‖ max(pubA,pubB))
code = u32be(material[0..4]) % 1e6
words = wordlist[material[4]],
wordlist[material[5]]
Verification is pinning only. No chain is ever consulted.
There is no plaintext path, including in debug builds. Certificates are self-signed carriers for a pinned key, and an unpinned key is accepted only while pairing mode is on — such a connection may carry nothing but the ceremony.
A partial client is a legitimate client.
Capabilities are negotiated strings, so a peer that never advertises
screen is simply never asked. You implement the tier you
want and stop.
| Tier | You implement | You get |
|---|---|---|
| 1 | Framing, envelope, canonical JSON | Vectors pass; nothing talks yet |
| 2 | Ed25519 identity, mutual TLS 1.3 with pinning, pairing derivation | You can pair and hold a session |
| 3 | HELLO negotiation, ping and RTT | A live peer that shows up and stays up |
| 4 | Clipboard and file transfer | The weekend client — genuinely useful |
| 5 | Screen viewer, or input sender | Feature-parity territory |
The gate is the vectors, and they are append-only.
Four files pin the bytes: messages, chunk frames, screen frames, and the pairing derivation with the SHA-256 of the frozen 256-word list. Bring any client — if it reproduces them and completes a handshake, it interoperates.
There is no blessed SDK and no registration. Changes travel as a pull request against the spec, all three implementations, and new vectors.