From af68854dfed558104d534c66d0321b69ee220b4d Mon Sep 17 00:00:00 2001 From: Alexander Gerasimov Date: Tue, 8 Sep 2026 11:39:27 +0300 Subject: [PATCH 1/2] v0.0.71 - inherit the HTTP/3 certificate from mod_ssl A host with "h3" in Protocols now serves HTTP/3 with the certificate mod_ssl already resolved for it (SSLCertificateFile and mod_md alike), the way mod_http2 rides mod_ssl. The certificate and key are loaded in the ap_ssl_add_cert_files hook, during startup while httpd still runs privileged, so a key readable only by root loads for QUIC as it does for mod_ssl instead of failing in the unprivileged child. H3CertificatePath and H3CertificateKeyPath are removed; drop them from existing configurations. Author: Alexander Gerasimov --- AUTHORS | 3 +- CHANGES | 13 ++++ CMakeLists.txt | 2 +- INSTALL | 13 ++-- README.md | 3 - container/README.md | 8 +-- container/httpd-linux.conf | 2 - container/httpd-windows.conf | 2 - docs/architecture.md | 2 +- docs/configuration.md | 2 +- docs/configuration_httpd.md | 68 +++++++++++--------- docs/containers.md | 2 +- docs/deploy.md | 3 +- interop/httpd.conf | 2 - mod_http3/include/h3_config.h | 30 ++++++--- mod_http3/include/h3_version.h | 4 +- mod_http3/include/quic/detail/h3q_tls.h | 20 +++--- mod_http3/include/quic/h3q.h | 7 +-- mod_http3/src/h3_config.c | 84 +++++++++++-------------- mod_http3/src/h3_hooks.c | 2 +- mod_http3/src/h3_io.c | 4 +- mod_http3/src/h3_server.c | 2 +- mod_http3/src/mod_http3.c | 6 +- mod_http3/src/quic/detail/h3q_tls.c | 19 +++--- mod_http3/src/quic/h3q.c | 5 +- test/http3/env.py | 7 --- test/http3/test_003_directives.py | 9 ++- 27 files changed, 170 insertions(+), 154 deletions(-) diff --git a/AUTHORS b/AUTHORS index 7addfb8..9bc56c1 100644 --- a/AUTHORS +++ b/AUTHORS @@ -18,5 +18,4 @@ Individuals * Jean-Frédéric Clere * Tarek Ibrahim - - + * Alexander Gerasimov https://codeit.guru/ diff --git a/CHANGES b/CHANGES index fff1d37..0916300 100644 --- a/CHANGES +++ b/CHANGES @@ -3,6 +3,19 @@ mod_http3 changes Changes are listed most recent first. Security-related entries always appear at the top of their release block. +v0.0.71 (2026-09-08) +-------------------- + *) SECURITY: Load the HTTP/3 certificate and key in post_config, while httpd + still runs privileged, so a root-only key no longer fails in the + unprivileged child and both mod_ssl and mod_http3 read the same files. + [Alexander Gerasimov ] + + *) A host with "h3" in Protocols now serves HTTP/3 with the certificate + mod_ssl resolved for it (SSLCertificateFile and mod_md alike), the way + mod_http2 rides mod_ssl. H3CertificatePath and H3CertificateKeyPath are + removed; drop them from existing configurations. + [Alexander Gerasimov ] + v0.0.70 (2026-09-06) -------------------- *) SECURITY: Updated the httpd submodule so mpm_event tolerates a connection diff --git a/CMakeLists.txt b/CMakeLists.txt index 08178d7..fc691f7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.26) -project(mod_http3 VERSION 0.0.70) +project(mod_http3 VERSION 0.0.71) # -- Compiler and Build Type Checks -- if(NOT CMAKE_C_COMPILER_ID MATCHES "^(GNU|MSVC)$") diff --git a/INSTALL b/INSTALL index b901ae9..9426aae 100644 --- a/INSTALL +++ b/INSTALL @@ -116,15 +116,13 @@ reject a directly-trusted self-signed leaf. Import ca.crt to trust the server. The script refuses to overwrite existing keys. - The key must be readable by the httpd child user: - - $ chgrp daemon /path/to/httpd/conf/certs/server.key - $ chmod 640 /path/to/httpd/conf/certs/server.key + The key is read at startup, before httpd drops privileges, so the + permissions mod_ssl accepts are enough. 3. Configure httpd. - LoadModule must appear before the block. - H3CertificatePath and H3CertificateKeyPath are required. + LoadModule must appear before the block. A host serves + HTTP/3 when h3 is in its Protocols and mod_ssl has its certificate. Minimal httpd.conf: @@ -142,8 +140,7 @@ SSLCertificateFile conf/certs/server.crt SSLCertificateKeyFile conf/certs/server.key - H3CertificatePath conf/certs/server.crt - H3CertificateKeyPath conf/certs/server.key + Protocols h3 h2 http/1.1 DocumentRoot htdocs diff --git a/README.md b/README.md index 69dc368..ba7bd49 100644 --- a/README.md +++ b/README.md @@ -69,9 +69,6 @@ Listen 4433 https Protocols h3 h2 http/1.1 - H3CertificatePath conf/server.crt - H3CertificateKeyPath conf/server.key - DocumentRoot htdocs Require all granted diff --git a/container/README.md b/container/README.md index 2c0b385..fc8225b 100644 --- a/container/README.md +++ b/container/README.md @@ -89,11 +89,11 @@ Listen 8443 https SSLEngine on + SSLCertificateFile conf/certs/server.crt + SSLCertificateKeyFile conf/certs/server.key Protocols h3 - H3CertificatePath conf/certs/server.crt - H3CertificateKeyPath conf/certs/server.key - H3Port 8443 + H3Port 8443 ``` @@ -110,7 +110,7 @@ podman logs mod_http3_dev | Error | Cause | Fix | |---|---|---| | `Cannot load .../mod_http3.so` | Build failed | Check build output | -| `Invalid command 'H3CertificatePath'` | Module not loaded | Verify LoadModule line | +| `Invalid command 'H3Port'` | Module not loaded | Verify LoadModule line | | `Permission denied` | SELinux | Add `:Z` to volume mounts | | HTTP/3 not working but HTTP/2 is | UDP port not mapped | Check `podman port mod_http3_dev` | diff --git a/container/httpd-linux.conf b/container/httpd-linux.conf index 8395725..e68bef2 100755 --- a/container/httpd-linux.conf +++ b/container/httpd-linux.conf @@ -38,8 +38,6 @@ Listen ${H3_PORT} https Protocols h3 - H3CertificatePath /src/dependencies/httpd-dist/conf/certs/server.crt - H3CertificateKeyPath /src/dependencies/httpd-dist/conf/certs/server.key H3Port ${H3_PORT} H3MaxConnections 100 H3MaxConcurrentStreams 128 diff --git a/container/httpd-windows.conf b/container/httpd-windows.conf index 203ddb8..3763730 100644 --- a/container/httpd-windows.conf +++ b/container/httpd-windows.conf @@ -32,8 +32,6 @@ Listen ${H3_PORT} https Protocols h3 - H3CertificatePath "C:/httpd/conf/certs/server.crt" - H3CertificateKeyPath "C:/httpd/conf/certs/server.key" H3Port ${H3_PORT} H3MaxConnections 100 H3MaxConcurrentStreams 128 diff --git a/docs/architecture.md b/docs/architecture.md index d58d98a..e7d7358 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -47,6 +47,6 @@ bytes as acknowledged once `SSL_write_ex` accepts them. HTTP/3 connections are UDP/QUIC connections, but request processing runs through standard Apache machinery. HTTP/3 is advertised over existing TCP responses using `Alt-Svc`; clients then establish QUIC on the advertised UDP port. -The module uses the first VirtualHost with both `H3CertificatePath` and `H3CertificateKeyPath` for its listener. Name-based virtual host selection then uses the request authority. IP-based virtual hosts remain unsupported because the necessary per-connection local address is not currently recovered. +The module's listener presents the certificate of the first VirtualHost that serves HTTP/3 (`h3` in `Protocols` on a host with a mod_ssl certificate); the certificate is loaded in `post_config`, before privileges drop. Name-based virtual host selection then uses the request authority. IP-based virtual hosts remain unsupported because the necessary per-connection local address is not currently recovered. See the [configuration guide](configuration.md) for operational control points. diff --git a/docs/configuration.md b/docs/configuration.md index b161535..04e0e36 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -4,7 +4,7 @@ Advanced build options, dependency management, and build internals. For quick start and deployment, see [INSTALL](../INSTALL). -For httpd runtime directives (`H3CertificatePath`, VirtualHost), see [httpd Configuration](configuration_httpd.md). +For httpd runtime directives (`Protocols h3`, `H3Port`, VirtualHost), see [httpd Configuration](configuration_httpd.md). ## Build Commands diff --git a/docs/configuration_httpd.md b/docs/configuration_httpd.md index 07b602a..f0d3779 100644 --- a/docs/configuration_httpd.md +++ b/docs/configuration_httpd.md @@ -15,21 +15,26 @@ mod_http3 enables HTTP/3 protocol support in Apache HTTP Server. The module: ## Configuration Directives -### H3CertificatePath +### Enabling HTTP/3 on a host -**Syntax:** `H3CertificatePath /path/to/certificate.pem` -**Context:** server config, virtual host -**Required:** Yes - -Path to the TLS certificate file for HTTP/3 connections. May point to the same file used by `SSLCertificateFile`. +A VirtualHost serves HTTP/3 when `h3` is in its `Protocols` and mod_ssl has a +certificate for it -- the same two things mod_http2 needs for `h2`: -### H3CertificateKeyPath - -**Syntax:** `H3CertificateKeyPath /path/to/private-key.pem` -**Context:** server config, virtual host -**Required:** Yes +```apache + + ServerName www.example.com + SSLEngine on + SSLCertificateFile /etc/httpd/ssl/www.crt + SSLCertificateKeyFile /etc/httpd/ssl/www.key + Protocols h3 h2 http/1.1 + +``` -Path to the TLS private key file for HTTP/3 connections. May point to the same file used by `SSLCertificateKeyFile`. +The certificate and key mod_ssl resolved for the host -- `SSLCertificateFile` +pairs and anything mod_md manages -- are loaded for QUIC as well, during +startup while httpd still runs privileged, so a key readable only by root works +as it does for mod_ssl. A host with `h3` in `Protocols` but no mod_ssl +certificate (`SSLEngine off`, or mod_ssl not loaded) does not serve HTTP/3. ### H3Port @@ -37,7 +42,7 @@ Path to the TLS private key file for HTTP/3 connections. May point to the same f **Context:** server config, virtual host **Default:** the port of the VirtualHost that configured HTTP/3 -UDP port the QUIC listener binds to. When unset, the module reuses the port of the VirtualHost that carries the `H3CertificatePath`/`H3CertificateKeyPath` pair, so TCP (HTTP/1.1, HTTP/2) and UDP (HTTP/3) share the same port number. Set it explicitly to serve HTTP/3 on a different port. +UDP port the QUIC listener binds to. When unset, the module reuses the port of the VirtualHost that serves HTTP/3, so TCP (HTTP/1.1, HTTP/2) and UDP (HTTP/3) share the same port number. Set it explicitly to serve HTTP/3 on a different port. ### H3MaxConcurrentStreams @@ -131,7 +136,7 @@ The idle timeout duration in seconds for QUIC connections. This maps to the stan **Context:** server config, virtual host **Default:** `on` -Whether to issue TLS 1.3 session tickets. A returning client that presents a ticket resumes its session and skips a certificate verification, which is the difference between a two-round-trip and a one-round-trip reconnect. Each worker process holds its own ticket keys, so a client resumes only when it returns to the process that issued its ticket; otherwise the server transparently falls back to a full handshake. Turn this off to force a full handshake on every connection. +Whether to issue TLS 1.3 session tickets. A returning client that presents a ticket resumes its session and skips a certificate verification, which is the difference between a two-round-trip and a one-round-trip reconnect. The ticket keys are created before httpd forks, so every child process resumes tickets issued by any other; a ticket from before a restart falls back to a full handshake. Turn this off to force a full handshake on every connection. ### H3AddressValidation @@ -224,8 +229,10 @@ The module automatically detects the port from the VirtualHost configuration: # HTTP/3 will listen on port 8443 ServerName secure.example.com - H3CertificatePath /etc/httpd/ssl/secure.crt - H3CertificateKeyPath /etc/httpd/ssl/secure.key + SSLEngine on + SSLCertificateFile /etc/httpd/ssl/secure.crt + SSLCertificateKeyFile /etc/httpd/ssl/secure.key + Protocols h3 h2 http/1.1 ``` @@ -233,21 +240,25 @@ Use `H3Port` to bind the QUIC listener to a different UDP port than the VirtualH ### Multiple VirtualHosts -The module uses the **first VirtualHost** that has both `H3CertificatePath` and `H3CertificateKeyPath` configured: +The QUIC listener presents the certificate of the **first VirtualHost** that serves HTTP/3; every other HTTP/3 host still advertises `Alt-Svc` and is selected by request authority: ```apache -# This VirtualHost is used for HTTP/3 +# This VirtualHost's certificate is the one QUIC presents ServerName primary.example.com - H3CertificatePath /etc/httpd/ssl/primary.crt - H3CertificateKeyPath /etc/httpd/ssl/primary.key + SSLEngine on + SSLCertificateFile /etc/httpd/ssl/primary.crt + SSLCertificateKeyFile /etc/httpd/ssl/primary.key + Protocols h3 h2 http/1.1 -# This VirtualHost is ignored for HTTP/3 +# Served over HTTP/3 too, but with primary's certificate ServerName secondary.example.com - H3CertificatePath /etc/httpd/ssl/secondary.crt - H3CertificateKeyPath /etc/httpd/ssl/secondary.key + SSLEngine on + SSLCertificateFile /etc/httpd/ssl/secondary.crt + SSLCertificateKeyFile /etc/httpd/ssl/secondary.key + Protocols h3 h2 http/1.1 ``` @@ -299,8 +310,8 @@ Disable the advertisement entirely with `H3AltSvc off`. The module validates configuration during Apache startup: -1. **Certificate Path Check:** `H3CertificatePath` is configured -2. **Key Path Check:** `H3CertificateKeyPath` is configured +1. At least one host serves HTTP/3: `h3` in `Protocols` on an `SSLEngine on` host +2. That host's certificate and key load If either check fails, Apache refuses to start. @@ -330,15 +341,16 @@ LogLevel http3:trace8 ``` # Successful configuration -h3_post_config: pid=[PID] cert=/path/to/cert key=/path/to/key h3_port=443 mpm=event threaded=1 forked=2 max_threads=25 +mod_http3: serving HTTP/3 with mod_ssl certificate /path/to/cert +h3_post_config: pid=[PID] h3_port=443 mpm=event threaded=1 forked=2 max_threads=25 # Worker thread started h3_child_init worker_thread_main # Errors -mod_http3: H3CertificatePath directive is required but not configured -mod_http3: H3CertificateKeyPath directive is required but not configured +mod_http3: no host serves HTTP/3: add h3 to Protocols on a host with SSLEngine on +mod_http3: loading certificate /path/to/cert with key /path/to/key failed: ... ``` ### Security diff --git a/docs/containers.md b/docs/containers.md index a15278e..ba834d9 100644 --- a/docs/containers.md +++ b/docs/containers.md @@ -200,5 +200,5 @@ curl has no HTTP/3 support. `curl -V | grep HTTP3` confirms it either way. **HTTP/1.1 works but HTTP/3 does not.** Almost always certificate permissions — see above. `podman logs mod_http3` shows the error from the child process. -**`Invalid command 'H3CertificatePath'`.** The configuration you mounted does not +**`Invalid command 'H3Port'`.** The configuration you mounted does not load the module. It needs `LoadModule http3_module modules/mod_http3.so`. diff --git a/docs/deploy.md b/docs/deploy.md index fbdd3da..df44966 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -20,8 +20,7 @@ Listen 4433 https SSLEngine on SSLCertificateFile conf/certs/server.crt SSLCertificateKeyFile conf/certs/server.key - H3CertificatePath conf/certs/server.crt - H3CertificateKeyPath conf/certs/server.key + Protocols h3 h2 http/1.1 DocumentRoot htdocs Require all granted diff --git a/interop/httpd.conf b/interop/httpd.conf index 78dbaf1..44f575c 100644 --- a/interop/httpd.conf +++ b/interop/httpd.conf @@ -45,8 +45,6 @@ Listen 443 https Protocols h3 - H3CertificatePath /interop/certs/cert.pem - H3CertificateKeyPath /interop/certs/priv.key H3Port 443 H3MaxConcurrentStreams 1000 diff --git a/mod_http3/include/h3_config.h b/mod_http3/include/h3_config.h index 9ccc12f..ca421f2 100644 --- a/mod_http3/include/h3_config.h +++ b/mod_http3/include/h3_config.h @@ -38,8 +38,9 @@ struct h3_server_conf { apr_port_t host_port; - const char* h3_cert_path; - const char* h3_key_path; + /** QUIC TLS context, built from mod_ssl's certificate when the host lists h3 in Protocols. */ + struct ssl_ctx_st* ssl_ctx; + apr_port_t h3_port; apr_uint32_t h3_max_concurrent_streams; apr_uint32_t h3_max_connections; @@ -83,8 +84,7 @@ void* h3_create_server_config(apr_pool_t* p, server_rec* s); /** * ap_merge_server_config callback: produce a child vhost config that - * inherits each unset field from the parent. cert/key/h3_port use the - * new value if non-NULL/non-zero, else the base. + * inherits each unset field from the parent. * @param p Pool for the merged config. * @param base_conf Parent h3_server_conf. * @param new_conf Child h3_server_conf. @@ -113,16 +113,26 @@ void* h3_create_dir_config(apr_pool_t* p, char* dir); void* h3_merge_dir_config(apr_pool_t* p, void* base, void* add); /** - * ap_post_config hook: resolve cert/key/h3_port for the listening vhost - * and log the resolved values. No-op in AP_SQ_MS_CREATE_PRE_CONFIG - * (pre-config phase). Returns OK if a fully-configured vhost is found, - * HTTP_INTERNAL_SERVER_ERROR otherwise. + * ap_ssl_add_cert_files hook: mod_ssl runs it for every SSLEngine vhost with + * the certificate and key files it is about to load (SSLCertificateFile plus + * anything mod_md added). When the vhost lists h3 in Protocols, builds its + * QUIC TLS context from them, here, before the server drops privileges. + * @param s The vhost being configured. + * @param p Config pool; owns the context. + * @param cert_files Certificate chain files, const char* elements. + * @param key_files Private key files, const char* elements. + * @return DECLINED, or HTTP_INTERNAL_SERVER_ERROR if the files do not load. + */ +int h3_ssl_add_cert_files(server_rec* s, apr_pool_t* p, apr_array_header_t* cert_files, apr_array_header_t* key_files); + +/** + * ap_post_config hook: fill in defaults on every vhost that serves HTTP/3; + * the first one owns the listener. No-op in AP_SQ_MS_CREATE_PRE_CONFIG. * @param p Config pool (unused). * @param plog Log pool (unused). * @param ptemp Temp pool (unused). * @param s The first server_rec in the configuration. - * @return OK, or HTTP_INTERNAL_SERVER_ERROR if no vhost has both - * H3CertificatePath and H3CertificateKeyPath set. + * @return OK, or HTTP_INTERNAL_SERVER_ERROR if no vhost serves HTTP/3. */ int h3_post_config(apr_pool_t* p, apr_pool_t* plog, apr_pool_t* ptemp, server_rec* s); diff --git a/mod_http3/include/h3_version.h b/mod_http3/include/h3_version.h index 4aa3ee9..41161f8 100644 --- a/mod_http3/include/h3_version.h +++ b/mod_http3/include/h3_version.h @@ -22,13 +22,13 @@ #define MOD_HTTP3_VERSION_MAJOR 0 #define MOD_HTTP3_VERSION_MINOR 0 -#define MOD_HTTP3_VERSION_PATCH 70 +#define MOD_HTTP3_VERSION_PATCH 71 // Construct a 24-bit packed version number from major, minor and patch. Version 1.2.3 becomes 0x010203. #define MOD_HTTP3_MAKE_VERSION(major, minor, patch) (((major) << 16) | ((minor) << 8) | (patch)) #define MOD_HTTP3_VERSION MOD_HTTP3_MAKE_VERSION(MOD_HTTP3_VERSION_MAJOR, MOD_HTTP3_VERSION_MINOR, MOD_HTTP3_VERSION_PATCH) -#define MOD_HTTP3_VERSION_STRING "0.0.70" +#define MOD_HTTP3_VERSION_STRING "0.0.71" #endif /* H3_VERSION_H */ diff --git a/mod_http3/include/quic/detail/h3q_tls.h b/mod_http3/include/quic/detail/h3q_tls.h index 58c51fb..cd197d1 100644 --- a/mod_http3/include/quic/detail/h3q_tls.h +++ b/mod_http3/include/quic/detail/h3q_tls.h @@ -26,15 +26,21 @@ #include "quic/h3q.h" /** - * Build the TLS context the listener serves from: certificate and key from - * @p cfg, "h3" as the only ALPN protocol, and a key log when SSLKEYLOGFILE is - * set. - * @param cfg Configuration supplying the certificate and key paths. - * @param err Buffer receiving the reason on failure; may be NULL. - * @param errlen Capacity of @p err. + * Build the TLS context the listener serves from: the certificate chain files + * with their keys (one per key type, as mod_ssl allows; a missing key file + * means the key sits in the chain file), "h3" as the only ALPN protocol, and + * a key log when SSLKEYLOGFILE is set. Safe before the server forks and drops + * privileges; children inherit the loaded keys. + * @param cert_files Certificate chain files; at least one. + * @param ncerts Number of entries in @p cert_files. + * @param key_files Private key files, matched by index to @p cert_files. + * @param nkeys Number of entries in @p key_files; may be fewer. + * @param session_tickets Non-zero to issue TLS 1.3 session tickets. + * @param err Buffer receiving the reason on failure; may be NULL. + * @param errlen Capacity of @p err. * @return New context, or NULL on failure. */ -SSL_CTX* h3q_tls_ctx_create(const h3q_config* cfg, char* err, size_t errlen); +SSL_CTX* h3q_tls_ctx_create(const char* const* cert_files, size_t ncerts, const char* const* key_files, size_t nkeys, int session_tickets, char* err, size_t errlen); /** * Record a message in a caller-supplied error buffer, appending the OpenSSL diff --git a/mod_http3/include/quic/h3q.h b/mod_http3/include/quic/h3q.h index c5cac25..9ba8ace 100644 --- a/mod_http3/include/quic/h3q.h +++ b/mod_http3/include/quic/h3q.h @@ -34,16 +34,15 @@ typedef struct h3q_stream h3q_stream; * a per-connection setting, applied by h3q_conn_prepare(). */ typedef struct h3q_config { - const char* cert_path; - const char* key_path; + /** TLS context from h3q_tls_ctx_create(); the engine takes its own reference. */ + struct ssl_ctx_st* ssl_ctx; unsigned address_validation : 1; - unsigned session_tickets : 1; } h3q_config; /** * Build the QUIC listener on @p udp_fd, together with the filter BIO that * recovers peer addresses from OpenSSL's accept queue. - * @param cfg Certificate, key and address validation. + * @param cfg TLS context and address validation. * @param udp_fd Pre-opened non-blocking UDP socket bound to the listen port, * borrowed for the engine's lifetime. * @param err Buffer receiving the reason on failure; may be NULL. diff --git a/mod_http3/src/h3_config.c b/mod_http3/src/h3_config.c index 460477e..389c119 100644 --- a/mod_http3/src/h3_config.c +++ b/mod_http3/src/h3_config.c @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -36,6 +37,7 @@ #include "h3_os.h" #include "h3_request.h" #include "mod_http3.h" +#include "quic/detail/h3q_tls.h" apr_port_t get_server_port(const server_rec* s) { @@ -60,8 +62,6 @@ void* h3_merge_server_config(apr_pool_t* p, void* base_conf, void* new_conf) h3_server_conf* base = (h3_server_conf*)base_conf; h3_server_conf* new = (h3_server_conf*)new_conf; - merged->h3_cert_path = new->h3_cert_path ? new->h3_cert_path : base->h3_cert_path; - merged->h3_key_path = new->h3_key_path ? new->h3_key_path : base->h3_key_path; merged->h3_port = new->h3_port ? new->h3_port : base->h3_port; merged->h3_max_concurrent_streams = new->h3_max_concurrent_streams ? new->h3_max_concurrent_streams : base->h3_max_concurrent_streams; merged->h3_max_connections = new->h3_max_connections ? new->h3_max_connections : base->h3_max_connections; @@ -95,28 +95,6 @@ static int mpm_query(int code) return ap_mpm_query(code, &value) == APR_SUCCESS ? value : -1; } -static const char* set_string(cmd_parms* cmd, const char* arg, const char* field) -{ - h3_server_conf* conf = ap_get_module_config(cmd->server->module_config, &http3_module); - if (!conf) - { - ap_log_error(APLOG_MARK, APLOG_ERR, 0, cmd->server, "mod_http3: server config missing in directive"); - return "mod_http3: internal error: no server config"; - } - *(const char**)((char*)conf + (apr_size_t)field) = apr_pstrdup(cmd->pool, arg); - return NULL; -} - -static const char* set_h3_cert_path(cmd_parms* cmd, void* dummy H3_UNUSED, const char* arg) -{ - return set_string(cmd, arg, (const char*)offsetof(h3_server_conf, h3_cert_path)); -} - -static const char* set_h3_key_path(cmd_parms* cmd, void* dummy H3_UNUSED, const char* arg) -{ - return set_string(cmd, arg, (const char*)offsetof(h3_server_conf, h3_key_path)); -} - static const char* set_h3_port(cmd_parms* cmd, void* dummy H3_UNUSED, const char* arg) { if (!arg || !*arg) @@ -585,6 +563,33 @@ static const char* set_h3_alt_svc_max_age(cmd_parms* cmd, void* dummy H3_UNUSED, return NULL; } +static apr_status_t ssl_ctx_cleanup(void* data) +{ + SSL_CTX_free(data); + return APR_SUCCESS; +} + +int h3_ssl_add_cert_files(server_rec* s, apr_pool_t* p, apr_array_header_t* cert_files, apr_array_header_t* key_files) +{ + CHECK(s && p && cert_files && key_files, return DECLINED;); + h3_server_conf* conf = ap_get_module_config(s->module_config, &http3_module); + if (!conf || ap_state_query(AP_SQ_MAIN_STATE) == AP_SQ_MS_CREATE_PRE_CONFIG || !ap_is_allowed_protocol(NULL, NULL, s, "h3") || cert_files->nelts == 0) + { + return DECLINED; + } + /* Still privileged here, so a root-only key loads the way it does for mod_ssl. */ + char err[H3Q_ERRLEN] = {0}; + conf->ssl_ctx = h3q_tls_ctx_create((const char* const*)cert_files->elts, (size_t)cert_files->nelts, (const char* const*)key_files->elts, (size_t)key_files->nelts, conf->h3_session_tickets != H3_FLAG_OFF, err, sizeof(err)); + if (!conf->ssl_ctx) + { + ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, "mod_http3: %s", err); + return HTTP_INTERNAL_SERVER_ERROR; + } + apr_pool_cleanup_register(p, conf->ssl_ctx, ssl_ctx_cleanup, apr_pool_cleanup_null); + ap_log_error(APLOG_MARK, APLOG_INFO, 0, s, "mod_http3: serving HTTP/3 with mod_ssl certificate %s", APR_ARRAY_IDX(cert_files, 0, const char*)); + return DECLINED; +} + int h3_post_config(apr_pool_t* p H3_UNUSED, apr_pool_t* plog H3_UNUSED, apr_pool_t* ptemp, server_rec* s) { CHECK(ptemp); @@ -599,7 +604,7 @@ int h3_post_config(apr_pool_t* p H3_UNUSED, apr_pool_t* plog H3_UNUSED, apr_pool for (server_rec* vs = s; vs; vs = vs->next) { h3_server_conf* vc = ap_get_module_config(vs->module_config, &http3_module); - if (vc->h3_cert_path && vc->h3_key_path) + if (vc->ssl_ctx) { vc->host_port = get_server_port(vs); if (vc->h3_port == 0) @@ -683,34 +688,21 @@ int h3_post_config(apr_pool_t* p H3_UNUSED, apr_pool_t* plog H3_UNUSED, apr_pool { vc->h3_idle_timeout = H3_IDLE_TIMEOUT_DEFAULT; } - conf = vc; - break; + if (!conf) + { + conf = vc; /* the first host owns the listener; the rest still advertise it */ + ap_log_error(APLOG_MARK, APLOG_INFO, 0, vs, "h3_post_config: pid=%d h3_port=%d mpm=%s threaded=%d forked=%d max_threads=%d", h3_getpid(), (int)vc->h3_port, ap_show_mpm(), mpm_query(AP_MPMQ_IS_THREADED), mpm_query(AP_MPMQ_IS_FORKED), mpm_query(AP_MPMQ_MAX_THREADS)); + } } } - CHECK(conf && conf->h3_cert_path && conf->h3_key_path, return HTTP_INTERNAL_SERVER_ERROR;); - - /* Validate cert and key files are readable */ - apr_file_t* f = NULL; - if (apr_file_open(&f, conf->h3_cert_path, APR_READ, APR_OS_DEFAULT, ptemp) != APR_SUCCESS) - { - ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, "mod_http3: H3CertificatePath not readable: %s", conf->h3_cert_path); - return HTTP_INTERNAL_SERVER_ERROR; - } - apr_file_close(f); - f = NULL; - - if (apr_file_open(&f, conf->h3_key_path, APR_READ, APR_OS_DEFAULT, ptemp) != APR_SUCCESS) + if (!conf) { - ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, "mod_http3: H3CertificateKeyPath not readable: %s", conf->h3_key_path); + ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, "mod_http3: no host serves HTTP/3: add h3 to Protocols on a host with SSLEngine on"); return HTTP_INTERNAL_SERVER_ERROR; } - apr_file_close(f); h3_request_init(); - - ap_log_error(APLOG_MARK, APLOG_INFO, 0, s, "h3_post_config: pid=%d cert=%s key=%s h3_port=%d mpm=%s threaded=%d forked=%d max_threads=%d", h3_getpid(), conf->h3_cert_path, conf->h3_key_path, (int)conf->h3_port, ap_show_mpm(), mpm_query(AP_MPMQ_IS_THREADED), mpm_query(AP_MPMQ_IS_FORKED), - mpm_query(AP_MPMQ_MAX_THREADS)); return OK; } @@ -727,8 +719,6 @@ void* h3_merge_dir_config(apr_pool_t* p H3_UNUSED, void* base, void* add H3_UNUS } const command_rec h3_cmds[] = { - AP_INIT_TAKE1("H3CertificatePath", set_h3_cert_path, NULL, RSRC_CONF, "Path to the SSL certificate file for HTTP/3"), - AP_INIT_TAKE1("H3CertificateKeyPath", set_h3_key_path, NULL, RSRC_CONF, "Path to the SSL certificate key file for HTTP/3"), AP_INIT_TAKE1("H3Port", set_h3_port, NULL, RSRC_CONF, "UDP port to listen on for QUIC/HTTP-3 (default: same as main server)"), AP_INIT_TAKE1("H3MaxConcurrentStreams", set_h3_max_concurrent_streams, NULL, RSRC_CONF, "Maximum number of concurrent HTTP/3 streams per connection (default: 100)"), AP_INIT_TAKE1("H3MaxConnections", set_h3_max_connections, NULL, RSRC_CONF, "Maximum concurrent QUIC/HTTP/3 connections per child process (default: 256)"), diff --git a/mod_http3/src/h3_hooks.c b/mod_http3/src/h3_hooks.c index 4090372..fdd6f78 100644 --- a/mod_http3/src/h3_hooks.c +++ b/mod_http3/src/h3_hooks.c @@ -84,7 +84,7 @@ int h3_hook_fixups(request_rec* r) h3_server_conf* conf = ap_get_module_config(r->server->module_config, &http3_module); - if (!conf || !conf->h3_cert_path || !conf->h3_key_path || conf->h3_port == 0) + if (!conf || !conf->ssl_ctx || conf->h3_port == 0) { return DECLINED; } diff --git a/mod_http3/src/h3_io.c b/mod_http3/src/h3_io.c index 7e74937..e190205 100644 --- a/mod_http3/src/h3_io.c +++ b/mod_http3/src/h3_io.c @@ -133,10 +133,8 @@ apr_status_t h3_io_listen_start(apr_pool_t* pchild, server_rec* s, h3_server_con char qerr[H3Q_ERRLEN] = {0}; h3q_config qcfg = { - .cert_path = conf->h3_cert_path, - .key_path = conf->h3_key_path, + .ssl_ctx = conf->ssl_ctx, .address_validation = (conf->h3_address_validation != H3_FLAG_OFF), - .session_tickets = (conf->h3_session_tickets != H3_FLAG_OFF), }; io->qengine = h3q_engine_create(&qcfg, udp_fd, qerr, sizeof(qerr)); if (!io->qengine) diff --git a/mod_http3/src/h3_server.c b/mod_http3/src/h3_server.c index ec22a21..10c2cfa 100644 --- a/mod_http3/src/h3_server.c +++ b/mod_http3/src/h3_server.c @@ -86,7 +86,7 @@ static h3_server_conf* find_h3_server(server_rec* s, server_rec** out_server) while (current) { h3_server_conf* tmp = ap_get_module_config(current->module_config, &http3_module); - if (tmp && tmp->h3_cert_path && tmp->h3_key_path && !conf) + if (tmp && tmp->ssl_ctx && !conf) { conf = tmp; conf->host_port = get_server_port(current); diff --git a/mod_http3/src/mod_http3.c b/mod_http3/src/mod_http3.c index fb8db48..da71e89 100644 --- a/mod_http3/src/mod_http3.c +++ b/mod_http3/src/mod_http3.c @@ -40,7 +40,11 @@ static void register_hooks(apr_pool_t* p H3_UNUSED) { ap_hook_handler(h3_status_handler, NULL, NULL, APR_HOOK_MIDDLE); - ap_hook_post_config(h3_post_config, NULL, NULL, APR_HOOK_MIDDLE); + /* mod_ssl hands out the certificate files it resolved before it loads them; + * post_config runs after it so those files are known by then. */ + static const char* const after_ssl[] = {"mod_ssl.c", NULL}; + ap_hook_ssl_add_cert_files(h3_ssl_add_cert_files, NULL, NULL, APR_HOOK_LAST); + ap_hook_post_config(h3_post_config, after_ssl, NULL, APR_HOOK_MIDDLE); ap_hook_create_request(h3_hook_http_create_request, NULL, NULL, APR_HOOK_REALLY_FIRST); ap_hook_pre_read_request(h3_hook_pre_read_request, NULL, NULL, APR_HOOK_MIDDLE); ap_hook_post_read_request(h3_hook_post_read_request, NULL, NULL, APR_HOOK_REALLY_FIRST); diff --git a/mod_http3/src/quic/detail/h3q_tls.c b/mod_http3/src/quic/detail/h3q_tls.c index 62a476c..6e2e72d 100644 --- a/mod_http3/src/quic/detail/h3q_tls.c +++ b/mod_http3/src/quic/detail/h3q_tls.c @@ -74,9 +74,10 @@ static void h3q_tls_keylog_cb(const SSL* ssl, const char* line) } } -SSL_CTX* h3q_tls_ctx_create(const h3q_config* cfg, char* err, size_t errlen) +SSL_CTX* h3q_tls_ctx_create(const char* const* cert_files, size_t ncerts, const char* const* key_files, size_t nkeys, int session_tickets, char* err, size_t errlen) { - CHECK(cfg); + CHECK(cert_files && ncerts > 0); + CHECK(key_files || nkeys == 0); SSL_CTX* ssl_ctx = SSL_CTX_new(OSSL_QUIC_server_method()); if (!ssl_ctx) @@ -88,17 +89,21 @@ SSL_CTX* h3q_tls_ctx_create(const h3q_config* cfg, char* err, size_t errlen) SSL_CTX_set_min_proto_version(ssl_ctx, TLS1_3_VERSION); SSL_CTX_set_max_proto_version(ssl_ctx, TLS1_3_VERSION); - if (SSL_CTX_use_certificate_chain_file(ssl_ctx, cfg->cert_path) <= 0 || SSL_CTX_use_PrivateKey_file(ssl_ctx, cfg->key_path, SSL_FILETYPE_PEM) <= 0) + for (size_t i = 0; i < ncerts; i++) { - h3q_tls_error(err, errlen, "loading the certificate or private key failed"); - SSL_CTX_free(ssl_ctx); - return NULL; + const char* key_file = i < nkeys ? key_files[i] : cert_files[i]; + if (SSL_CTX_use_certificate_chain_file(ssl_ctx, cert_files[i]) <= 0 || SSL_CTX_use_PrivateKey_file(ssl_ctx, key_file, SSL_FILETYPE_PEM) <= 0) + { + h3q_tls_error(err, errlen, "loading certificate %s with key %s failed", cert_files[i], key_file); + SSL_CTX_free(ssl_ctx); + return NULL; + } } static const unsigned char sid_ctx[] = "mod_http3"; SSL_CTX_set_session_id_context(ssl_ctx, sid_ctx, sizeof(sid_ctx) - 1); - if (!cfg->session_tickets) + if (!session_tickets) { /* TLS 1.3 resumption travels in tickets, so issuing none turns it off. */ SSL_CTX_set_num_tickets(ssl_ctx, 0); diff --git a/mod_http3/src/quic/h3q.c b/mod_http3/src/quic/h3q.c index db86952..ff71440 100644 --- a/mod_http3/src/quic/h3q.c +++ b/mod_http3/src/quic/h3q.c @@ -39,12 +39,13 @@ h3q_engine* h3q_engine_create(const h3q_config* cfg, int udp_fd, char* err, size } engine->peer_addr_ex_index = -1; - engine->ssl_ctx = h3q_tls_ctx_create(cfg, err, errlen); - if (!engine->ssl_ctx) + if (!cfg->ssl_ctx || !SSL_CTX_up_ref(cfg->ssl_ctx)) { + h3q_tls_error(err, errlen, "no TLS context to serve from"); h3q_engine_destroy(engine); return NULL; } + engine->ssl_ctx = cfg->ssl_ctx; BIO_METHOD* bm = BIO_meth_new(BIO_TYPE_FILTER | BIO_get_new_index(), "h3q_peer_addr"); if (!bm) diff --git a/test/http3/env.py b/test/http3/env.py index 2b821aa..8d4fdde 100644 --- a/test/http3/env.py +++ b/test/http3/env.py @@ -77,8 +77,6 @@ def add_vhost_test1( proxy_self=False, h2proxy_self=False, h3_port=True, - h3_cert_path=None, - h3_key_path=None, h3_max_concurrent_streams=None, h3_stream_buffer_size=None, h3_max_request_body_size=None, @@ -107,11 +105,6 @@ def add_vhost_test1( if h3_port: port = h3_port if not isinstance(h3_port, bool) else self.env.https_port self.add(f"H3Port {port}") - - cert = h3_cert_path if h3_cert_path else self.env.test_cert_file - key = h3_key_path if h3_key_path else self.env.test_key_file - self.add(f"H3CertificatePath {cert}") - self.add(f"H3CertificateKeyPath {key}") if h3_max_concurrent_streams is not None: self.add(f"H3MaxConcurrentStreams {h3_max_concurrent_streams}") diff --git a/test/http3/test_003_directives.py b/test/http3/test_003_directives.py index 29a35c9..0d1bcb7 100644 --- a/test/http3/test_003_directives.py +++ b/test/http3/test_003_directives.py @@ -30,12 +30,11 @@ def test_002_h3port_in_vhost(self, env): assert "H3Port" in conf assert str(env.https_port) in conf - def test_003_h3_cert_directives_in_vhost(self, env): + def test_003_h3_cert_inherited_from_mod_ssl(self, env): + # No mod_http3 certificate directive exists; mod_ssl's pair is what QUIC serves. conf = _read_test_conf(env) - assert "H3CertificatePath" in conf - assert "H3CertificateKeyPath" in conf - assert env.test_cert_file in conf - assert env.test_key_file in conf + assert "SSLCertificateFile" in conf + assert env.apache_restart() == 0 def test_004_protocols_h3_in_vhost(self, env): conf = _read_test_conf(env) From ebaef8b3fc59437d8d1646c3f9dbfd7666ab860a Mon Sep 17 00:00:00 2001 From: Alexander Gerasimov Date: Tue, 8 Sep 2026 13:50:19 +0300 Subject: [PATCH 2/2] v0.0.71 - select the HTTP/3 certificate by SNI Each virtual host on a shared port now presents its own certificate over HTTP/3, chosen by the client SNI (ServerName and exact ServerAlias names); an unmatched name gets the listener default. A cert_cb on the listener context applies the matched host certificate, key and chain to the connection: SSL_set_SSL_CTX does not switch the certificate of a QUIC connection. Landing it exposed that the module ran with AP_MODULE_FLAG_NONE, so a host without H3 directives shared the main server configuration; per-host H3AltSvc and H3AltSvcMaxAge were silently ignored. Set ALWAYS_MERGE. Test suite: stop.conf never named a pid file, so on builds whose default is run/httpd.pid "apachectl -k stop" stopped nothing and every restart talked to the previous server. With that fixed the suite runs in under two minutes: 122 passed, 2 failed (test_028, the 2.4.68 ErrorDocument crash), 5 skipped. Author: Alexander Gerasimov --- CHANGES | 16 +++++++++ docs/configuration_httpd.md | 5 ++- mod_http3/include/h3_config.h | 5 +-- mod_http3/src/h3_config.c | 49 +++++++++++++++++++++++++++- mod_http3/src/mod_http3.c | 2 +- test/http3/test_029_sni.py | 46 ++++++++++++++++++++++++++ test/pyhttpd/conf/stop.conf.template | 5 +++ 7 files changed, 121 insertions(+), 7 deletions(-) create mode 100644 test/http3/test_029_sni.py diff --git a/CHANGES b/CHANGES index 0916300..98c9916 100644 --- a/CHANGES +++ b/CHANGES @@ -16,6 +16,22 @@ v0.0.71 (2026-09-08) removed; drop them from existing configurations. [Alexander Gerasimov ] + *) Select the HTTP/3 certificate by SNI, so each virtual host on a shared + port serves its own certificate instead of the first host's. + [Alexander Gerasimov ] + + *) Give every virtual host its own mod_http3 configuration + (AP_MODULE_FLAG_ALWAYS_MERGE). A host without H3 directives used to share + the main server's, so per-host settings such as H3AltSvc and H3AltSvcMaxAge + were silently ignored and hosts could not carry their own certificate. + [Alexander Gerasimov ] + + *) Test suite: stop.conf now names the same pid file as httpd.conf, so + "apachectl -k stop" actually stops the server on httpd builds whose + default pid file lives in run/; before, every restart in the suite kept + talking to the previous server. + [Alexander Gerasimov ] + v0.0.70 (2026-09-06) -------------------- *) SECURITY: Updated the httpd submodule so mpm_event tolerates a connection diff --git a/docs/configuration_httpd.md b/docs/configuration_httpd.md index f0d3779..6ab59d6 100644 --- a/docs/configuration_httpd.md +++ b/docs/configuration_httpd.md @@ -240,10 +240,9 @@ Use `H3Port` to bind the QUIC listener to a different UDP port than the VirtualH ### Multiple VirtualHosts -The QUIC listener presents the certificate of the **first VirtualHost** that serves HTTP/3; every other HTTP/3 host still advertises `Alt-Svc` and is selected by request authority: +All HTTP/3 hosts on a port share one QUIC listener. The certificate is chosen by the client's SNI: a host is matched on its `ServerName` and exact `ServerAlias` names (wildcard aliases are not matched) and presents its own certificate; a name that matches no host gets the listener's default, the certificate of the first HTTP/3 host in the configuration chain. Requests are then routed by authority as for TCP: ```apache -# This VirtualHost's certificate is the one QUIC presents ServerName primary.example.com SSLEngine on @@ -252,7 +251,7 @@ The QUIC listener presents the certificate of the **first VirtualHost** that ser Protocols h3 h2 http/1.1 -# Served over HTTP/3 too, but with primary's certificate +# Presents secondary.crt to clients that ask for secondary.example.com ServerName secondary.example.com SSLEngine on diff --git a/mod_http3/include/h3_config.h b/mod_http3/include/h3_config.h index ca421f2..0d9d4a9 100644 --- a/mod_http3/include/h3_config.h +++ b/mod_http3/include/h3_config.h @@ -127,8 +127,9 @@ int h3_ssl_add_cert_files(server_rec* s, apr_pool_t* p, apr_array_header_t* cert /** * ap_post_config hook: fill in defaults on every vhost that serves HTTP/3; - * the first one owns the listener. No-op in AP_SQ_MS_CREATE_PRE_CONFIG. - * @param p Config pool (unused). + * the first one owns the listener, and an SNI callback swaps in each other + * host's certificate by name. No-op in AP_SQ_MS_CREATE_PRE_CONFIG. + * @param p Config pool; owns the SNI host table. * @param plog Log pool (unused). * @param ptemp Temp pool (unused). * @param s The first server_rec in the configuration. diff --git a/mod_http3/src/h3_config.c b/mod_http3/src/h3_config.c index 389c119..be97c88 100644 --- a/mod_http3/src/h3_config.c +++ b/mod_http3/src/h3_config.c @@ -590,8 +590,36 @@ int h3_ssl_add_cert_files(server_rec* s, apr_pool_t* p, apr_array_header_t* cert return DECLINED; } -int h3_post_config(apr_pool_t* p H3_UNUSED, apr_pool_t* plog H3_UNUSED, apr_pool_t* ptemp, server_rec* s) +/** One HTTP/3 host name and the TLS context that carries its certificate. */ +typedef struct { + const char* name; + SSL_CTX* ctx; +} h3_sni_host; + +/// cert_cb: serve each host its own certificate over one listener, selected by SNI. +/// SSL_set_SSL_CTX does not switch the certificate of a QUIC connection; applying +/// the matched host's certificate, key and chain to the connection does. +static int h3_sni_select_cert(SSL* ssl, void* arg) +{ + const apr_array_header_t* hosts = arg; + const char* sni = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name); + for (int i = 0; sni && i < hosts->nelts; i++) + { + const h3_sni_host* h = &APR_ARRAY_IDX(hosts, i, h3_sni_host); + if (apr_cstr_casecmp(h->name, sni) == 0) + { + STACK_OF(X509)* chain = NULL; + SSL_CTX_get0_chain_certs(h->ctx, &chain); + return SSL_use_certificate(ssl, SSL_CTX_get0_certificate(h->ctx)) == 1 && SSL_use_PrivateKey(ssl, SSL_CTX_get0_privatekey(h->ctx)) == 1 && (!chain || SSL_set1_chain(ssl, chain) == 1); + } + } + return 1; /* no match: the listener's own certificate */ +} + +int h3_post_config(apr_pool_t* p, apr_pool_t* plog H3_UNUSED, apr_pool_t* ptemp, server_rec* s) +{ + CHECK(p); CHECK(ptemp); CHECK(s); h3_server_conf* conf = NULL; @@ -601,11 +629,27 @@ int h3_post_config(apr_pool_t* p H3_UNUSED, apr_pool_t* plog H3_UNUSED, apr_pool return OK; } + /* Names of every HTTP/3 host, so the listener can pick a certificate by SNI. */ + apr_array_header_t* sni = apr_array_make(p, 4, sizeof(h3_sni_host)); + for (server_rec* vs = s; vs; vs = vs->next) { h3_server_conf* vc = ap_get_module_config(vs->module_config, &http3_module); if (vc->ssl_ctx) { + if (vs->server_hostname) + { + h3_sni_host* e = apr_array_push(sni); + e->name = vs->server_hostname; + e->ctx = vc->ssl_ctx; + } + /* ServerAlias exact names; wildcards are not matched (ponytail: exact only). */ + for (int i = 0; vs->names && i < vs->names->nelts; i++) + { + h3_sni_host* e = apr_array_push(sni); + e->name = APR_ARRAY_IDX(vs->names, i, const char*); + e->ctx = vc->ssl_ctx; + } vc->host_port = get_server_port(vs); if (vc->h3_port == 0) { @@ -702,6 +746,9 @@ int h3_post_config(apr_pool_t* p H3_UNUSED, apr_pool_t* plog H3_UNUSED, apr_pool return HTTP_INTERNAL_SERVER_ERROR; } + /* The listener serves conf's certificate by default and swaps by SNI. */ + SSL_CTX_set_cert_cb(conf->ssl_ctx, h3_sni_select_cert, sni); + h3_request_init(); return OK; } diff --git a/mod_http3/src/mod_http3.c b/mod_http3/src/mod_http3.c index da71e89..a5d71f7 100644 --- a/mod_http3/src/mod_http3.c +++ b/mod_http3/src/mod_http3.c @@ -76,5 +76,5 @@ HTTP3_PUBLIC module http3_module = { h3_merge_server_config, /* merge per-server config structures */ h3_cmds, /* command apr_table_t */ register_hooks, /* register hooks */ - AP_MODULE_FLAG_NONE /* flags */ + AP_MODULE_FLAG_ALWAYS_MERGE /* every vhost gets its own config, H3 directives or not */ }; diff --git a/test/http3/test_029_sni.py b/test/http3/test_029_sni.py new file mode 100644 index 0000000..30e683f --- /dev/null +++ b/test/http3/test_029_sni.py @@ -0,0 +1,46 @@ +import os + +import pytest + +from pyhttpd.certs import HttpdTestCA + + +class TestSni: + """Each HTTP/3 host presents its own certificate, chosen by the client's SNI.""" + + @pytest.fixture(autouse=True, scope="class") + def _class_scope(self, env): + from .env import H3Conf + + # A second certificate with a different subject than the shared test one. + name = f"test2.{env.http_tld}" + creds = HttpdTestCA.create_root(name=name, store_dir=env.gen_dir) + cert = os.path.join(env.gen_dir, "test2-sni.crt") + key = os.path.join(env.gen_dir, "test2-sni.key") + creds.save_cert_pem(cert) + creds.save_pkey_pem(key) + + conf = H3Conf(env) + conf.add_vhost_test1() + conf.start_vhost([name], doc_root="htdocs/two", with_ssl=True, with_certificates=False) + conf.add(f"SSLCertificateFile {cert}") + conf.add(f"SSLCertificateKeyFile {key}") + conf.add("Protocols h3 http/1.1") + conf.end_vhost() + conf.install() + assert env.apache_restart() == 0 + + def _subject(self, env, host): + url = env.mkurl("https", host, "/index.html") + r = env.curl_get(url, options=["--http3-only", "-k", "-v"]) + assert r.exit_code == 0, r.stderr + r.stdout + assert r.response["protocol"] == "HTTP/3" + lines = [l for l in r.stderr.splitlines() if "subject:" in l] + assert lines, r.stderr + return lines[0] + + def test_001_second_host_gets_its_own_certificate(self, env): + assert f"test2.{env.http_tld}" in self._subject(env, "test2") + + def test_002_first_host_keeps_the_shared_certificate(self, env): + assert f"test2.{env.http_tld}" not in self._subject(env, "test1") diff --git a/test/pyhttpd/conf/stop.conf.template b/test/pyhttpd/conf/stop.conf.template index 21bae84..e19c263 100644 --- a/test/pyhttpd/conf/stop.conf.template +++ b/test/pyhttpd/conf/stop.conf.template @@ -5,6 +5,11 @@ ServerName localhost ServerRoot "${server_dir}" +# Same pid file as httpd.conf, or -k stop finds nothing on builds whose +# compiled-in default is run/httpd.pid and every "restart" talks to the old server. +DefaultRuntimeDir logs +PidFile httpd.pid + Include "conf/modules.conf" DocumentRoot "${server_dir}/htdocs"