tls: mutual TLS on the TCP side, for both OpenSSL and kTLS - #185
Merged
Conversation
ioxide could require client certificates over QUIC (#180) and not over TCP. The TCP server had no peer verification at all: no SSL_CTX_set_verify, no load_verify_locations, and nothing in TlsOptions to configure one. An https:// port could prove who the SERVER was and never ask the other question. TlsOptions gains ClientCaPath, ClientCaPem and RequireClientCertificate. Anchors from a file go through load_verify_locations; anchors from memory are parsed into the context's own store, mirroring how the server's certificate already comes from either. With a file we also send the CA names in the CertificateRequest, so a client holding several certificates picks the one this server accepts instead of guessing - a failure to enumerate them costs the hint, not the verification. RequireClientCertificate is the interesting distinction. Off, a client with no certificate still connects and TlsSession.PeerSubject is null, so a handler can serve a public route and refuse a protected one. On, it is refused at the handshake. Either way a certificate that IS offered is verified: "optional" governs presenting nothing, not presenting anything. PeerSubject is the other half. Enforcing an identity is not much use if nothing can read one, and a value there means the chain VALIDATED - an invalid one already failed the handshake, so a connection that reaches a handler never carries a merely-offered certificate. Orthogonal to kTLS, which is why both samples exist and differ by one line. The certificate is exchanged and verified during the handshake, which OpenSSL performs either way; the kernel only takes over record crypto afterwards. MtlsKtlsPipes is MtlsOpenSslPipes with KernelTx = true and nothing else. Nothing changes for anyone not using it: every path here sits behind "are anchors configured", and a server without them performs the handshake it performed before. Verified end to end with curl against the sample: valid client cert -> 200, X-Client: /CN=alice cert from a rogue CA -> tlsv1 alert unknown ca no cert, optional -> 403 from the handler no cert, required -> tlsv13 alert certificate required 11 new tests, driven with SslStream so a pass means agreeing with an independent implementation. Build clean; 25 unit, 23 tls and 28 http green. The three kTLS cases skip without `sudo modprobe tls`.
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.
ioxide could require client certificates over QUIC (#180) and not over TCP. The TCP server had no peer verification at all - no
SSL_CTX_set_verify, noload_verify_locations, and nothing inTlsOptionsto configure one. Anhttps://port could prove who the server was and had no way to ask the other question.Both TLS paths now support it.
SslStreamoverTcpConnectionStreamalways could, because that is the BCL's implementation and the caller configures it - this closes the two paths ioxide owns.The options
Anchors from a file go through
load_verify_locations; anchors from memory are parsed into the context's own store, mirroring how the server's own certificate already comes from either source. With a file we additionally send the CA names in the CertificateRequest, so a client holding several certificates picks the one this server accepts rather than guessing - failing to enumerate them costs the hint, not the verification.Two things worth reviewing
RequireClientCertificategoverns presenting NOTHING, not presenting ANYTHING. Off, a client with no certificate connects andPeerSubjectis null, so a handler can serve a public route and refuse a protected one. On, that client is refused at the handshake. Either way a certificate that is offered gets verified - a rogue one is rejected in both modes. There is a test pinning exactly that, because the opposite reading would make the option mean "trust anything".TlsSession.PeerSubjectis the half that makes it useful. Enforcing an identity is not much use if nothing can read one. A value there means the chain VALIDATED: an invalid chain already failed the handshake, so a connection reaching a handler never carries a merely-offered certificate.mTLS and kTLS are orthogonal
Which is why there are two samples and they differ by one line. The certificate is exchanged and verified during the handshake, which OpenSSL performs either way; the kernel only takes over record crypto afterwards.
Playground/Tls/MtlsKtlsPipesisPlayground/Tls/MtlsOpenSslPipeswithKernelTx = trueand nothing else changed -diffthem and that is all there is. Both serve throughTlsConnectionDualPipe.Every mTLS test runs on both paths for the same reason: asserting it is cheaper than reasoning about it.
Nothing changes for anyone not using it
Every new path sits behind "are anchors configured". A server without them performs the handshake it performed before, and there is a test asserting that a client which HAS a certificate is never asked for one when no CA is set.
Verified end to end
Both samples, driven with curl and real certificates:
200,X-Client: /CN=alice200,X-Client: /CN=alicetlsv1 alert unknown catlsv1 alert unknown ca403from the handler403from the handlertlsv13 alert certificate requiredThe kTLS body decrypts to the full 8192 bytes, so kernel-produced records are intact after a handshake that exchanged a client certificate.
Tests
11 new cases, driven with
SslStreamas the client so a pass means agreeing with an independent implementation rather than only with ourselves: verified identity reaching the handler, refusal with none when required, refusal of another CA's certificate, the optional-mode matrix, in-memory anchors matching file anchors, the no-CA baseline, and three configuration mistakes that must fail at startup rather than at some later handshake.Build clean. 29 TLS tests green with 0 skipped (
sudo modprobe tlsloaded), plus 25 unit and 28 http.