feat(crypto): add rustls config builders for the signaling channel - #34
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
fpTXT key.What this adds
CertificateManager::server_config()— a rustlsServerConfigpresenting the manager's certificate. Client certificates are not requested; the sender is authenticated by the pairing exchange, not by TLS.client_config_pinned(fingerprint)— aClientConfigthat accepts exactly one certificate. Comparison ignores case and separators, soA3:B2:…,a3b2…anda3-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/PairingConfirmmessages inopenplay-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.rsruns 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-signalingis still plaintext, so this is additive. Re-adds therustls/rustls-pemfiledependencies toopenplay-crypto, which were dropped when the crate had no TLS code.Verified locally on macOS arm64:
fmt --checkclean,clippy --all-targets --all-features -D warningsclean,cargo test --all216 passed / 0 failed.