Security model
How eebus establishes trust: self-signed certificates identified by Subject Key Identifier, TLS 1.2 with mutual authentication, certificate rotation, and the threat model.
Trust is a list of SKIs
There is no certificate authority in EEBUS and no central directory. Each node holds a self-signed ECDSA P-256 certificate, and its identity is the Subject Key Identifier — twenty bytes derived from the public key. Pairing means each side records the other's SKI.
Nobody has to be online, and no vendor can revoke a heat pump's ability to talk to its own energy manager.
The crate enforces it with a custom rustls verifier on both sides. The certificate chain is not validated against a root store — there is no root — but the presented certificate must carry the SKI that was approved, and the SKI must be derived from the key that actually completed the handshake.
…and trust is a number, not a yes
§12.3.2 gives trust three independent categories — user, PKI, second factor — and Table 10 maps every way a key can be accepted onto values in them. Three rules read those numbers, each a SHALL:
| user trust < 8 | "the communication SHALL be aborted" |
| user trust < 32 | §12.5: "the SHIP node PIN SHALL NOT be transmitted" to this peer |
| commissioning over SHIP | needs 32 in user trust or second factor |
TrustLevel::USER_VERIFIED.permits_pin_transmission(); // true — a person said yes
TrustLevel::AUTO_ACCEPT.permits_pin_transmission(); // false — it is only trusted at 8The middle rule is the one with teeth. The QR-code flow of §12.5 exists because a node that auto-accepts needs a second factor, so handing that secret to a peer admitted by auto-accept alone gives it to whoever answered the socket. send_pin_if_permitted refuses and raises Event::PinWithheld.
Categories do not add up: a key verified by auto-accept and by a user is at 32, not 40 — "only the mechanism which offers the highest trust level in this category SHALL be accounted for". PKI trust alone opens nothing, being "no verification mode, as it does not offer the necessary user trust".
Proving the PIN earns a second factor, once. §12.5 lets only the first peer since a factory default reach 32 — which is what permits it to act as a commissioning tool — and caps every later one at 16. TrustStore::award_pin_trust applies it, because only the store outlives the connection; Node::eebus_reset re-opens it.
§12.3.2 closes by inviting a layer above SHIP to use these numbers, and a §14a box deciding whether the thing on the other end may set a grid limit is that layer.
TLS 1.2, mutual, no resumption
- 1.2 only, because SHIP §9 says 1.3 "is not considered in this version".
- Mutual authentication always: both sides present certificates.
- Session resumption is off. §9.6 makes it a SHOULD, but a resumed session skips the certificate exchange, and the certificate is what identifies a SHIP peer.
- The cipher suites are those §9.1 mandates; a peer offering only CBC suites will not connect.
The cryptography provider is the consumer's choice
rustls 0.23 can be backed by ring or by aws-lc-rs, and this crate names neither. cert, tls and runtime require exactly one of the ring and aws-lc-rs features, and a build that names both — or neither — stops with a compile_error! that says why.
rustls' provider is process-global and linking both panics on first use, so the choice belongs to the binary, not a library.
cargo add eebus --features runtime,ring
# or, for a build that must not contain `ring`:
cargo add eebus --no-default-features --features std,runtime,aws-lc-rsNothing here ever reads the process default. The provider is built explicitly, with exactly the three cipher suites and the one key-exchange group §9 permits — the post-quantum hybrids aws-lc-rs offers by default are dropped, because a SHIP node may not negotiate one — so a consumer that has installed its own default keeps it. eebus::tls::CRYPTO_PROVIDER says which backend a running binary got, which is worth a line in the start-up log.
Forgetting a peer
SHIP §12.2.2 states two things about the trust list, and this crate serves both. Persisting it is "STRONGLY RECOMMENDED", because the alternative is asking a user to compare forty hex digits again after every power cut — TrustStore::to_json and from_json are the two calls that make saving it a one-liner, and TrustedPeer carries the name and SHIP ID a person needs to tell one line of the store from the next.
Being able to delete all of it is a SHALL: "at least the SHIP node SHALL offer a possibility to delete all stored foreign public keys (e.g. via factory reset)." That is Node::eebus_reset, and it is what an "EEBUS reset" on a device's user interface has to reach. It returns how many peers were forgotten, and it deliberately does not restore the node's own certificate — §12.1.1 asks a factory reset to bring back the identity printed on the label, and only the device knows where that was stored.
Rotating a certificate
Replacing a certificate changes the node's name, so SHIP §12.1.3 defines a procedure rather than leaving it to chance: the new material is announced with an updateCounter, the peer records it, and the relationship survives. The crate implements it end to end, and the Hub follows the update so an application does not have to.
Pairing
Two paths, both deliberate acts:
- Manual — the user is shown the peer's SKI and approves it. The handshake stays
pendingmeanwhile, prolonging its wait for as long as the peer allows, and the approval completes it in place:Hub::approve, or adding the SKI to theTrustStorethe handshake is watching.Hub::refuseends it withhello: abortedinstead. See Pairing. - Pairing Service — an installer configures a control unit from a QR code carrying a shared secret, and the digest exchange (Pairing Service 1.0.0 §7) establishes trust with nobody asked anything. What is trusted is a certificate fingerprint, which §10.2 makes equivalent to a trusted SKI. A replay guard (§11) refuses a captured exchange, one unit is trusted at a time (§10.3), and a working pairing is not replaceable until it has been unreachable for fifteen minutes (§4.3). See Pairing.
There is no third path. register is never advertised as true and there is no "auto accept" mode, both of which SHIP IG §2.3 forbids.
Threat model
The attacker is on the building's LAN. They can send SHIP frames, malformed JSON, oversized datagrams and bad certificates at any node.
- Parsing — the framing, the codec, the QR payload, the TXT record and a whole datagram through the engine are each a
cargo fuzztarget, run nightly in CI. - Impersonation — requires the peer's private key. A SKI is a hash of the public key; presenting the SKI without the key fails the handshake.
- Replay — SPINE message counters are monotonic per source; the Pairing Service has its own guard.
- A stolen pairing secret — printed, so photographable. The digest covers both certificate fingerprints, so a captured announcement cannot be re-signed for another certificate; the replay guard refuses it verbatim; and §4.3 means a working pairing is not replaceable at all. An attacker with the secret and fifteen minutes of the real unit being offline can pair, which is the model the specification chose.
- A timing oracle on a short secret — the two comparisons that matter, a SHIP PIN and a pairing secret, go through one constant-time equality rather than stopping at the first differing byte. A PIN's whole defence is the escalating penalty of SHIP §13.4.4.3.4, and that penalty counts attempts: an equality that leaks a byte at a time turns eight hex digits into thirty-two guesses, and the penalty never notices.
- Resource exhaustion — every table a peer can grow is bounded; see below.
- A paired peer acting in another's name — a binding or subscription call names its client in the payload, and the engine accepts it only from the device SHIP authenticated as the sender. Without that, one paired peer could release another's binding — the one thing standing between an Energy Guard's limit and a second energy manager — or grant itself a relation under the other's address. Discovery is filed under the header's source for the same reason, not under whatever device the payload claims to describe. And a heartbeat keeps a Controllable System out of its failsafe state only when it comes from the Energy Guard it subscribed to, not from whichever peer notifies one.
What is not defended: an attacker with the private key is the node, and an attacker who can make the user approve their SKI is paired. Both are the intended semantics of the standard.
Resource exhaustion
SHIP and SPINE cap none of these, and the omission is exploitable: a device that dials in a thousand times takes the memory of every node that answers.
| Constant | Bounds | At the limit |
|---|---|---|
DEFAULT_MAX_CONNECTIONS = 16 | connections per hub, handshakes still running included; Hub::set_max_connections changes it | socket dropped before TLS, or connectionClose; TooManyConnections |
MAX_CONNECTIONS_PER_PEER = 2 | connections to one peer, while §12.2.3 arbitrates | as above |
MAX_PENDING_TRUST = 4 | peers waiting on an approval, one slot per SKI | hello: aborted at once, so it retries rather than squats |
MAX_PEERS = 32 | device addresses the engine tracks | the one that has told it least is evicted, with everything filed under it |
MAX_REMOTE_FUNCTIONS = 32 | functions remembered per peer | least recently updated is evicted |
MAX_RELATIONS_PER_PEER = 32 | bindings, and as many subscriptions, from each peer | errorNumber 3, overload |
MAX_DEFERRED_WRITES = 16 | writes waiting on the application | errorNumber 3 |
MAX_LIST_ENTRIES = 128 | entries in one function's list | errorNumber 3 |
MAX_DEVICE_ADDRESS_LEN = 256 | a device address, which must also be printable | datagram discarded |