From 0882a5cc658c402efdc9e833b2b30ac445dcecca Mon Sep 17 00:00:00 2001 From: Alexander Gerasimov Date: Fri, 9 Oct 2026 11:01:46 +0300 Subject: [PATCH 1/4] Lock the session allocator: workers and the event thread share it Worker threads allocate from stream subpools while the event thread allocates from the session pool. All of them use one allocator, which had no mutex, so concurrent allocations corrupted the heap and crashed the child under load. Set a mutex on the allocator, as mod_http2 does. --- CHANGES | 6 ++++++ mod_http3/src/h3_io.c | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/CHANGES b/CHANGES index fff1d37..8b70905 100644 --- a/CHANGES +++ b/CHANGES @@ -3,6 +3,12 @@ mod_http3 changes Changes are listed most recent first. Security-related entries always appear at the top of their release block. +unreleased +-------------------- + *) Fixed a heap race that crashed the child: worker threads and the event + thread allocated from one session allocator that had no mutex. + [Alexander Gerasimov ] + v0.0.70 (2026-09-06) -------------------- *) SECURITY: Updated the httpd submodule so mpm_event tolerates a connection diff --git a/mod_http3/src/h3_io.c b/mod_http3/src/h3_io.c index 7e74937..7f764b7 100644 --- a/mod_http3/src/h3_io.c +++ b/mod_http3/src/h3_io.c @@ -271,6 +271,15 @@ static apr_status_t spawn_serviced_session(h3_io_t* io, h3q_conn* conn) } apr_allocator_owner_set(allocator, session_pool); apr_pool_tag(session_pool, "h3_session"); + /* Workers allocate from stream subpools while this thread makes its own: share the allocator safely. */ + apr_thread_mutex_t* alloc_mutex = NULL; + if (apr_thread_mutex_create(&alloc_mutex, APR_THREAD_MUTEX_DEFAULT, session_pool) != APR_SUCCESS) + { + apr_pool_destroy(session_pool); + h3q_conn_free(conn); + return APR_EGENERAL; + } + apr_allocator_mutex_set(allocator, alloc_mutex); h3_session* session = NULL; if (h3_session_create(&session, io->server, conn, session_pool) != APR_SUCCESS) From dca480ea8bfaec0d0469442e9df07e988bab7f31 Mon Sep 17 00:00:00 2001 From: Alexander Gerasimov Date: Fri, 9 Oct 2026 11:01:56 +0300 Subject: [PATCH 2/4] Advance pending handshakes while draining A pending handshake counts as an MPM connection. The event thread advanced pending handshakes only when not draining, so a handshake in flight at a graceful restart did not finish or time out. The old child then never reached zero connections and kept the port. --- CHANGES | 5 +++++ mod_http3/src/h3_threads.c | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/CHANGES b/CHANGES index 8b70905..48d2205 100644 --- a/CHANGES +++ b/CHANGES @@ -5,6 +5,11 @@ Security-related entries always appear at the top of their release block. unreleased -------------------- + *) Fixed a graceful restart that never ended: a handshake still pending + when the child started to drain did not finish or time out, so the old + child kept its connection count and the port. + [Alexander Gerasimov ] + *) Fixed a heap race that crashed the child: worker threads and the event thread allocated from one session allocator that had no mutex. [Alexander Gerasimov ] diff --git a/mod_http3/src/h3_threads.c b/mod_http3/src/h3_threads.c index bfe1d66..68dbc50 100644 --- a/mod_http3/src/h3_threads.c +++ b/mod_http3/src/h3_threads.c @@ -89,6 +89,10 @@ void* APR_THREAD_FUNC h3_event_thread(apr_thread_t* thread, void* data) h3q_conn_free(conn); } } + } + if (io->thread_running) + { + /* Also while draining: a pending handshake counts as an MPM connection, so it must finish or time out. */ progress_pending_handshakes(io); } From 870661ad590be58fbcd895afcaead7198dd42f51 Mon Sep 17 00:00:00 2001 From: Alexander Gerasimov Date: Fri, 9 Oct 2026 11:01:57 +0300 Subject: [PATCH 3/4] Block a write-blocked stream again each time nghttp3 offers it nghttp3_conn_resume_stream puts a blocked stream back in the write queue. The module blocked the stream only on the first refusal, so when the peer kept its window shut, nghttp3 offered the stream again and again and the event thread used 100% CPU. --- CHANGES | 4 ++++ mod_http3/src/h3_stream.c | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 48d2205..19132b1 100644 --- a/CHANGES +++ b/CHANGES @@ -5,6 +5,10 @@ Security-related entries always appear at the top of their release block. unreleased -------------------- + *) Fixed an event-thread spin while a client holds its flow-control window + shut: a blocked stream is now blocked again each time nghttp3 offers it. + [Alexander Gerasimov ] + *) Fixed a graceful restart that never ended: a handshake still pending when the child started to drain did not finish or time out, so the old child kept its connection count and the port. diff --git a/mod_http3/src/h3_stream.c b/mod_http3/src/h3_stream.c index 894b5bd..c999028 100644 --- a/mod_http3/src/h3_stream.c +++ b/mod_http3/src/h3_stream.c @@ -128,13 +128,14 @@ void flush_nghttp3(h3_session* session) nghttp3_conn_add_ack_offset(session->ngh3, sid, res.accepted); if (res.blocked) { - /* Send buffer full: skip this stream instead of busy-looping on the same vec. */ + /* Send buffer full: skip this stream instead of busy-looping on the same vec. + * Block it again every time: resume_stream reschedules a blocked stream. */ if (!h3s->write_blocked) { h3s->write_blocked = 1; session->blocked_streams++; - nghttp3_conn_block_stream(session->ngh3, sid); } + nghttp3_conn_block_stream(session->ngh3, sid); continue; } } From edaf8849f88d9638862858366ac2114602b07821 Mon Sep 17 00:00:00 2001 From: Alexander Gerasimov Date: Fri, 9 Oct 2026 13:09:53 +0300 Subject: [PATCH 4/4] v0.0.71 - inherit the HTTP/3 certificate from mod_ssl, select it by SNI 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. Each virtual host on a shared port now presents its own certificate over HTTP/3, chosen by the client SNI; an unmatched name gets the listener default. Names come from ServerName, ServerAlias and the wildcard ServerAlias names that httpd keeps in wild_names, and every entry is matched with ap_strcasecmp_match, the matcher httpd uses for its own virtual hosts. 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. Author: Alexander Gerasimov --- AUTHORS | 3 +- CHANGES | 31 +++++- 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 | 67 +++++++----- docs/containers.md | 2 +- docs/deploy.md | 3 +- interop/httpd.conf | 2 - mod_http3/include/h3_config.h | 33 ++++-- 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 | 138 ++++++++++++++++-------- 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 | 8 +- 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 +- test/http3/test_029_sni.py | 54 ++++++++++ test/pyhttpd/conf/stop.conf.template | 5 + 29 files changed, 302 insertions(+), 157 deletions(-) create mode 100644 test/http3/test_029_sni.py 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 19132b1..00d411c 100644 --- a/CHANGES +++ b/CHANGES @@ -3,8 +3,37 @@ mod_http3 changes Changes are listed most recent first. Security-related entries always appear at the top of their release block. -unreleased +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 ] + + *) 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. Names + are matched as httpd matches virtual hosts, wildcard ServerAlias + included. + [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 ] + *) Fixed an event-thread spin while a client holds its flow-control window shut: a blocked stream is now blocked again each time nghttp3 offers it. [Alexander Gerasimov ] 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..0eb3115 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,24 @@ 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: +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 `ServerAlias` names, wildcards included, the way httpd matches virtual hosts, 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 is used for HTTP/3 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 +# Presents secondary.crt to clients that ask for secondary.example.com 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 +309,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 +340,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..0d9d4a9 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,27 @@ 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. - * @param p Config pool (unused). + * 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, 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. - * @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..c7dce37 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,8 +563,64 @@ static const char* set_h3_alt_svc_max_age(cmd_parms* cmd, void* dummy H3_UNUSED, return NULL; } -int h3_post_config(apr_pool_t* p H3_UNUSED, apr_pool_t* plog H3_UNUSED, apr_pool_t* ptemp, server_rec* s) +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; +} + +/** 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); + /* httpd's ServerAlias matcher: wildcards for wild_names, exact otherwise. */ + if (ap_strcasecmp_match(sni, h->name) == 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; @@ -596,11 +630,33 @@ 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->h3_cert_path && vc->h3_key_path) + 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: httpd keeps exact names in names and wildcards in wild_names. */ + 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; + } + for (int i = 0; vs->wild_names && i < vs->wild_names->nelts; i++) + { + h3_sni_host* e = apr_array_push(sni); + e->name = APR_ARRAY_IDX(vs->wild_names, i, const char*); + e->ctx = vc->ssl_ctx; + } vc->host_port = get_server_port(vs); if (vc->h3_port == 0) { @@ -683,34 +739,24 @@ 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) + if (!conf) { - ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, "mod_http3: H3CertificatePath not readable: %s", conf->h3_cert_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); - f = NULL; - if (apr_file_open(&f, conf->h3_key_path, APR_READ, APR_OS_DEFAULT, ptemp) != APR_SUCCESS) - { - ap_log_error(APLOG_MARK, APLOG_ERR, 0, s, "mod_http3: H3CertificateKeyPath not readable: %s", conf->h3_key_path); - return HTTP_INTERNAL_SERVER_ERROR; - } - apr_file_close(f); + /* 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(); - - 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 +773,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 7f764b7..0612ff1 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..a5d71f7 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); @@ -72,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/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) diff --git a/test/http3/test_029_sni.py b/test/http3/test_029_sni.py new file mode 100644 index 0000000..5a3f10f --- /dev/null +++ b/test/http3/test_029_sni.py @@ -0,0 +1,54 @@ +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(extra_lines=[f"ServerAlias *.wild.{env.http_tld}"]) + 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") + + def test_003_wildcard_alias_gets_its_host_certificate(self, env): + """A name matched only by test1's wildcard ServerAlias gets test1's certificate.""" + assert self._subject(env, "a.wild") == self._subject(env, "test1") + + def test_004_wildcard_does_not_match_the_bare_domain(self, env): + """*.wild. does not match wild., as in httpd, so it falls back to the listener default.""" + assert self._subject(env, "wild") != 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"