eebus

Home / Docs / Security model

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 SHIPneeds 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 8

The 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-rs

Nothing 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 pending meanwhile, prolonging its wait for as long as the peer allows, and the approval completes it in place: Hub::approve, or adding the SKI to the TrustStore the handshake is watching. Hub::refuse ends it with hello: aborted instead. 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 fuzz target, 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.

ConstantBoundsAt the limit
DEFAULT_MAX_CONNECTIONS = 16connections per hub, handshakes still running included; Hub::set_max_connections changes itsocket dropped before TLS, or connectionClose; TooManyConnections
MAX_CONNECTIONS_PER_PEER = 2connections to one peer, while §12.2.3 arbitratesas above
MAX_PENDING_TRUST = 4peers waiting on an approval, one slot per SKIhello: aborted at once, so it retries rather than squats
MAX_PEERS = 32device addresses the engine tracksthe one that has told it least is evicted, with everything filed under it
MAX_REMOTE_FUNCTIONS = 32functions remembered per peerleast recently updated is evicted
MAX_RELATIONS_PER_PEER = 32bindings, and as many subscriptions, from each peererrorNumber 3, overload
MAX_DEFERRED_WRITES = 16writes waiting on the applicationerrorNumber 3
MAX_LIST_ENTRIES = 128entries in one function's listerrorNumber 3
MAX_DEVICE_ADDRESS_LEN = 256a device address, which must also be printabledatagram discarded