From cb10bc46b8d34615b8644fc6bd2b500ae5952a9d Mon Sep 17 00:00:00 2001 From: Michael Boquard Date: Mon, 1 Jul 2024 20:58:08 -0400 Subject: [PATCH 1/6] configuration: Add support for P12 files PKCS#12 files (or PFX files) can be used to hold various cryptographic keys and certificates in a secure way. These files are encrypted using a user provided password and allows for secure transmission of keys and certs from an issuer to an endpoint. This commit allows for users of Redpanda to use a P12 file (with a supplied password for decryption) instead of a plain-text key and cert when setting up TLS configurations. Signed-off-by: Michael Boquard --- src/v/config/rjson_serialization.cc | 20 +++- src/v/config/tests/tls_config_convert_test.cc | 93 +++++++++++++++++-- src/v/config/tls_config.cc | 83 ++++++++++++++--- src/v/config/tls_config.h | 19 +++- 4 files changed, 184 insertions(+), 31 deletions(-) diff --git a/src/v/config/rjson_serialization.cc b/src/v/config/rjson_serialization.cc index c005d3220f157..932fd86f71714 100644 --- a/src/v/config/rjson_serialization.cc +++ b/src/v/config/rjson_serialization.cc @@ -9,6 +9,7 @@ #include "config/rjson_serialization.h" +#include "config/tls_config.h" #include "config/types.h" namespace json { @@ -51,11 +52,20 @@ void rjson_serialize_impl( w.Bool(v.get_require_client_auth()); if (v.get_key_cert_files()) { - w.Key("key_file"); - w.String(v.get_key_cert_files()->key_file.c_str()); - - w.Key("cert_file"); - w.String(v.get_key_cert_files()->cert_file.c_str()); + ss::visit( + v.get_key_cert_files().value(), + [&w](const config::key_cert& k) { + w.Key("key_file"); + w.String(k.key_file.c_str()); + w.Key("cert_file"); + w.String(k.cert_file.c_str()); + }, + [&w](const config::p12_container& p) { + w.Key("p12_file"); + w.String(p.p12_path.c_str()); + w.Key("p12_password"); + w.String("REDACTED"); + }); } if (v.get_truststore_file()) { diff --git a/src/v/config/tests/tls_config_convert_test.cc b/src/v/config/tests/tls_config_convert_test.cc index 3ba62e4186ec8..9fc36f462fe89 100644 --- a/src/v/config/tests/tls_config_convert_test.cc +++ b/src/v/config/tests/tls_config_convert_test.cc @@ -8,11 +8,13 @@ // by the Apache License, Version 2.0 #include "config/configuration.h" +#include "config/tls_config.h" #include "utils/to_string.h" #include #include +#include #include config::tls_config read_from_yaml(ss::sstring yaml_string) { @@ -40,10 +42,10 @@ SEASTAR_THREAD_TEST_CASE(test_decode_full_abs_path) { " require_client_auth: true\n"; auto full_cfg = read_from_yaml(with_values); BOOST_TEST(full_cfg.is_enabled()); - BOOST_TEST( - (*full_cfg.get_key_cert_files()).key_file == "/fake/key_file.key"); - BOOST_TEST( - (*full_cfg.get_key_cert_files()).cert_file == "/fake/cret_file.crt"); + const auto& key_cert = std::get( + *full_cfg.get_key_cert_files()); + BOOST_TEST(key_cert.key_file == "/fake/key_file.key"); + BOOST_TEST(key_cert.cert_file == "/fake/cret_file.crt"); BOOST_TEST(*full_cfg.get_truststore_file() == "/fake/truststore"); BOOST_TEST(*full_cfg.get_crl_file() == "/fake/crl"); BOOST_TEST(full_cfg.get_require_client_auth()); @@ -59,8 +61,10 @@ SEASTAR_THREAD_TEST_CASE(test_decode_full_rel_path) { " require_client_auth: true\n"; auto full_cfg = read_from_yaml(with_values); BOOST_TEST(full_cfg.is_enabled()); - BOOST_TEST((*full_cfg.get_key_cert_files()).key_file != "./key_file.key"); - BOOST_TEST((*full_cfg.get_key_cert_files()).cert_file != "./cret_file.crt"); + const auto& key_cert = std::get( + *full_cfg.get_key_cert_files()); + BOOST_TEST(key_cert.key_file != "./key_file.key"); + BOOST_TEST(key_cert.cert_file != "./cret_file.crt"); BOOST_TEST(*full_cfg.get_truststore_file() != "./truststore"); BOOST_TEST(*full_cfg.get_crl_file() != "./crl"); BOOST_TEST(full_cfg.get_require_client_auth()); @@ -92,9 +96,82 @@ SEASTAR_THREAD_TEST_CASE(test_decode_enabled_but_contains_empty_path) { " require_client_auth: false\n"; auto full_cfg = read_from_yaml(with_values); BOOST_TEST(full_cfg.is_enabled()); - BOOST_TEST((*full_cfg.get_key_cert_files()).key_file == ""); - BOOST_TEST((*full_cfg.get_key_cert_files()).cert_file == ""); + const auto& key_cert = std::get( + *full_cfg.get_key_cert_files()); + BOOST_TEST(key_cert.key_file == ""); + BOOST_TEST(key_cert.cert_file == ""); BOOST_TEST(*full_cfg.get_truststore_file() == ""); BOOST_TEST(*full_cfg.get_crl_file() == ""); BOOST_TEST(!full_cfg.get_require_client_auth()); } + +SEASTAR_THREAD_TEST_CASE(test_decode_p12_file) { + auto with_values = "tls_config:\n" + " enabled: true\n" + " truststore_file: /fake/truststore\n" + " crl_file: /fake/crl\n" + " require_client_auth: true\n" + " p12_file: /fake/temp.pfx\n" + " p12_password: test\n"; + auto full_cfg = read_from_yaml(with_values); + BOOST_TEST(full_cfg.is_enabled()); + const auto& p12_bag = std::get( + *full_cfg.get_key_cert_files()); + BOOST_TEST(p12_bag.p12_path == "/fake/temp.pfx"); + BOOST_TEST(p12_bag.p12_password == "test"); + BOOST_TEST(*full_cfg.get_truststore_file() == "/fake/truststore"); + BOOST_TEST(*full_cfg.get_crl_file() == "/fake/crl"); + BOOST_TEST(full_cfg.get_require_client_auth()); +} + +SEASTAR_THREAD_TEST_CASE(test_decode_p12_full_rel_path) { + auto with_values = "tls_config:\n" + " enabled: true\n" + " truststore_file: ./truststore\n" + " crl_file: ./crl\n" + " require_client_auth: true\n" + " p12_file: ./temp.pfx\n" + " p12_password: test\n"; + auto full_cfg = read_from_yaml(with_values); + BOOST_TEST(full_cfg.is_enabled()); + const auto& p12_bag = std::get( + *full_cfg.get_key_cert_files()); + BOOST_TEST(p12_bag.p12_path != "./temp.pfx"); + BOOST_TEST(p12_bag.p12_password == "test"); + BOOST_TEST(*full_cfg.get_truststore_file() != "./truststore"); + BOOST_TEST(*full_cfg.get_crl_file() != "./crl"); + BOOST_TEST(full_cfg.get_require_client_auth()); +} + +SEASTAR_THREAD_TEST_CASE(test_decode_p12_and_key_cert) { + auto with_values = "tls_config:\n" + " enabled: true\n" + " cert_file: /fake/cret_file.crt\n" + " key_file: /fake/key_file.key\n" + " truststore_file: /fake/truststore\n" + " crl_file: /fake/crl\n" + " require_client_auth: true\n" + " p12_file: /fake/temp.pfx\n" + " p12_password: test\n"; + try { + read_from_yaml(with_values); + BOOST_REQUIRE(false); + } catch (const YAML::TypedBadConversion&) { + // good! + } +} + +SEASTAR_THREAD_TEST_CASE(test_decode_p12_missing_password) { + auto with_values = "tls_config:\n" + " enabled: true\n" + " truststore_file: /fake/truststore\n" + " crl_file: /fake/crl\n" + " require_client_auth: true\n" + " p12_file: /fake/temp.pfx\n"; + try { + read_from_yaml(with_values); + BOOST_REQUIRE(false); + } catch (const YAML::TypedBadConversion&) { + // good! + } +} diff --git a/src/v/config/tls_config.cc b/src/v/config/tls_config.cc index 91c5063aeb36a..d84558c1d5f26 100644 --- a/src/v/config/tls_config.cc +++ b/src/v/config/tls_config.cc @@ -16,6 +16,8 @@ #include #include +#include +#include namespace config { ss::future> @@ -47,10 +49,20 @@ tls_config::get_credentials_builder() const& { if (_key_cert) { f = f.then([this, &builder] { - return builder.set_x509_key_file( - (*_key_cert).cert_file, - (*_key_cert).key_file, - ss::tls::x509_crt_format::PEM); + return ss::visit( + _key_cert.value(), + [&builder](const key_cert& c) { + return builder.set_x509_key_file( + c.cert_file, + c.key_file, + ss::tls::x509_crt_format::PEM); + }, + [&builder](const p12_container& p) { + return builder.set_simple_pkcs12_file( + p.p12_path, + ss::tls::x509_crt_format::PEM, + p.p12_password); + }); }); } @@ -71,14 +83,28 @@ tls_config::get_credentials_builder() && { } std::optional tls_config::validate(const tls_config& c) { - if (c.get_require_client_auth() && !c.get_truststore_file()) { - return "Trust store is required when client authentication is " - "enabled"; + const auto contains_p12_file = [&c]() { + if (c.get_key_cert_files()) { + return std::holds_alternative( + (*c.get_key_cert_files())); + } + return false; + }; + if ( + c.get_require_client_auth() && !c.get_truststore_file() + && !contains_p12_file()) { + return "Trust store or P12 file is required when client authentication " + "is enabled"; } return std::nullopt; } +std::ostream& operator<<(std::ostream& o, const config::p12_container& p) { + fmt::print(o, "{{ p12 file: {}, p12 password: REDACTED }}", p.p12_password); + return o; +} + std::ostream& operator<<(std::ostream& o, const config::key_cert& c) { o << "{ " << "key_file: " << c.key_file << " " @@ -123,8 +149,16 @@ Node convert::encode(const config::tls_config& rhs) { node["require_client_auth"] = rhs.get_require_client_auth(); if (rhs.get_key_cert_files()) { - node["cert_file"] = (*rhs.get_key_cert_files()).key_file; - node["key_file"] = (*rhs.get_key_cert_files()).cert_file; + ss::visit( + rhs.get_key_cert_files().value(), + [&node](const config::key_cert& c) { + node["cert_file"] = c.cert_file; + node["key_file"] = c.key_file; + }, + [&node](const config::p12_container& p12) { + node["p12_file"] = p12.p12_path; + node["p12_password"] = "REDACTED"; + }); } if (rhs.get_truststore_file()) { @@ -150,19 +184,38 @@ bool convert::decode( ^ static_cast(node["cert_file"])) { return false; } + + // Must have either both or neither for PKCS#12 files + if ( + static_cast(node["p12_file"]) + ^ static_cast(node["p12_password"])) { + return false; + } + + // Cannot have both key/cert and P12 file + if ( + static_cast(node["key_file"]) + && static_cast(node["p12_file"])) { + return false; + } auto enabled = node["enabled"] && node["enabled"].as(); if (!enabled) { rhs = config::tls_config( false, std::nullopt, std::nullopt, std::nullopt, false); } else { - auto key_cert = node["key_file"] ? std::make_optional( - config::key_cert{ - to_absolute(node["key_file"].as()), - to_absolute(node["cert_file"].as())}) - : std::nullopt; + std::optional container; + if (node["key_file"]) { + container.emplace(config::key_cert{ + to_absolute(node["key_file"].as()), + to_absolute(node["cert_file"].as())}); + } else if (node["p12_file"]) { + container.emplace(config::p12_container{ + to_absolute(node["p12_file"].as()), + node["p12_password"].as()}); + } rhs = config::tls_config( enabled, - key_cert, + container, to_absolute(read_optional(node, "truststore_file")), to_absolute(read_optional(node, "crl_file")), node["require_client_auth"] diff --git a/src/v/config/tls_config.h b/src/v/config/tls_config.h index 618c264ad0001..babfe37aa0a01 100644 --- a/src/v/config/tls_config.h +++ b/src/v/config/tls_config.h @@ -20,6 +20,7 @@ #include #include +#include namespace config { @@ -34,6 +35,18 @@ struct key_cert { friend std::ostream& operator<<(std::ostream& o, const key_cert& c); }; +struct p12_container { + ss::sstring p12_path; + ss::sstring p12_password; + + friend bool operator==(const p12_container&, const p12_container&) + = default; + + friend std::ostream& operator<<(std::ostream& os, const p12_container& p); +}; + +using key_cert_container = std::variant; + inline constexpr std::string_view tlsv1_2_cipher_string = "ECDHE-RSA-AES128-GCM-SHA256:AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:" "AES256-GCM-SHA384:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-RSA-AES128-SHA:AES128-" @@ -51,7 +64,7 @@ class tls_config { tls_config( bool enabled, - std::optional key_cert, + std::optional key_cert, std::optional truststore, std::optional crl, bool require_client_auth) @@ -63,7 +76,7 @@ class tls_config { bool is_enabled() const { return _enabled; } - const std::optional& get_key_cert_files() const { + const std::optional& get_key_cert_files() const { return _key_cert; } @@ -91,7 +104,7 @@ class tls_config { private: bool _enabled{false}; - std::optional _key_cert; + std::optional _key_cert; std::optional _truststore_file; std::optional _crl_file; bool _require_client_auth{false}; From 28a09c80bc36bd1817ef776a344d8e2bed9e8158 Mon Sep 17 00:00:00 2001 From: Michael Boquard Date: Tue, 2 Jul 2024 10:59:41 -0400 Subject: [PATCH 2/6] dt: Cleanup of tls.py Signed-off-by: Michael Boquard --- tests/rptest/services/tls.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/rptest/services/tls.py b/tests/rptest/services/tls.py index 9084510c67bca..929b3e1348a14 100644 --- a/tests/rptest/services/tls.py +++ b/tests/rptest/services/tls.py @@ -316,7 +316,7 @@ def __init__(self, logger, ca_end_date=None, cert_expiry_days=1): self._ca_end_date = ca_end_date self._cert_expiry_days = cert_expiry_days self._ca = self._create_ca() - self.certs = {} + self.certs: dict[str, Certificate] = {} def _with_dir(self, *args): return os.path.join(self._dir.name, *args) @@ -346,7 +346,7 @@ def _exec(self, cmd): retries += 1 - def _create_ca(self): + def _create_ca(self) -> CertificateAuthority: cfg = self._with_dir("ca.conf") key = self._with_dir("ca.key") crt = self._with_dir("ca.crt") @@ -385,7 +385,7 @@ def _create_crl(self, ca: str, cfg: str, crl_srl: str) -> str: return crl @property - def ca(self): + def ca(self) -> CertificateAuthority: return self._ca def create_cert(self, @@ -478,7 +478,7 @@ def __init__(self, ext='signing_ca_ext', )) self._cert_chain = self._create_ca_cert_chain() - self.certs = {} + self.certs: dict[str, Certificate] = {} @property def ca(self) -> CertificateAuthority: @@ -549,7 +549,7 @@ def _create_ca_cert_chain(self) -> CertificateAuthority: # Now do the same for the CRLs crl_files = [ca.crl for ca in self._cas] crl_out = self._with_dir('ca', 'signing-crl-chain.crl') - pathlib.Path(out).touch() + pathlib.Path(crl_out).touch() with open(crl_out, 'w') as outfile: for fname in reversed(crl_files): with open(fname, 'r') as infile: From 5151053568f9a96874eb96d3b1e5b0dca37f44b1 Mon Sep 17 00:00:00 2001 From: Michael Boquard Date: Tue, 2 Jul 2024 13:54:29 -0400 Subject: [PATCH 3/6] dt: Added ability to generate P12 files Signed-off-by: Michael Boquard --- tests/rptest/services/tls.py | 39 ++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/tests/rptest/services/tls.py b/tests/rptest/services/tls.py index 929b3e1348a14..b0ac1084da839 100644 --- a/tests/rptest/services/tls.py +++ b/tests/rptest/services/tls.py @@ -4,6 +4,8 @@ import pathlib import subprocess import os +import string +import random _ca_config_tmpl = """ # OpenSSL CA configuration file @@ -296,8 +298,8 @@ CertificateAuthority = collections.namedtuple("CertificateAuthority", ["cfg", "key", "crt", "crl"]) -Certificate = collections.namedtuple("Certificate", - ["cfg", "key", "crt", "ca"]) +Certificate = collections.namedtuple( + "Certificate", ["cfg", "key", "crt", "ca", "p12_file", "p12_password"]) class TLSCertManager: @@ -318,6 +320,12 @@ def __init__(self, logger, ca_end_date=None, cert_expiry_days=1): self._ca = self._create_ca() self.certs: dict[str, Certificate] = {} + @staticmethod + def generate_password( + length: int, + choices: str = string.ascii_letters + string.digits) -> str: + return ''.join(random.choices(choices, k=length)) + def _with_dir(self, *args): return os.path.join(self._dir.name, *args) @@ -400,6 +408,7 @@ def create_cert(self, key = self._with_dir(f"{name}.key") csr = self._with_dir(f"{name}.csr") crt = self._with_dir(f"{name}.crt") + p12_file = self._with_dir(f"{name}.p12") with open(cfg, "w") as f: if common_name is None: @@ -420,10 +429,28 @@ def create_cert(self, f"-extensions signing_node_req -in {csr} -out {crt} " f"-days {self._cert_expiry_days} -outdir {self._dir.name} -batch") - cert = Certificate(cfg, key, crt, self.ca) + p12_password = self.generate_pkcs12_file(p12_file, key, crt, + self.ca.crt) + + cert = Certificate(cfg, key, crt, self.ca, p12_file, p12_password) self.certs[name] = cert return cert + def generate_pkcs12_file(self, + p12_file: str, + key: str, + crt: str, + ca: str, + pw_length: int = 16) -> str: + """ + Runs command to generate a new PKCS#12 file and returns the generated password + """ + p12_password = self.generate_password(length=pw_length) + self._exec( + f'openssl pkcs12 -export -inkey {key} -in {crt} -certfile {ca} -passout pass:{p12_password} -out {p12_file}' + ) + return p12_password + # TODO(oren): reasons enum def revoke_cert(self, crt: Certificate, reason: str = "unspecified"): self._exec( @@ -572,6 +599,7 @@ def create_cert(self, key = self._with_dir('certs', f"{name}.key") csr = self._with_dir('certs', f"{name}.csr") crt = self._with_dir('certs', f"{name}.crt") + p12_file = self._with_dir('certs', f"{name}.p12") with open(cfg, "w") as f: if common_name is None: @@ -588,6 +616,9 @@ def create_cert(self, f"-in {csr} -out {crt} -extensions server_ext -days {self.cert_expiry_days} -batch" ) - cert = Certificate(cfg, key, crt, self.ca) + p12_password = self.generate_pkcs12_file(p12_file, key, crt, + self.ca.crt) + + cert = Certificate(cfg, key, crt, self.ca, p12_file, p12_password) self.certs[name] = cert return cert From c50a2c5139379983f0987d3988347ff66369962c Mon Sep 17 00:00:00 2001 From: Michael Boquard Date: Tue, 2 Jul 2024 14:04:33 -0400 Subject: [PATCH 4/6] dt: Added saving of PKCS12 file to node Signed-off-by: Michael Boquard --- tests/rptest/services/redpanda.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/rptest/services/redpanda.py b/tests/rptest/services/redpanda.py index febd191753395..1f0dc1e801640 100644 --- a/tests/rptest/services/redpanda.py +++ b/tests/rptest/services/redpanda.py @@ -1236,6 +1236,7 @@ class RedpandaServiceBase(RedpandaServiceABC, Service): CLUSTER_BOOTSTRAP_CONFIG_FILE = "/etc/redpanda/.bootstrap.yaml" TLS_SERVER_KEY_FILE = "/etc/redpanda/server.key" TLS_SERVER_CRT_FILE = "/etc/redpanda/server.crt" + TLS_SERVER_P12_FILE = "/etc/redpanda/server.p12" TLS_CA_CRT_FILE = "/etc/redpanda/ca.crt" TLS_CA_CRL_FILE = "/etc/redpanda/ca.crl" SYSTEM_TLS_CA_CRT_FILE = "/usr/local/share/ca-certificates/ca.crt" @@ -2801,6 +2802,15 @@ def write_tls_certs(self): os.path.dirname(RedpandaService.TLS_SERVER_CRT_FILE)) node.account.copy_to(cert.crt, RedpandaService.TLS_SERVER_CRT_FILE) + self.logger.info( + f"Writing Redpanda node P12 file: {RedpandaService.TLS_SERVER_P12_FILE}" + ) + self.logger.debug("P12 file is binary encoded") + node.account.mkdirs( + os.path.dirname(RedpandaService.TLS_SERVER_P12_FILE)) + node.account.copy_to(cert.p12_file, + RedpandaService.TLS_SERVER_P12_FILE) + self.logger.info( f"Writing Redpanda node tls ca cert file: {RedpandaService.TLS_CA_CRT_FILE}" ) From 18ddeb19db88a07d07a55b3412767973d6b5c5c7 Mon Sep 17 00:00:00 2001 From: Michael Boquard Date: Tue, 2 Jul 2024 19:29:56 -0400 Subject: [PATCH 5/6] dt: Added plumbing for PKCS12 file Signed-off-by: Michael Boquard --- tests/rptest/services/redpanda.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/tests/rptest/services/redpanda.py b/tests/rptest/services/redpanda.py index 1f0dc1e801640..8f0120d58cea6 100644 --- a/tests/rptest/services/redpanda.py +++ b/tests/rptest/services/redpanda.py @@ -864,6 +864,18 @@ def create_service_client_cert(self, service: Service, """ raise NotImplementedError("create_service_client_cert") + def use_pkcs12_file(self) -> bool: + """ + Use the generated PKCS#12 file instead of the key/cert + """ + return False + + def p12_password(self, node: ClusterNode) -> str: + """ + Get the PKCS#12 file password for the node + """ + raise NotImplementedError("p12_password") + class SecurityConfig: # the system currently has a single principal mapping rule. this is @@ -4010,17 +4022,25 @@ def is_fips_capable(node) -> bool: conf = yaml.dump(doc) if self._security.tls_provider: + p12_password = self._security.tls_provider.p12_password( + node) if self._security.tls_provider.use_pkcs12_file( + ) else None tls_config = [ dict( enabled=True, require_client_auth=self.require_client_auth(), name=n, - key_file=RedpandaService.TLS_SERVER_KEY_FILE, - cert_file=RedpandaService.TLS_SERVER_CRT_FILE, truststore_file=RedpandaService.TLS_CA_CRT_FILE, crl_file=RedpandaService.TLS_CA_CRL_FILE, ) for n in ["dnslistener", "iplistener"] ] + for n in tls_config: + if p12_password is None: + n["cert_file"] = RedpandaService.TLS_SERVER_CRT_FILE + n["key_file"] = RedpandaService.TLS_SERVER_KEY_FILE + else: + n["p12_file"] = RedpandaService.TLS_SERVER_P12_FILE + n["p12_password"] = p12_password doc = yaml.full_load(conf) doc["redpanda"].update(dict(kafka_api_tls=tls_config)) conf = yaml.dump(doc) From 90d4bdb6abdb7c50ce01396b803e76919efbf0f9 Mon Sep 17 00:00:00 2001 From: Michael Boquard Date: Tue, 9 Jul 2024 16:26:39 -0400 Subject: [PATCH 6/6] dt: Added pkcs12 test Added PKCS#12 file smoke test Signed-off-by: Michael Boquard --- tests/rptest/tests/pkcs12_test.py | 99 +++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 tests/rptest/tests/pkcs12_test.py diff --git a/tests/rptest/tests/pkcs12_test.py b/tests/rptest/tests/pkcs12_test.py new file mode 100644 index 0000000000000..072ad841be0a1 --- /dev/null +++ b/tests/rptest/tests/pkcs12_test.py @@ -0,0 +1,99 @@ +# Copyright 2024 Redpanda Data, Inc. +# +# Use of this software is governed by the Business Source License +# included in the file licenses/BSL.md +# +# As of the Change Date specified in that file, in accordance with +# the Business Source License, use of this software will be governed +# by the Apache License, Version 2.0 + +import socket + +from ducktape.cluster.cluster import ClusterNode +from ducktape.mark import matrix +from ducktape.services.service import Service +from rptest.clients.rpk import RpkTool +from rptest.services.admin import Admin +from rptest.services.cluster import cluster +from rptest.services.redpanda import TLSProvider, SecurityConfig +from rptest.services.tls import Certificate, CertificateAuthority, TLSCertManager +from rptest.tests.redpanda_test import RedpandaTest + + +class P12TLSProvider(TLSProvider): + def __init__(self, tls: TLSCertManager, use_pkcs12: bool): + self.tls = tls + self.use_pkcs12 = use_pkcs12 + + @property + def ca(self) -> CertificateAuthority: + return self.tls.ca + + def create_broker_cert(self, service: Service, + node: ClusterNode) -> Certificate: + assert node in service.nodes + return self.tls.create_cert(node.name) + + def create_service_client_cert(self, _: Service, name: str) -> Certificate: + return self.tls.create_cert(socket.gethostname(), + name=name, + common_name=name) + + def use_pkcs12_file(self) -> bool: + return self.use_pkcs12 + + def p12_password(self, node: ClusterNode) -> str: + assert node.name in self.tls.certs, f"No certificate associated with node {node.name}" + return self.tls.certs[node.name].p12_password + + +class PKCS12Test(RedpandaTest): + """ + Tests used to validate the functionality of using a PKCS#12 file + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, + num_brokers=3, + skip_if_no_redpanda_log=True, + **kwargs) + self.user, self.password, self.algorithm = self.redpanda.SUPERUSER_CREDENTIALS + self.admin = Admin(self.redpanda) + + def setUp(self): + # Skip set up to allow test to control how Redpanda's TLS settings are configured + pass + + def _prepare_cluster(self, use_pkcs12: bool): + self.tls = TLSCertManager(self.logger) + self.provider = P12TLSProvider(self.tls, use_pkcs12) + self.user_cert = self.tls.create_cert(socket.gethostname(), + common_name="walterP", + name="user") + + self.security = SecurityConfig() + self.security.endpoint_authn_method = "mtls_identity" + self.security.tls_provider = self.provider + self.security.require_client_auth = True + self.security.enable_sasl = False + + self.redpanda.set_security_settings(self.security) + self.redpanda.add_extra_rp_conf({ + 'kafka_mtls_principal_mapping_rules': + [self.security.principal_mapping_rules] + }) + + super().setUp() + self.admin.create_user("walterP", self.password, self.algorithm) + self.rpk = RpkTool(self.redpanda, tls_cert=self.user_cert) + + @cluster(num_nodes=3) + @matrix(use_pkcs12=[True, False]) + def test_smoke(self, use_pkcs12: bool): + """ + Simple smoke test to verify that the PKCS12 file is being used + """ + self._prepare_cluster(use_pkcs12) + TOPIC_NAME = "foo" + self.rpk.create_topic(TOPIC_NAME) + topics = [t for t in self.rpk.list_topics()] + assert TOPIC_NAME in topics, f"Missing topic '{TOPIC_NAME}': {topics}"