From f02c646e56dc8faa1e1e0bc6a234befb95d568cd Mon Sep 17 00:00:00 2001 From: Alexander Krimm Date: Fri, 23 Jan 2026 17:37:19 +0100 Subject: [PATCH] RestServer: use the full certificate chain ... instead of just the leaf. Without this, verification of the ssl connection will fail in browsers which do not have the intermediaries in their cache. To verify run MajordomoRest_example in a directory containing a valid certificate chain in `demo_public.crt` and corresponding key in `demo_private.key` and then run `openssl s_client -showcerts -connect localhost:8080` and observe that the full chain is present instead of just the leaf. Signed-off-by: Alexander Krimm --- .../include/majordomo/RestServer.hpp | 21 ++++++++---- .../majordomo/TlsServerSession_Ossl.hpp | 17 +++++++--- src/rest/include/rest/RestUtils.hpp | 33 +++++++++++++------ 3 files changed, 50 insertions(+), 21 deletions(-) diff --git a/src/majordomo/include/majordomo/RestServer.hpp b/src/majordomo/include/majordomo/RestServer.hpp index 10d3a799..65160aa8 100644 --- a/src/majordomo/include/majordomo/RestServer.hpp +++ b/src/majordomo/include/majordomo/RestServer.hpp @@ -223,7 +223,7 @@ inline int alpn_select_proto_cb(SSL * /*ssl*/, const unsigned char **out, unsign return SSL_TLSEXT_ERR_OK; } -inline std::expected create_ssl_ctx(EVP_PKEY *key, X509 *cert) { +inline std::expected create_ssl_ctx(EVP_PKEY *key, std::span cert) { auto ssl_ctx = SSL_CTX_Ptr(SSL_CTX_new(TLS_server_method()), SSL_CTX_free); if (!ssl_ctx) { return std::unexpected(std::format("Could not create SSL/TLS context: {}", ERR_error_string(ERR_get_error(), nullptr))); @@ -236,9 +236,16 @@ inline std::expected create_ssl_ctx(EVP_PKEY *key, X50 if (SSL_CTX_use_PrivateKey(ssl_ctx.get(), key) <= 0) { return std::unexpected(std::format("Could not configure private key")); } - if (SSL_CTX_use_certificate(ssl_ctx.get(), cert) != 1) { + if (SSL_CTX_use_certificate(ssl_ctx.get(), cert[0].get()) != 1) { return std::unexpected(std::format("Could not configure certificate file")); } + if (cert.size() > 1) { + for (std::size_t i = 1; i < cert.size(); ++i) { + if (SSL_CTX_add_extra_chain_cert(ssl_ctx.get(), cert[i].get()) != 1) { + return std::unexpected(std::format("Could not add certificate chain file {}", i)); + } + } + } if (!SSL_CTX_check_private_key(ssl_ctx.get())) { return std::unexpected("Private key does not match the certificate"); @@ -2211,7 +2218,7 @@ struct RestServer { Http3ServerSocket _quicServerSocket; SSL_CTX_Ptr _sslCtxTcp = SSL_CTX_Ptr(nullptr, SSL_CTX_free); EVP_PKEY_Ptr _key = EVP_PKEY_Ptr(nullptr, EVP_PKEY_free); - X509_Ptr _cert = X509_Ptr(nullptr, X509_free); + std::vector _cert; std::shared_ptr _sharedData = std::make_shared(); std::map> _h2Sessions; std::unordered_map>> _h3Sessions; @@ -2231,7 +2238,7 @@ struct RestServer { RestServer(RestServer &&) = default; RestServer &operator=(RestServer &&) = default; - RestServer(SSL_CTX_Ptr sslCtxTcp, EVP_PKEY_Ptr key, X509_Ptr cert) + RestServer(SSL_CTX_Ptr sslCtxTcp, EVP_PKEY_Ptr key, std::vector cert) : _sslCtxTcp(std::move(sslCtxTcp)), _key(std::move(key)), _cert(std::move(cert)) { if (_sslCtxTcp) { SSL_library_init(); @@ -2241,7 +2248,7 @@ struct RestServer { } static std::expected unencrypted() { - return RestServer(SSL_CTX_Ptr(nullptr, SSL_CTX_free), EVP_PKEY_Ptr(nullptr, EVP_PKEY_free), X509_Ptr(nullptr, X509_free)); + return RestServer(SSL_CTX_Ptr(nullptr, SSL_CTX_free), EVP_PKEY_Ptr(nullptr, EVP_PKEY_free), {}); } static std::expected sslWithBuffers(std::string_view certBuffer, std::string_view keyBuffer) { @@ -2253,7 +2260,7 @@ struct RestServer { if (!maybeKey) { return std::unexpected(maybeKey.error()); } - auto maybeSslCtxTcp = create_ssl_ctx(maybeKey->get(), maybeCert->get()); + auto maybeSslCtxTcp = create_ssl_ctx(maybeKey->get(), maybeCert.value()); if (!maybeSslCtxTcp) { return std::unexpected(maybeSslCtxTcp.error()); } @@ -2269,7 +2276,7 @@ struct RestServer { if (!maybeKey) { return std::unexpected(maybeKey.error()); } - auto maybeSslCtxTcp = create_ssl_ctx(maybeKey->get(), maybeCert->get()); + auto maybeSslCtxTcp = create_ssl_ctx(maybeKey->get(), maybeCert.value()); if (!maybeSslCtxTcp) { return std::unexpected(maybeSslCtxTcp.error()); } diff --git a/src/majordomo/include/majordomo/TlsServerSession_Ossl.hpp b/src/majordomo/include/majordomo/TlsServerSession_Ossl.hpp index c75c3629..d6856fa1 100644 --- a/src/majordomo/include/majordomo/TlsServerSession_Ossl.hpp +++ b/src/majordomo/include/majordomo/TlsServerSession_Ossl.hpp @@ -191,7 +191,7 @@ class TLSServerContext { return *this; } - int init(const opencmw::rest::detail::EVP_PKEY_Ptr &key, const opencmw::rest::detail::X509_Ptr &cert, AppProtocol app_proto) { + int init(const opencmw::rest::detail::EVP_PKEY_Ptr &key, const std::vector &cert, AppProtocol app_proto) { constexpr static unsigned char sid_ctx[] = "ngtcp2 server"; ssl_ctx_ = SSL_CTX_new(TLS_server_method()); @@ -226,9 +226,18 @@ class TLSServerContext { return -1; } - if (SSL_CTX_use_certificate(ssl_ctx_, cert.get()) != 1) { - std::cerr << "SSL_CTX_use_certificate_chain_file: " << ERR_error_string(ERR_get_error(), nullptr) << std::endl; - return -1; + if (!cert.empty()) { + if (SSL_CTX_use_certificate(ssl_ctx_, cert[0].get()) != 1) { + std::cerr << "SSL_CTX_use_certificate_chain_file: " << ERR_error_string(ERR_get_error(), nullptr) << std::endl; + return -1; + } + + for (std::size_t i = 1; i < cert.size(); ++i) { + if (SSL_CTX_add_extra_chain_cert(ssl_ctx_, X509_up_ref(cert[i].get()) ? cert[i].get() : nullptr) != 1) { + std::cerr << "Could not add certificate chain file " << i << ": " << ERR_error_string(ERR_get_error(), nullptr) << std::endl; + return -1; + } + } } if (SSL_CTX_check_private_key(ssl_ctx_) != 1) { diff --git a/src/rest/include/rest/RestUtils.hpp b/src/rest/include/rest/RestUtils.hpp index f7b800a3..0cb8d7b6 100644 --- a/src/rest/include/rest/RestUtils.hpp +++ b/src/rest/include/rest/RestUtils.hpp @@ -83,30 +83,43 @@ inline std::expected createCertificateStore(std::st return cert_store; } -inline std::expected readServerCertificateFromBuffer(std::string_view X509_ca_bundle) { +inline std::expected, std::string> readServerCertificateFromBuffer(std::string_view X509_ca_bundle) { BIO *certBio = BIO_new(BIO_s_mem()); BIO_write(certBio, X509_ca_bundle.data(), static_cast(X509_ca_bundle.size())); - auto certX509 = X509_Ptr(PEM_read_bio_X509(certBio, nullptr, nullptr, nullptr), X509_free); + std::vector certs; + while (true) { + auto certX509 = X509_Ptr(PEM_read_bio_X509(certBio, nullptr, nullptr, nullptr), X509_free); + if (!certX509) { + break; + } + certs.push_back(std::move(certX509)); + } BIO_free(certBio); - if (!certX509) { + if (certs.empty()) { return std::unexpected(std::format("failed to read certificate from buffer:\n#---start---\n{}\n#---end---\n", X509_ca_bundle)); } - return certX509; + return certs; } -inline std::expected readServerCertificateFromFile(std::filesystem::path fpath) { +inline std::expected, std::string> readServerCertificateFromFile(std::filesystem::path fpath) { auto path = fpath.string(); - BIO *certBio - = BIO_new_file(path.data(), "r"); + BIO *certBio = BIO_new_file(path.data(), "r"); if (!certBio) { return std::unexpected(std::format("failed to read certificate from file {}: {}", path, ERR_error_string(ERR_get_error(), nullptr))); } - auto certX509 = X509_Ptr(PEM_read_bio_X509(certBio, nullptr, nullptr, nullptr), X509_free); + std::vector certs; + while (true) { + auto certX509 = X509_Ptr(PEM_read_bio_X509(certBio, nullptr, nullptr, nullptr), X509_free); + if (!certX509) { + break; + } + certs.push_back(std::move(certX509)); + } BIO_free(certBio); - if (!certX509) { + if (certs.empty()) { return std::unexpected(std::format("failed to read certificate key from file: {}", path)); } - return certX509; + return certs; } inline std::expected readServerPrivateKeyFromBuffer(std::string_view x509_private_key) {