v0.1.17
Two library changes from #84 and #85, both validated against my oven and dryer before release, plus appliance clock synchronization in the reference bridge.
The handshake diagnostic takes an authentication provider (#84)
diagnose_dtls_handshake accepted certificate credentials and nothing else, so a PSK endpoint could be reached but never characterised. It now takes any provider from protocol.auth:
from smartthings_local.protocol.auth import PskAuth
from smartthings_local.protocol.dtls_probe import diagnose_dtls_handshake
result = diagnose_dtls_handshake(host, port,
auth=PskAuth(identity=psk_identity, key=psk_key))auth= is mutually exclusive with cert_pem/key_pem/cert_path/key_path, which keep working unchanged.
A diagnostic never enforces trust, whichever credential reaches it, and that now includes a SamsungServerProfile's pinned server identity. CertificateAuth configures a context to gate on OpenSSL's verdict, and honouring that here would turn an appliance's fatal alert into a local verify failure. The result has to report what the appliance did, so an untrusted chain is classified. DtlsCoapSession gates as it always has.
On my hardware the two credentials separate cleanly. My dryer answers auth=PskAuth with a throwaway key by negotiating ECDHE-PSK and rejecting the identity, rejected with unknown_psk_identity. My oven answers the same call with handshake_failure and an empty flight, the shape of a cipher-list mismatch. Both complete a certificate handshake. Two units and one handshake each, so that says nothing about any other model, but the two alerts do separate a wrong credential from a wrong carrier.
The module CLI carries the same thing as --diagnostic --psk-identity HEX --psk-key HEX. A command line is readable by every process on the host, so pass a throwaway value there.
handshake_msgs is shorter, and truthful. A handshake record after ChangeCipherSpec is encrypted, so its first byte is ciphertext, and the classifier was reading that byte as a message type. Around one byte in 256 collides with a known name, so a completed handshake could report a message the server never sent: over 30 loopback handshakes it invented a Finished once. The list now holds the plaintext flight alone. NewSessionTicket and unknown_psk_identity gained entries, where a bare hs4 and 115 appeared before.
A NUL in a PSK identity is refused with the reason (#85)
PskAuth rejected an identity containing a zero byte with a bare identity cannot contain a NUL byte, which read as this library being fussy. The constraint is OpenSSL's, and the refusal is the safe outcome.
OpenSSL's DTLS 1.2 PSK client callback returns the identity as a C string and takes its strlen. Measured over a real ECDHE-PSK handshake, reading psk_identity out of the ClientKeyExchange: a 16-byte identity with a NUL at byte 8 reaches the wire as 8 bytes, and nothing raises locally. An appliance is then asked to authenticate an identity nobody holds. Both CI dependency sets confirm it, on OpenSSL 4.0.0 and 3.1.0, and the measurement ships as a test.
An appliance has no such limit: RT-OCF and iotivity-lite both carry the identity as counted bytes. DTLS 1.2 offers no length-carrying PSK callback, which leaves such a credential unusable here, roughly 6% of uniformly random 16-byte identities.
PskAuth.validate_identity(identity) exposes the check without a key, raising with the reason, so code holding a credential can refuse it at import time and show why:
try:
PskAuth.validate_identity(psk_identity)
except (TypeError, ValueError) as exc:
print(f"unusable PSK identity: {exc}")Reference bridge: appliance clock synchronization
mqtt_demo can write the bridge host's clock to an appliance that exposes currentTime, which keeps a panel's own timestamps usable on a unit with no route to Samsung's cloud. Each stamp is logged with the zone it was written in. Demo-only, with no library change behind it.
The demo's bind-mount path variable is now SMARTTHINGS_LOCAL_APPDATA_DIR. A generic name could be shadowed by another Compose project's exported value, which would silently mount a different project's appdata.