diff --git a/doc/qbk/03_core/9_ssl-tls-shutdown.qbk b/doc/qbk/03_core/10_ssl_tls_shutdown.qbk similarity index 100% rename from doc/qbk/03_core/9_ssl-tls-shutdown.qbk rename to doc/qbk/03_core/10_ssl_tls_shutdown.qbk diff --git a/doc/qbk/03_core/9_ssl_tls_certificate.qbk b/doc/qbk/03_core/9_ssl_tls_certificate.qbk new file mode 100644 index 00000000..f424ed91 --- /dev/null +++ b/doc/qbk/03_core/9_ssl_tls_certificate.qbk @@ -0,0 +1,217 @@ +[/ + Copyright (c) 2025 Mohammad Nejati + + Distributed under the Boost Software License, Version 1.0. (See accompanying + file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + + Official repository: https://github.com/boostorg/beast +] + + +[section SSL/TLS Certificate] + +[/-----------------------------------------------------------------------------] + +[heading Certificate Authority] + +A Certificate Authority (CA) is a trusted entity that signs digital +certificates, enabling users to verify their authenticity. Rather than storing +every individual certificate for each server (which would be impractical due to +the sheer volume and frequent renewals), users can store a limited set of root +certificates to authenticate server certificates as needed. + +Boost.Asio provides various methods for loading certificate authority +certificates: + +* [@boost:/doc/html/boost_asio/reference/ssl__context/add_certificate_authority.html `net::ssl::context::add_certificate_authority`] +* [@boost:/doc/html/boost_asio/reference/ssl__context/add_verify_path.html `net::ssl::context::add_verify_path`] +* [@boost:/doc/html/boost_asio/reference/ssl__context/load_verify_file.html `net::ssl::context::load_verify_file`] +* [@boost:/doc/html/boost_asio/reference/ssl__context/set_default_verify_paths.html `net::ssl::context::set_default_verify_paths`] + +It is important to set up peer verification so that the TLS/SSL handshake fails +if certificate verification is unsuccessful: + +[snippet_core_6] + +A client must also verify that the hostname or IP address in the certificate +matches the expected one. The +[@boost:/doc/html/boost_asio/reference/ssl__host_name_verification.html +`net::ssl::host_name_verification`] helper function object can perform this +verification according to the rules described in RFC 6125: + +[snippet_core_7] + +A server can also request and verify a client certificate to authenticate the +client: + +[snippet_core_8] + +[/-----------------------------------------------------------------------------] + +[heading Server Certificate] + +A Server Certificate is a digital certificate that confirms a server's identity +as the legitimate destination for a client. It contains a verifiable signature +that ensures it was issued by a trusted certificate authority (CA). + +When a server certificate is issued by an intermediate certificate authority, +and the client lacks those intermediate certificates, the server should provide +all the relevant certificates to the client. This allows the client to verify +the final certificate in the chain against the root certificate. + +The following Boost.Asio methods can be used for loading a certificate or a +certificate chain: + +* [@boost:/doc/html/boost_asio/reference/ssl__context/use_certificate.html `net::ssl::context::use_certificate`] +* [@boost:/doc/html/boost_asio/reference/ssl__context/use_certificate_file.html `net::ssl::context::use_certificate_file`] +* [@boost:/doc/html/boost_asio/reference/ssl__context/use_certificate_chain.html `net::ssl::context::use_certificate_chain`] +* [@boost:/doc/html/boost_asio/reference/ssl__context/use_certificate_chain_file.html `net::ssl::context::use_certificate_chain_file`] + +[/-----------------------------------------------------------------------------] + +[heading Client Certificate] + +A server can authenticate clients by requiring and verifying their certificates, +preventing access for those without a valid certificate and private key. The +server enforces this by modifying peer verification settings: + +[snippet_core_8] + +If used, the necessary CA certificates must be loaded into the server's SSL +context to enable verification of the client's certificate. + +[/-----------------------------------------------------------------------------] + +[heading Common Name and Subject Alternative Name] + +The Subject Alternative Name (SAN) is an extension in X.509 certificates that +allows multiple domain names, subdomains, or IP addresses to be associated with +a single SSL/TLS certificate. Before that it was the Common Name field in the +certificate subject which could contain a single hostname. + +[@https://datatracker.ietf.org/doc/html/rfc6125#appendix-B.2 RFC 6125] +recommends that if a certificate includes a SAN dNSName field, the client must +ignore the subject CN field. Some modern browsers, such as Google Chrome, check +only the SAN section in an SSL/TLS certificate and reject certificates that +contain only the CN field. + +[/-----------------------------------------------------------------------------] + +[heading Private Key] + +The private key of a certificate is required during the SSL/TLS handshake to +prove that the certificate's provider is its rightful owner + +The following Boost.Asio methods can be used for loading a private key: + +* [@boost:/doc/html/boost_asio/reference/ssl__context/use_private_key.html `net::ssl::context::use_private_key`] +* [@boost:/doc/html/boost_asio/reference/ssl__context/use_private_key_file.html `net::ssl::context::use_private_key_file`] +* [@boost:/doc/html/boost_asio/reference/ssl__context/use_rsa_private_key.html `net::ssl::context::use_rsa_private_key`] +* [@boost:/doc/html/boost_asio/reference/ssl__context/use_rsa_private_key_file.html `net::ssl::context::use_rsa_private_key_file`] + +If the private key is secured with a password, the +[@boost:/doc/html/boost_asio/reference/ssl__context/set_password_callback.html +net::ssl::context::set_password_callback] allows specifying a callable object to +retrieve the password. + +[/-----------------------------------------------------------------------------] + +[heading Self-Signed and Self-Issued Certificates] + +A self-issued certificate is a certificate where the issuer and subject are the +same entity. + +A self-signed certificate is a self-issued certificate in which the digital +signature can be verified using the public key within the certificate. + +[warning + Installing an untrusted, self-issued, or self-signed CA certificate poses a + significant security risk, as there are no restrictions on the domains for + which it can issue certificates. This allows attackers to generate + fraudulent certificates for any public domain, enabling man-in-the-middle + attacks if they gain access to your network. +] + +[/-----------------------------------------------------------------------------] + +[heading Diffie-Hellman (DH) Parameters] + +Diffie-Hellman (DH) key exchange is a cryptographic protocol that allows two +parties to securely establish a shared secret over an insecure communication +channel. The key exchange process involves both parties agreeing on a set of +parameters, known as Diffie-Hellman parameters, which include a large prime +number `p` and a generator `g`. Since generating these parameters is a +computationally expensive task, a user might prefer to provide a precomputed +value at startup. + +The following Boost.Asio methods can be used for loading DH parameters: + +* [@boost:/doc/html/boost_asio/reference/ssl__context/use_tmp_dh.html `net::ssl::context::use_tmp_dh`] +* [@boost:/doc/html/boost_asio/reference/ssl__context/use_tmp_dh_file.html `net::ssl::context::use_tmp_dh_file`] + +If no DH parameter is provided, OpenSSL will refuse to perform any handshake +that uses DHE-based cipher suites but will still work with other cipher suites, +such as those based on ECDHE. + +[/-----------------------------------------------------------------------------] + +[heading A Self-Issued Certificate Example] + +In the following example, we will generate a self-signed CA certificate and use +it to issue both server and client certificates. + +* Generate a CA certificate: + +``` +openssl req -new -newkey rsa:4096 -keyout ca.key -x509 -out ca.crt -subj "/CN=localhost" -days 365 +``` + + +* Generate a Server CSR: + +``` +openssl req -new -newkey rsa:4096 -keyout server.key -out server.csr -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" +``` + + +* Sign the Server CSR using our CA: + +``` +openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -copy_extensions copy -days 365 -out server.crt +``` + + +* Generate a Client CSR: + +``` +openssl req -new -newkey rsa:4096 -keyout client.key -out client.csr -subj "/CN=client.1" +``` + + +* Sign the Client CSR using our CA: + +``` +openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -days 365 -out client.crt +``` + +* Generate a DH parameters file: + +``` +openssl dhparam -out dh4096.pem 4096 +``` + +Server example: [path_link example/doc/ssl/server.cpp server.cpp] + +Note that the server is configured in such a way that it requests and verifies +the client certificate. You can disable this by commenting out the related line +in the example. + +You can test the server using this cURL command: + +``` +curl https://localhost:8080 --cacert ca.crt --cert client.crt --key client.key +``` + +Also, you can use the client example: [path_link example/doc/ssl/client.cpp client.cpp] + +[endsect] diff --git a/doc/qbk/03_core/_core.qbk b/doc/qbk/03_core/_core.qbk index 13934fca..594e2491 100644 --- a/doc/qbk/03_core/_core.qbk +++ b/doc/qbk/03_core/_core.qbk @@ -96,7 +96,8 @@ effect: [include 5_buffers.qbk] [include 6_files.qbk] [include 7_composed.qbk] -[include 9_ssl-tls-shutdown.qbk] +[include 9_ssl_tls_certificate.qbk] +[include 10_ssl_tls_shutdown.qbk] [endsect] [section:config Configuration] diff --git a/example/CMakeLists.txt b/example/CMakeLists.txt index 8979f264..1c11d90d 100644 --- a/example/CMakeLists.txt +++ b/example/CMakeLists.txt @@ -13,4 +13,6 @@ add_subdirectory(advanced) add_subdirectory(http) add_subdirectory(websocket) +add_subdirectory(doc) + add_subdirectory(echo-op) diff --git a/example/Jamfile b/example/Jamfile index 1fe2d727..bf7c5329 100644 --- a/example/Jamfile +++ b/example/Jamfile @@ -30,5 +30,7 @@ build-project advanced ; build-project http ; build-project websocket ; +build-project doc ; + # legacy build-project echo-op ; diff --git a/example/doc/CMakeLists.txt b/example/doc/CMakeLists.txt new file mode 100644 index 00000000..9704d31f --- /dev/null +++ b/example/doc/CMakeLists.txt @@ -0,0 +1,12 @@ +# +# Copyright (c) 2025 Mohammad Nejati +# +# Distributed under the Boost Software License, Version 1.0. (See accompanying +# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +# +# Official repository: https://github.com/boostorg/beast +# + +if (OPENSSL_FOUND) + add_subdirectory(ssl) +endif () diff --git a/example/doc/Jamfile b/example/doc/Jamfile new file mode 100644 index 00000000..0493a4e9 --- /dev/null +++ b/example/doc/Jamfile @@ -0,0 +1,10 @@ +# +# Copyright (c) 2025 Mohammad Nejati +# +# Distributed under the Boost Software License, Version 1.0. (See accompanying +# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +# +# Official repository: https://github.com/boostorg/beast +# + +build-project ssl ; diff --git a/example/doc/ssl/CMakeLists.txt b/example/doc/ssl/CMakeLists.txt new file mode 100644 index 00000000..fb1a03fe --- /dev/null +++ b/example/doc/ssl/CMakeLists.txt @@ -0,0 +1,44 @@ +# +# Copyright (c) 2025 Mohammad Nejati +# +# Distributed under the Boost Software License, Version 1.0. (See accompanying +# file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +# +# Official repository: https://github.com/boostorg/beast +# + +# Client +add_executable(doc-ssl-client + Jamfile + client.cpp) + +source_group("" FILES + Jamfile + client.cpp) + +target_include_directories(doc-ssl-client + PRIVATE ${PROJECT_SOURCE_DIR}) + +target_link_libraries(doc-ssl-client + PRIVATE Boost::beast OpenSSL::SSL OpenSSL::Crypto) + +set_target_properties(doc-ssl-client + PROPERTIES FOLDER "example-doc-ssl") + +# Server +add_executable(doc-ssl-server + Jamfile + server.cpp) + +source_group("" FILES + Jamfile + server.cpp) + +target_include_directories(doc-ssl-server + PRIVATE ${PROJECT_SOURCE_DIR}) + +target_link_libraries(doc-ssl-server + PRIVATE Boost::beast OpenSSL::SSL OpenSSL::Crypto) + +set_target_properties(doc-ssl-server + PROPERTIES FOLDER "example-doc-ssl") diff --git a/example/doc/ssl/Jamfile b/example/doc/ssl/Jamfile new file mode 100644 index 00000000..426ad697 --- /dev/null +++ b/example/doc/ssl/Jamfile @@ -0,0 +1,20 @@ +import ac ; + +project + : requirements + [ ac.check-library /boost/beast/test//lib-asio-ssl : /boost/beast/test//lib-asio-ssl/static : no ] + ; + +exe doc-ssl-client : + client.cpp + : + coverage:no + ubasan:no + ; + +exe doc-ssl-server : + server.cpp + : + coverage:no + ubasan:no + ; diff --git a/example/doc/ssl/client.cpp b/example/doc/ssl/client.cpp new file mode 100644 index 00000000..9580486d --- /dev/null +++ b/example/doc/ssl/client.cpp @@ -0,0 +1,137 @@ +// +// Copyright (c) 2025 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/boostorg/beast +// + +#include +#include +#include +#include + +#include + +#if defined(BOOST_ASIO_HAS_CO_AWAIT) + +namespace beast = boost::beast; +namespace http = beast::http; +namespace net = boost::asio; +namespace ssl = net::ssl; + +void +print_exception(std::exception_ptr eptr) +{ + if(eptr) + { + try + { + std::rethrow_exception(eptr); + } + catch(std::exception& e) + { + std::cerr << e.what() << std::endl; + } + } +} + +net::awaitable +request(ssl::context& ctx) +{ + auto executor = co_await net::this_coro::executor; + net::ip::tcp::endpoint endpoint{ {}, 8080 }; + ssl::stream stream{ executor, ctx }; + + // Connect TCP socket + co_await stream.lowest_layer().async_connect(endpoint); + + // Set Server Name Indication (SNI) + if(!SSL_set_tlsext_host_name(stream.native_handle(), "localhost")) + { + throw beast::system_error( + static_cast(::ERR_get_error()), + net::error::get_ssl_category()); + } + + // Set a callback to verify that the hostname in the server + // certificate matches the expected value + stream.set_verify_callback(ssl::host_name_verification("localhost")); + + // Perform the SSL handshake + co_await stream.async_handshake(ssl::stream_base::client); + + // Write an HTTP GET request + http::request req{ http::verb::get, "/", 11 }; + req.set(http::field::host, "localhost"); + req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING); + co_await http::async_write(stream, req); + + // Read the response + beast::flat_buffer buf; + http::response res; + co_await http::async_read(stream, buf, res); + + // Print the response body + std::cout << res.body(); + + // Gracefully shutdown the SSL stream + auto [ec] = co_await stream.async_shutdown(net::as_tuple); + if(ec && ec != ssl::error::stream_truncated) + throw boost::system::system_error(ec); +} + +int +main() +{ + try + { + // The io_context is required for all I/O + net::io_context ioc; + + // The SSL context is required, and holds certificates, + // configurations and session related data + ssl::context ctx{ ssl::context::sslv23 }; + + // https://docs.openssl.org/3.4/man3/SSL_CTX_set_options/ + ctx.set_options( + ssl::context::no_sslv2 | ssl::context::default_workarounds | + ssl::context::single_dh_use); + + // set up the peer verification mode so that the TLS/SSL handshake fails + // if the certificate verification is unsuccessful + ctx.set_verify_mode(ssl::verify_peer); + + // The servers's certificate will be verified against this + // certificate authority. + ctx.load_verify_file("ca.crt"); + + // In a real application, the passphrase would be read from + // a secure place, such as a key vault. + ctx.set_password_callback([](auto, auto) { return "123456"; }); + + // Client certificate and private key (if server request for). + ctx.use_certificate_chain_file("client.crt"); + ctx.use_private_key_file("client.key", ssl::context::pem); + + net::co_spawn(ioc, request(ctx), print_exception); + + ioc.run(); + } + catch(std::exception& e) + { + std::cerr << e.what() << std::endl; + } +} + +#else + +int +main(int, char*[]) +{ + std::printf("awaitables require C++20\n"); + return EXIT_FAILURE; +} + +#endif diff --git a/example/doc/ssl/server.cpp b/example/doc/ssl/server.cpp new file mode 100644 index 00000000..d9e11953 --- /dev/null +++ b/example/doc/ssl/server.cpp @@ -0,0 +1,133 @@ +// +// Copyright (c) 2025 Mohammad Nejati +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/boostorg/beast +// + +#include +#include +#include +#include + +#include + +#if defined(BOOST_ASIO_HAS_CO_AWAIT) + +namespace beast = boost::beast; +namespace http = beast::http; +namespace net = boost::asio; +namespace ssl = net::ssl; + +void +print_exception(std::exception_ptr eptr) +{ + if(eptr) + { + try + { + std::rethrow_exception(eptr); + } + catch(std::exception& e) + { + std::cerr << e.what() << std::endl; + } + } +} + +net::awaitable +handle_session(ssl::stream stream) +{ + // Perform the SSL handshake + co_await stream.async_handshake(ssl::stream_base::server); + + // Read and discard a request + beast::flat_buffer buf; + http::request req; + co_await http::async_read(stream, buf, req); + + // Write the response + http::response res; + res.body() = "Hello!"; + co_await http::async_write(stream, res); + + // Gracefully shutdown the SSL stream + auto [ec] = co_await stream.async_shutdown(net::as_tuple); + if(ec && ec != ssl::error::stream_truncated) + throw boost::system::system_error(ec); +} + +net::awaitable +acceptor(ssl::context& ctx) +{ + auto executor = co_await net::this_coro::executor; + net::ip::tcp::endpoint endpoint{ {}, 8080 }; + net::ip::tcp::acceptor acceptor{ executor, endpoint }; + + for(;;) + { + net::co_spawn( + executor, + handle_session({ co_await acceptor.async_accept(), ctx }), + print_exception); + } +} + +int +main() +{ + try + { + // The io_context is required for all I/O + net::io_context ioc; + + // The SSL context is required, and holds certificates, + // configurations and session related data + ssl::context ctx{ ssl::context::sslv23 }; + + // https://docs.openssl.org/3.4/man3/SSL_CTX_set_options/ + ctx.set_options( + ssl::context::no_sslv2 | ssl::context::default_workarounds | + ssl::context::single_dh_use); + + // Comment this line to disable client certificate request. + ctx.set_verify_mode( + ssl::verify_peer | ssl::verify_fail_if_no_peer_cert); + + // The client's certificate will be verified against this + // certificate authority. + ctx.load_verify_file("ca.crt"); + + // In a real application, the passphrase would be read from + // a secure place, such as a key vault. + ctx.set_password_callback([](auto, auto) { return "123456"; }); + + // Server certificate and private key. + ctx.use_certificate_chain_file("server.crt"); + ctx.use_private_key_file("server.key", ssl::context::pem); + + // DH parameters for DHE-based cipher suites + ctx.use_tmp_dh_file("dh4096.pem"); + + net::co_spawn(ioc, acceptor(ctx), print_exception); + + ioc.run(); + } + catch(std::exception& e) + { + std::cerr << e.what() << std::endl; + } +} + +#else + +int +main(int, char*[]) +{ + std::printf("awaitables require C++20\n"); + return EXIT_FAILURE; +} + +#endif diff --git a/test/doc/core_snippets.cpp b/test/doc/core_snippets.cpp index 864c239b..b4ab1257 100644 --- a/test/doc/core_snippets.cpp +++ b/test/doc/core_snippets.cpp @@ -136,4 +136,29 @@ if(ec != net::ssl::error::stream_truncated) } } +void ssl_tls() +{ + net::io_context ioc; + net::ssl::context ctx(net::ssl::context::tlsv12); + net::ssl::stream stream(ioc, ctx); + + //[snippet_core_6 + // Verify the remote server's certificate + ctx.set_verify_mode(net::ssl::verify_peer); + //] + + //[snippet_core_7 + // Verify the remote server's Hostname or IP address + stream.set_verify_callback( + net::ssl::host_name_verification("host.name")); + //] + + //[snippet_core_8 + // Verify the remote client's certificate + ctx.set_verify_mode( + net::ssl::verify_peer | + net::ssl::verify_fail_if_no_peer_cert); + //] +} + } // doc_core_snippets