Home / Docs / SHIP: the transport
SHIP: the transport
SHIP explained: mDNS-SD discovery, TLS 1.2 with mutual authentication, the five-phase handshake, trust by Subject Key Identifier, and double-connection resolution.
SHIP — Smart Home IP — gets two devices from "on the same LAN" to "exchanging SPINE datagrams". It covers discovery, transport security, trust establishment and framing, and it is where most interoperability failures happen.
Finding a peer
A SHIP node announces itself over mDNS-SD as _ship._tcp.local, with a TXT record carrying its identity and state:
id=Demo-HeatPump-123456 the SHIP ID
ski=43c9da85a18329d6… the Subject Key Identifier, 40 hex characters
path=/ship/ the WebSocket path
register=false whether it is inviting pairingregister is never true in this crate: SHIP IG §2.3 forbids advertising open registration, and forbids an "auto accept" mode along with it. Pairing is a deliberate act.
With the mdns feature the crate both announces and browses; without it you supply the address yourself.
Identity is the certificate
There is no username, no central authority and no PKI. A node holds a self-signed ECDSA P-256 certificate, and its name is the Subject Key Identifier — twenty bytes derived from the public key, written as forty hex characters. Two devices are paired when each has recorded the other's SKI.
- Trust is per-node and symmetric. There is nobody to revoke it centrally.
- A certificate cannot be replaced casually — replacing it changes the node's name. SHIP §12.1.3 defines a rotation procedure with an
updateCounterfor exactly this reason. - TLS session resumption is off, because it skips the certificate exchange — see Security.
TLS 1.2, and only 1.2
SHIP §9 says TLS 1.3 "is not considered in this version", so the crate offers 1.2 with mutual authentication and the cipher suites §9.1 mandates. A peer offering only CBC suites will not connect.
The cert feature generates conforming node certificates. rcgen writes the Subject Key Identifier extension only for certificate authorities, and a SHIP node certificate is a leaf. Without the extension written by hand the field §12.2 expects is simply absent, and a peer cannot identify the node at all.
The handshake
Once the WebSocket is up, five phases run before any SPINE datagram may cross (§13.4.3–13.4.8):
init— a single byte in each direction.hello— each side declares whether it isready,pendingoraborted, with a waiting time. An untrusted peer stayspending; the pending node sends prolongation requests, and each grant restarts the granter's Wait-For-Ready timer (§13.4.4.1.3). TheHubreports it asTrustRequested— Pairing.protocolHandshake— agree the SHIP version and the message format. JSON-UTF8 in practice; the specification allows others. §13.4.4.2.2 requires support for every version from 1.0 up to a node's own maximum, so two peers settle on the lower.Node::handshake_configsets the ceiling —ShipVersion::V1_0pins a device to the certification minimum — andShipConnection::ship_version,Hub::ship_versionandHubEvent::Connectedreport what came of it. It matters: 1.0 has noaccessMethods.id, so a 1.0 peer cannot be dialled back.pinVerification— optional, with penalties for wrong attempts.accessMethods— exchange identities.accessMethods.idis populated, which SHIP 1.1.0 makes mandatory and which a peer needs in order to dial back in the other direction.
sequenceDiagram
autonumber
participant C as Client
participant S as Server
Note over C,S: TCP · TLS 1.2 mutual auth · WebSocket upgrade
C->>S: init (one byte)
S->>C: init (one byte)
rect rgb(230, 245, 239)
Note over C,S: hello — trust is settled here
C->>S: connectionHello: pending
S->>C: connectionHello: pending
loop while a person decides, capped at T_hello_init
C->>S: prolongationRequest
S->>C: granted — restarts the granter's own timer
end
C->>S: connectionHello: ready
S->>C: connectionHello: ready
end
C->>S: protocolHandshake: announceMax 1.1, JSON-UTF8
S->>C: protocolHandshake: select 1.1, JSON-UTF8
C->>S: protocolHandshake: select (echoed back)
C->>S: connectionPinState: none
S->>C: connectionPinState: none
C->>S: accessMethodsRequest
S->>C: accessMethods: id
Note over C,S: data exchange — SPINE datagrams may now crossThe close handshake (§13.4.8) is symmetric and equally required; dropping the socket is not a close.
handshake.handle_message(&msg, now); // a frame arrived
handshake.poll_transmit(); // frames to send
handshake.poll_timeout(); // when the next SHIP timer fires
handshake.poll_event(); // "trust needed", "ready", "closed"The Pairing Service
Beyond manual SKI approval, the Pairing Service 1.0.0 lets an installer configure a control unit from a QR code and have trust established with nobody asked anything. It is behind the pairing feature, on by default, and both roles are implemented: devA, the household device that evaluates requests (§9), and devZ, the control unit that announces them (§8). The digest is an HMAC-SHA256 over the TXT record's fields in the order §7 fixes, keyed by the printed secret and a fresh nonce, and a replay guard (§11) refuses a captured announcement twice.
What is trusted is a certificate fingerprint rather than a SKI: §10.2 has it checked at the TLS handshake and treated as equivalent, and §10.3 allows one control unit at a time. Trust lives in the same store as SHIP's (§10.4). The timing rules are the subtle part — when a request is on the air (§4.2) and when a node will look at one at all (§4.3) — and both are worked through in Pairing; the two simulators do the whole exchange over a real network.
Fingerprint is the SHA-256 of the DER certificate in the 64-uppercase-hex wire form, and it is strict about case: the digest covers the text as sent, so a lowercase value would mean verifying a message the sender never signed.
Keys the QR payload grammar gains later are skipped on the way in (SRIP-310/15) and written back out unchanged, so a tool that re-renders a code — onto a screen after a certificate update, say — does not strip fields it happens not to understand.
Two nodes that dial each other at once
If both peers discover each other simultaneously, both dial, and two connections exist where one should. SHIP §12.2.3 resolves it deterministically: the node with the larger SKI keeps the most recent connection and drops the older; the node with the smaller SKI waits three seconds and then pings to confirm which survived.
Each node judges "most recent" by its own clock, so both can close and leave none (enbility, 2025); ship-go keeps the initiator's connection instead. This crate follows §12.2.3 and bounds the hazard: one ping round, then a decision, then a redial. That also interoperates with ship-go without a compatibility switch.
Framing
Each SHIP message is one binary WebSocket message whose first byte names its type (§13.4.3); text frames are refused with 1003 (§10.3). The framing layer is one of the five fuzz targets, because it is the first thing an attacker on the LAN reaches.