Repository navigation
PBS-31 Implement support for SSL/TLS for the listener - #161
Open
kamil-holubicki wants to merge 3 commits into
Open
kamil-holubicki wants to merge 3 commits into
kamil-holubicki wants to merge 3 commits into
Conversation
https://perconadev.atlassian.net/browse/PBS-33 Before this change, the Binlog Server's built-in caching_sha2_password authentication could only issue a fast-auth success ('0x03') against the configured plaintext password. A client that ran 'mysql --get-server-public-key' (or '--server-public-key-path=<pem>') on a cache miss would trigger a '0x04' full-authentication challenge that PBS could neither issue nor complete: it had no RSA key pair, no PEM send, and no OAEP decrypt. Implement caching_sha2_password full authentication end to end. The new 'minimysql::caching_sha2_password_authenticator' encapsulates the whole plugin sub-protocol as a state machine: fresh 20-byte greeting salt (matching Percona Server's 'generate_user_salt()' in 'mysys/crypt_genhash_impl.cc'), AuthMethodSwitch data formatting, '0x03'/'0x04' selection, '0x02' public-key-request handling, PEM send, RSA-OAEP decrypt with the caching_sha2_password XOR-with-salt-repeated wrapping, and a cleartext-over-secure branch reserved for the day TLS lands (PBS-31). 'minimysql::connection_context' delegates every auth step to the authenticator through a small 'auth_packet_encoder' interface so the frame codec stays in one place. 'minimysql::network_service::session()' was reworked around an auth-method-agnostic loop. It sends the greeting, optionally emits an AuthMethodSwitch when 'needs_auth_method_switch()' says the client picked a different plugin (re-using the fresh-salt helper), then calls 'begin_authentication()' and cycles through 'take_authentication_outbound_frames()' / 'submit_authentication_frame()' until 'authentication_state()' is no longer 'in_progress'. The same loop drives fast-auth ('0x03'), full-auth-over-RSA ('0x04' -> PEM/'0x02' -> ciphertext), and future cleartext-over-TLS ('0x04' -> cleartext) without special casing. PBS has no SHA-2 digest cache, so every session is treated as a first-login / cache miss and 'begin_authentication()' always enqueues '0x04'. The RSA / PEM handshake is therefore the default and only path exercised in production. The fast-auth helpers ('verify_greeting_scramble()', 'enqueue_fast_auth_success()', and the 'scramble()' static) are deliberately kept in the authenticator with a documented pointer to where a future SHA-2 cache would re-enable the shortcut, gated on a cache lookup rather than a live plaintext compare. Added two OpenSSL RAII wrappers under 'src/opensslpp/', following the existing 'cipher_context' / 'crypto_rng' convention (opaque 'unique_ptr<void, impl_deleter>' pimpl, 'native_helper' in the '.cpp', 'core_error' on failure, 'util::byte_span' IO, no '<openssl/*>' in headers): * 'opensslpp::digest_context' plus 'opensslpp::digest_code_type' - 'EVP_MD_CTX' wrapper (SHA-256 today) with a one-shot 'calculate()' helper; used by 'scramble()'. * 'opensslpp::rsa_private_key' - 'EVP_PKEY' loaded from an in-memory PEM buffer, exposing 'get_cipher_length_in_bytes()' and 'decrypt_oaep()' (PKCS#1 v2 OAEP with OpenSSL default SHA-1 MGF). After this change, no '.cpp' outside 'src/opensslpp/' includes an '<openssl/*>' header directly; both 'connection_context.cpp' (random salt) and 'caching_sha2_password_authenticator.cpp' (RSA decrypt, SHA-256 digest) go through the wrappers. The authenticator unit test in 'tests/CMakeLists.txt' links 'binsrv::lib_opensslpp' instead of the raw 'OpenSSL::Crypto'. Server RSA key pair sourced from an optional 'pbs_listener' block in the JSON main_config ('binsrv::pbs_listener_config' with fields 'rsa_public_key_path' and 'rsa_private_key_path'). 'pull_operation' reads the block when present and forwards the two paths through 'minimysql::network_service' -> 'connection_context' -> authenticator as 'std::string_view'; when the block is absent it forwards empty views. The authenticator loads the pair from disk when both paths are non-empty, accepts both-empty at construction (no keys loaded, subsequent full-auth attempts fail per-session), and rejects one-sided configuration with a clear error. 'pbs_listener_config::validate()' additionally rejects one-sided configuration at the JSON-config layer with a matching message. There are no embedded default RSA keys - the operator provides them. Unit test ('tests/caching_sha2_password_authenticator_test.cpp') covers 'scramble()' correctness, the RSA full-auth success and failure paths (both via '0x02' PEM request and via a pre-loaded local PEM), cleartext-over-secure success, one-sided-path rejection, both-empty-path acceptance at construction, and locks in the always-full-auth policy by asserting that a matching greeting scramble still drives '0x04'. Tests that need real keys generate them on the fly with the new 'write_temp_rsa_key_pair()' helper and write them to '/tmp/pbs_test_server_rsa_*.pem' - no static test-only PEM constants live in-source. MTR test ('mtr/binlog_streaming/t/caching_sha2_full_auth.test' with the server RSA key pair shipped as static assets at 'mtr/binlog_streaming/std_data/caching_sha2_full_auth_{pubkey,privkey} .pem') covers three end-to-end scenarios inside one binsrv lifetime - correct password + '--get-server-public-key', correct password + '--server-public-key-path=<pem>', and wrong password + PEM fetch (asserted '--error 1'). The test feeds both key paths into the generated binsrv JSON config through the new '$binsrv_pbs_listener_rsa_public_key_path' / '$binsrv_pbs_listener_rsa_private_key_path' MTR vars, which 'generate_binsrv_config.inc' turns into the 'pbs_listener' block. After graceful SIGTERM the stdout log is grepped for 'client authentication succeeded for rpl' and 'client authentication failed for rpl' markers so each case is confirmed both by mysql client exit code and by server-side logging. 'mtr/binlog_streaming/t/auth_method_switch.test' (PBS-32) was updated to pass '--get-server-public-key' on both mysql invocations, because the always-full-auth policy requires clients to complete '0x04' on plain TCP. Once PBS-31 wires TLS these flags can be dropped in favour of the secure-transport cleartext branch. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
https://perconadev.atlassian.net/browse/PBS-31 Pure refactor - no functional change. Prepares the minimysql session layer for the upcoming TLS-listener work by making the post-greeting session body and its authentication sub-loop generic over the boost::asio socket type, so the same code drives both a plain 'tcp::socket' session and (in the follow-up commit) a 'ssl::stream<tcp::socket>' session after a successful TLS handshake. Extracted two template functions from what used to be a monolithic 'session()' coroutine in 'src/minimysql/network_service.cpp': * 'template <typename Socket> perform_authentication(Socket &socket, connection_context &, endpoint &, buffer &) -> awaitable<bool>' - runs the caching_sha2_password authentication loop (AuthMethodSwitch on plugin mismatch, then begin_authentication / take_outbound_frames / read_client_frame / submit_authentication_frame until 'in_progress' clears), and emits the final OK or access_denied. Returns true on success, false on rejection. * 'template <typename Socket> session_body(Socket &socket, connection_context &, endpoint &, buffer &)' - runs perform_authentication and, on success, the command loop (query / ping / binlog_dump / quit dispatch). Everything that happens on the connection after the initial handshake exchange. 'session()' itself continues to take a 'tcp::socket' by value (no signature change) and delegates its post-greeting half to 'session_body(socket, ...)'. Its behaviour is unchanged; only the factoring changed. Made 'minimysql::network_io_operations' header-only for the same reason - the free functions 'async_read_mysql_frame' and 'async_write_mysql_frame' are now templates parametrized on the stream type, so a caller passing a 'tcp::socket' or a 'ssl::stream<tcp::socket>' picks up the same implementation without an extra virtual dispatch: * Moved the bodies from 'src/minimysql/network_io_operations.cpp' into 'src/minimysql/network_io_operations.hpp' as template definitions. * Deleted 'src/minimysql/network_io_operations.cpp'. * Removed the corresponding source entry from 'minimysql_source_files' in the top-level 'CMakeLists.txt'. Behaviour verification: 'binlog_server' compiles clean, all 9 ctest targets pass (byte_span_encoding_test, uuid_test, tag_test, gtid_test, gtid_set_test, event_test, cipher_context_test, crypto_rnd_test, caching_sha2_password_authenticator_test). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
kamil-holubicki
force-pushed
the
PBS-31
branch
3 times, most recently
from
September 15, 2026 18:17
fa3d221 to
ca26902
Compare
https://perconadev.atlassian.net/browse/PBS-31 Sits on top of the preceding "PBS-31 prep: refactor minimysql session for socket-generic templates" commit, which turned the post-greeting session body and its authentication sub-loop into templates parametrized on the boost::asio socket type. This commit adds the actual TLS functionality on that foundation: an optional TLS-enabled listener whose sessions upgrade the transport in place and then reuse the same 'session_body' template that a plaintext session uses. Problem: Before this pair of commits the Binlog Server accepted only plaintext client connections on its MySQL-protocol listener. It did not advertise CLIENT_SSL, so mysql clients running with '--ssl-mode=REQUIRED' could not connect and every authentication ran in the clear. The "cleartext-password-after-0x04 is safe only on a secure transport" invariant of caching_sha2_password kept the fast path disabled unconditionally, diverging from Percona Server behaviour. Solution: Add optional per-listener TLS, matching a TLS-configured Percona Server node with 'require_secure_transport=OFF'. The listener is TLS-enabled when the 'pbs_listener.ssl_cert_path' and 'pbs_listener.ssl_key_path' fields of the main_config JSON are both non-empty (config validation rejects one-sided configuration). TLS configuration is captured in a shared 'minimysql::ssl_acceptor_context' owned by 'network_service'; the connection_context advertises CLIENT_SSL in its greeting only when the acceptor is present. TLSv1.2 and TLSv1.3 are the accepted protocol versions; older SSL/TLS versions are explicitly disabled. Client-certificate verification is disabled (server-cert only), matching the mysql CLI default. Per session, the transport starts on the raw TCP socket, and the first client greeting drives the branch. A 'Protocol::SSLRequest' against a TLS-configured listener triggers a TLS handshake ('perform_ssl_handshake' with the same timeout used for the rest of authentication) and switches all subsequent I/O to the encrypted stream via 'session_body' called on a 'boost::asio::ssl::stream<tcp::socket>'; the same intent against a plaintext-only listener drops the connection with a diagnostic and no error frame, matching Percona Server's "if (!context.have_ssl()) return packet_error;" in 'sql/auth/sql_authentication.cc'. Once TLS is established 'context.mark_transport_secure()' flips 'connection_is_secure()' to true, which unlocks the caching_sha2_password cleartext-after-0x04 fast path for TLS clients as in Percona Server. Because the previous commit already made 'session_body' and 'perform_authentication' templates over the socket type, the TLS branch here reuses exactly the same authentication and command-loop implementation as the plaintext branch - the only per-branch code is the SSLRequest check, the handshake call, and the 'mark_transport_secure' notification. OpenSSL wrapper: 'opensslpp::verify_ssl_ctx_private_key_matches_certificate' runs the 'SSL_CTX_check_private_key()' check on the underlying SSL_CTX handle and throws 'opensslpp::core_error' with a caller-supplied prefix plus the drained OpenSSL error queue, keeping the raw OpenSSL calls out of 'minimysql::ssl_acceptor_context'. New unit tests: 'connection_context_ssl_test.cpp' covers the CLIENT_SSL advertisement predicate, the client_requested_ssl / is_sslrequest_greeting predicates, and the mark_transport_secure transition; 'ssl_acceptor_context_test.cpp' covers TLS-context construction (successful pair, one-sided, mismatched pair, missing files) using MySQL's test-suite std_data cert / key files. MTR test: 'mtr/binlog_streaming/t/ssl_listener.test' brings the whole thing up end to end - spawns 'binlog_server pull' with a 'pbs_listener' block pointing at std_data server-cert / server-key, runs one '--ssl-mode=REQUIRED' mysql client (TLS handshake + cleartext-over-0x04 auth) and one '--ssl-mode=DISABLED --get-server-public-key' client (plain-TCP RSA auth) against the same listener, then greps the stdout log for "TLS handshake completed", "(over TLS)" and "(over plain TCP)" markers. Handshake success over TLSv1.2 and TLSv1.3, and the Percona-style rejection of SSL-requesting clients against a plaintext-only listener, were verified end-to-end. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
kamil-holubicki
force-pushed
the
PBS-31
branch
from
September 15, 2026 18:45
ca26902 to
c3fad80
Compare
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.
No description provided.