Skip to content

feat(crypto): add rustls config builders for the signaling channel - #34

Merged
snadahalli merged 2 commits into
masterfrom
feat/signaling-tls
Aug 26, 2026
Merged

feat(crypto): add rustls config builders for the signaling channel#34
snadahalli merged 2 commits into
masterfrom
feat/signaling-tls

Conversation

@snadahalli

Copy link
Copy Markdown
Owner

The receiver's certificate is self-signed and its address is a bare LAN IP, so neither end can use webpki's usual path: there is no CA to chain to and no hostname to match. Instead the sender pins the SHA-256 fingerprint of the receiver's certificate, which mDNS already advertises in the fp TXT key.

What this adds

  • CertificateManager::server_config() — a rustls ServerConfig presenting the manager's certificate. Client certificates are not requested; the sender is authenticated by the pairing exchange, not by TLS.
  • client_config_pinned(fingerprint) — a ClientConfig that accepts exactly one certificate. Comparison ignores case and separators, so A3:B2:…, a3b2… and a3-b2-… are equivalent.
  • PinnedCertVerifier, which replaces identity checking only. Signature verification still goes through the crypto provider, so a pinned certificate that cannot sign the handshake is still rejected.

What pinning does not buy

Pinning here is deliberately not authentication, and the module docs say so at length. The TXT record is unauthenticated, so an attacker on the same LAN can advertise a receiver with their own fingerprint, and a sender that has never seen the real one will pin the attacker's certificate.

It gives confidentiality against a passive eavesdropper, and detects a substituted certificate on any later connection. Authenticating the receiver needs the user to confirm a code shown on both screens — the PairingChallenge / PairingConfirm messages in openplay-protocol, which are not wired up yet.

Tests

Nine tests. The unit tests drive the verifier directly, since that is the decision pinning actually makes. tests/tls_handshake_test.rs runs a real TLS handshake over a loopback socket for the accept and reject cases, plus one asserting that a stock verifier with an empty trust store rejects the same certificate — which is what makes pinning necessary rather than merely convenient.

Scope

Nothing constructs this yet; openplay-signaling is still plaintext, so this is additive. Re-adds the rustls / rustls-pemfile dependencies to openplay-crypto, which were dropped when the crate had no TLS code.

Verified locally on macOS arm64: fmt --check clean, clippy --all-targets --all-features -D warnings clean, cargo test --all 216 passed / 0 failed.

The receiver's certificate is self-signed and its address is a bare LAN
IP, so neither end can use webpki's usual path: there is no CA to chain
to and no hostname to match. Instead the sender pins the SHA-256
fingerprint of the receiver's certificate, which mDNS already advertises
in the `fp` TXT key.

Adds `CertificateManager::server_config()` and `client_config_pinned()`,
plus a `PinnedCertVerifier` that replaces identity checking only —
signature verification still goes through the crypto provider, so a
pinned certificate that cannot sign the handshake is still rejected.

Pinning is deliberately not authentication. The TXT record is
unauthenticated, so an attacker on the same LAN can advertise their own
fingerprint and a sender that has never seen the real one will pin it.
This buys confidentiality against a passive eavesdropper and detects a
substituted certificate on any later connection; authenticating the
receiver needs the `PairingChallenge`/`PairingConfirm` exchange in
openplay-protocol, which is not wired up yet. The module docs say so.

Tests drive the verifier directly and also run a real TLS handshake over
a loopback socket, including a case asserting that a stock verifier with
an empty trust store rejects the same certificate — which is what makes
pinning necessary rather than merely convenient.

Nothing constructs this yet; `openplay-signaling` is still plaintext.
CLAUDE.md's project-status section stated that `openplay-crypto` no longer
depends on `rustls`, which this PR makes false. The neighbouring claim in the
same line — that `CertificateManager` is never constructed outside its own
tests — stays true, since the config builders added here have no callers.

Also gives `tls.rs` a row in the crypto status table, noting that pinning is
not peer authentication so the table cannot be read as claiming the signaling
channel is authenticated.
@snadahalli
snadahalli merged commit 5443c3d into master Aug 26, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant