Lift certificate pinning into the connector contract

fluksio.flow.connector_tls: trust the certificate a device presented on
first contact, kept in ConnectorNode.store, and name the new fingerprint
when another one is refused. Additive, so no contract version bump; hue
and wfrac use it.
This commit is contained in:
2026-10-07 09:48:24 +02:00
parent e9c3d0944d
commit cd6f8fec55
3 changed files with 139 additions and 1 deletions
+28
View File
@@ -240,6 +240,34 @@ curl -X DELETE $FLUKSIO/connectors/hue/state -H "Authorization: Bearer $TOKEN"
So a connector has to cope with a key disappearing: clearing is how somebody
tells it to start over.
### Pinning a device's certificate
A device on the local network presents a certificate no system trusts and that
names a serial or a MAC rather than its address, so `verify=True` fails and
`verify=False` trusts anyone. `fluksio.flow.connector_tls` trusts the
certificate the device presented on first contact instead, kept in `self.store`
under `cert:<address>`:
```python
from fluksio.flow.connector_tls import context_for, pinned_certificate, raise_if_changed
address = f"{self.config.host}:{self.config.port}"
pem = pinned_certificate(self.store, address, self.config.timeout) # blocking
try:
with httpx.Client(verify=context_for(pem)) as client:
...
except httpx.ConnectError as exc:
raise_if_changed(exc, pem, address, self.config.timeout, "my_node_type")
raise
```
- First contact sends a TLS hello and nothing else; a device that does not
complete one raises `OSError` and nothing is pinned.
- Any other certificate is refused in the handshake, before a key or a command
is sent. `raise_if_changed` turns that refusal into `CertificateChanged`,
which names the new SHA-256 and the key to clear, under the node type given.
- Clearing the key accepts whatever the device presents next.
## Lifecycle
```python