Skip to content

Commit c5c127d

Browse files
committed
TASK-193: Serve public certificate TLS through native HTTP/1 listeners
Install provider-neutral shared credentials backed by atomic registry snapshots. Pin accepted generations through bounded handshake and raw/provider retirement, retain one native connection owner, propagate real TLS metadata, and make multi-listener binding transactional. Verified behavioral RED and final normal/ASan/UBSan/TSan TLS-off/on gates: 113 programs, 1711 cases and 26962 checks without failures or skips. All 24 installed public shared/static managed/external cells pass; TLS-on cells verify 23 independent-client scenarios. Strict package/declaration audits, plaintext package traffic, check-local, distribution manifests, lint and 61 tooling contracts pass. Preserve H2/H3 and expanded profile rejection; nonlocal execution remains CI/v3-PR owned.
1 parent 7631185 commit c5c127d

49 files changed

Lines changed: 4945 additions & 156 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.gitignore‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,5 @@ __pycache__/
140140
.debug/
141141

142142
!test/integ/native_websocket_regression.py
143+
144+
!scripts/check-v3-public-tls.py

‎Makefile.am‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,3 +557,5 @@ EXTRA_DIST += scripts/test-v3-installed-consumer.py scripts/test-v3-package-work
557557
EXTRA_DIST += docs/v2-to-v3-migration.md docs/v3-api.md docs/task-183-migration-evidence.md examples/README.md
558558

559559
EXTRA_DIST += scripts/audit-v3-native-linkage.sh scripts/test-v3-cutover.py scripts/check-v3-runtime-coexistence.py docs/task-184-cutover-evidence.md
560+
561+
EXTRA_DIST += scripts/check-v3-public-tls.py test/installed/v3_tls_consumer.cpp

‎docs/native-tls-build.md‎

Lines changed: 91 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,11 @@ cross builds must run the smoke on the target in CI.
2626

2727
`<httpserver/features.hpp>` exposes the same compiled `query_features()` declaration
2828
in both builds, without backend headers or configuration macros. Availability of
29-
the provider is distinct from availability of TCP TLS, HTTP/2, and HTTP/3. These
30-
transports remain unavailable until their owning tasks implement them. Native
31-
`listen()` returns `not_supported` before binding any endpoint for a configuration
32-
requesting those transports; `server_options::validate()` remains semantic.
29+
the provider is distinct from availability of TCP TLS, HTTP/2, and HTTP/3. TCP TLS is available in TLS-on builds for certificate-authenticated HTTP/1.0
30+
and HTTP/1.1 in both managed and external modes. HTTP/2 and HTTP/3 remain
31+
unavailable until their owning tasks implement public listeners. `listen()`
32+
rejects unsupported protocol/profile requests before binding;
33+
`server_options::validate()` remains semantic.
3334

3435
Run `make check-v3-native-linkage` in each mode. It inspects the executable behind
3536
any libtool wrapper and the native archive, fails closed on missing artifacts or
@@ -57,7 +58,7 @@ checks configure/build provenance, installed metadata, effective public headers,
5758
recursive dependency graphs and all static-archive members, including unused
5859
members. Provider build proof remains distinct from encrypted listener or QUIC
5960
interoperability proof. Nonlocal BSD, macOS and Windows execution belongs to CI
60-
and the v3 PR; public TLS/H2/H3 listeners and credential installation remain gaps.
61+
and the v3 PR; public H2/H3 listeners and expanded TLS profiles remain later-task work.
6162

6263
Run the clean installed-consumer matrix from a bootstrapped source checkout:
6364

@@ -72,7 +73,7 @@ Repeat `--prerequisite-prefix` for separate prerequisite library prefixes and
7273
(such as macOS SDK zlib metadata). The runner refuses an existing output path,
7374
clears ambient consumer include/link flags, and isolates pkg-config discovery.
7475
It independently builds and installs both modes, compiles shared and static
75-
consumers, checks provider availability and the typed TLS-listener rejection,
76+
consumers, checks provider/TCP TLS availability and typed missing-credential outcomes,
7677
serves bounded plaintext traffic, and compares installed header file sets and
7778
effective declarations. `make check-v3-installed-consumer
7879
INSTALL_AUDIT_ARGS='...'` invokes the same standalone matrix.
@@ -89,8 +90,8 @@ See [TASK-148 local receipts and limitations](task-148-installed-consumer-eviden
8990

9091
TASK-130 establishes a private nonblocking TCP TLS adapter over owned raw I/O
9192
operations. Its [local evidence](task-130-tls-io-evidence.md) covers the adapter
92-
and independent loopback peers. Public TCP TLS availability remains false until
93-
listeners have a usable credential/configuration path.
93+
and independent loopback peers. TASK-193 composes this adapter and the existing credential registry into the
94+
public native TCP listener.
9495

9596
## Installed four-platform package gate
9697

@@ -103,3 +104,85 @@ public transport rejection behavior, and nonlocal CI ownership. The opt-in
103104
runner requires a genuine selected shared OpenSSL provider satisfying configure;
104105
CI prepares checksummed OpenSSL 3.5.9. Policy and orchestration fixture tests run
105106
through `make check-v3-installed-package-fixtures`.
107+
108+
109+
## Public certificate credentials and native TCP TLS
110+
111+
```cpp
112+
#include <httpserver.hpp>
113+
#include <memory>
114+
115+
// certificate_pem includes the leaf followed by its intermediate chain;
116+
// private_key_pem contains the matching unencrypted PEM private key.
117+
auto credentials = std::make_shared<httpserver::server::tls_credentials>();
118+
httpserver::server::tls_credentials_config material;
119+
material.hosts.push_back({"www.example.com", certificate_pem, private_key_pem,
120+
{"http/1.1"}});
121+
auto installed = credentials->replace(material);
122+
if (!installed.ok()) { /* handle the typed outcome */ }
123+
httpserver::server::server_options options;
124+
options.tls().provider = httpserver::server::tls_provider::system_default;
125+
options.tls().profile = httpserver::server::tls_profile::certificates;
126+
options.add_listener({"127.0.0.1", 8443, true, credentials});
127+
httpserver::server::native_server server(options);
128+
// Register ordinary routes, then check server.listen().
129+
```
130+
131+
`tls_credentials` copies share a store. Each successful `replace(config)` builds
132+
and validates a complete immutable generation before atomic publication. Rejected
133+
material (invalid host/default index, malformed PEM, mismatched key or unsupported
134+
ALPN) leaves the previous publication active. The store retains provider objects,
135+
not the PEM input. Caller buffers and handles may die after configuration; the
136+
listener retains the store and accepted handshakes/sessions pin their generation
137+
through retiring I/O completions. No global credential store exists.
138+
139+
Host names use exact canonical DNS matching, with case folding and a final dot
140+
removed. Unknown or absent SNI selects `default_host` (zero by default). Each
141+
listener has its own explicit store; sharing is intentional only when the same
142+
store is assigned. ALPN may contain `http/1.1` or be empty for ordinary HTTP/1
143+
fallback. A listener with HTTP/1.1 disabled must initially use empty ALPN. Later
144+
publication cannot negotiate a disabled ALPN; wire requests for a disabled H1
145+
version are rejected before routing. `exchange::tls()` and request hook contexts
146+
carry library-owned immutable authenticated peer metadata; plaintext uses null,
147+
and anonymous certificate TLS has no client certificate identity.
148+
149+
Native admission reserves its connection budget before constructing TLS state.
150+
Handshakes share the existing bounded provider lane and use an absolute
151+
`timeouts().handshake` deadline. A stalled peer cannot reach an H1 reader or
152+
handler. Drain cancels pending handshakes and stops new admission; established
153+
H1 exchanges retain ordinary drain semantics. `request_stop()` remains safe from
154+
handlers; call blocking `stop()` from the coordinator. External hosts must keep
155+
advancing published readiness, wake and timer deadlines, including empty dispatch
156+
when only a deadline is due (the same contract as plaintext).
157+
158+
Pure structural option errors take precedence in both loop modes. An unavailable
159+
build/protocol/profile returns `not_supported`. TLS-on missing/unpublished
160+
credentials or incompatible ALPN return `invalid_argument`. TLS-off exposes the
161+
same declarations, returns `not_supported` from `replace` and rejects valid TLS
162+
requests before binding. Every endpoint binds before admission activates; failure
163+
on a later bind synchronously releases earlier endpoints and reports no bound
164+
ports. A bind failure is terminal for that server instance.
165+
166+
TASK-193 supports public certificate/default/SNI configuration and replacement
167+
lifetime safety. Public mTLS, external PSK and ACME operations remain TASK-194
168+
work; native requests for unsupported profiles reject honestly. Private profile
169+
suites continue to test their existing support. The rotation example now uses the
170+
public store and demonstrates typed publication outcomes; expanded installed
171+
profile/rotation examples belong to TASK-194.
172+
173+
Run the installed certificate capability matrix after staging a package:
174+
175+
```sh
176+
python3 scripts/check-v3-public-tls.py --prefix /installed/package \
177+
--provider-prefix /selected/openssl --build /identified/build \
178+
--tls yes --output /fresh/public-tls-receipts
179+
```
180+
181+
The same source compiles in TLS-off and TLS-on lanes. The runner checks staged
182+
public header origins, pkg-config identity, shared/static linkage and dependency
183+
graphs; independent Python SSL clients verify chains, named hosts, SNI/default
184+
certificates, ALPN, H1 status/version/body and lifecycle behavior in both loops.
185+
Unknown/absent SNI checks verify the chain and compare the selected certificate
186+
with its independently hostname-verified named certificate. TLS-off runs the same
187+
configuration code and requires typed unavailability. Nonlocal platform checks
188+
remain CI/v3-PR owned.

‎docs/v2-to-v3-migration.md‎

Lines changed: 28 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,17 @@ TCP/TLS/QUIC transport evidence; these are different kinds of proof.
2222
| --- | --- | --- |
2323
| Plaintext HTTP/1.0 and HTTP/1.1 | Managed and external listeners available | `v3_routes`, `v3_external_event_loop` |
2424
| HTTP/1.1 WebSocket Upgrade | Coroutine API available without legacy WS build flags | `v3_websocket_echo` |
25-
| TLS, HTTP/2 and HTTP/3 listeners | `native_server::listen()` returns `not_supported` before binding any endpoint | `v3_protocol_options` |
25+
| Certificate TCP TLS HTTP/1 listeners | Public credential store, managed/external listeners in TLS-on builds | `test/installed/v3_tls_consumer.cpp` |
26+
| HTTP/2 and HTTP/3 listeners | `native_server::listen()` returns `not_supported` before binding any endpoint | `v3_protocol_options` |
2627
| Common HTTP/1.1, HTTP/2, HTTP/3 handlers | Semantic exchange contract exercised for each version | `test/unit/v3_examples_test.cpp` |
27-
| Certificate rotation | Private registry only; no public credential installation or rotation API | `test/examples/v3_tls_rotation.cpp`, TLS rotation tests |
28+
| Certificate rotation | Public `tls_credentials::replace`, immutable accepted selections | `test/examples/v3_tls_rotation.cpp`, installed TLS runner |
2829

2930
`server_options::validate()` checks configuration semantics, not transport
3031
availability. A TLS-on build can validate certificate-profile H2/H3 options and
3132
still refuse `listen()`. `query_features()` distinguishes provider availability
32-
from public transports. Mixed plaintext/TLS configurations are also rejected
33-
before binding. The private audit's encrypted handshakes and Extended CONNECT
34-
journeys do not make public encrypted listeners available.
33+
from public transports. Valid credential-backed mixed plaintext/TLS listeners
34+
serve HTTP/1; missing credentials reject atomically. Private H2/H3 and Extended
35+
CONNECT evidence remains distinct from public protocol availability.
3536

3637
## Server construction and routing
3738

@@ -157,20 +158,28 @@ exclusions, not failed parity observations.
157158

158159
Native TLS selects optional OpenSSL at build time with `--enable-v3-tls`; the
159160
current build gate requires stable OpenSSL **3.5.x patch 9 or later** and its QUIC
160-
TLS callbacks. v2 GnuTLS server options and PSK callbacks have no native public
161-
credential-installation mapping. `server_options::tls()` selects semantic
162-
provider/profile options. External PSK is restricted to HTTP/1; enabling H2/H3
163-
with that profile returns `not_supported`. Initial-handshake client auth supports
164-
optional verified or required certificates; post-handshake auth is unavailable.
165-
166-
The [private rotation demonstration](../test/examples/v3_tls_rotation.cpp) uses
167-
`detail::tls_credentials_registry::replace()` and `acquire()`: successful
168-
publication replaces the active generation, old selections retain their contexts,
169-
and failed replacement leaves the active generation unchanged. Its private header
170-
requires `HTTPSERVER_COMPILATION` and is not installed. The independent verified
171-
TLS peers in [tls_credentials_rotation_test.cpp](../test/unit/tls_credentials_rotation_test.cpp)
172-
prove established sessions continue after replacement. This example is
173-
repository-internal and cannot be compiled against installed public headers.
161+
TLS callbacks. Public certificate values go through `server::tls_credentials`;
162+
`listener_options::credentials` associates the store. `server_options::tls()`
163+
selects provider/profile policy. Public mTLS/PSK/ACME scenarios remain TASK-194
164+
work; their native listener requests reject honestly. Private initial-handshake
165+
client-auth/PSK suites retain existing coverage; post-handshake auth is unavailable.
166+
167+
The [public publication demonstration](../test/examples/v3_tls_rotation.cpp)
168+
uses `server::tls_credentials::replace()`: successful publication replaces the
169+
active generation, accepted sessions retain their contexts, and failed
170+
replacement leaves the active publication unchanged. It uses only installed
171+
public headers. `check-v3-public-tls.py` verifies selected certificates and held
172+
handshakes/connections across replacement through installed shared/static
173+
libraries. The private rotation suite remains supporting lifetime evidence.
174174

175175
BSD, Windows and other nonlocal runs belong to CI and the v3 PR; the
176176
[TASK-183 local evidence](task-183-migration-evidence.md) records what ran locally.
177+
178+
179+
TASK-193 adds public certificate TCP TLS through
180+
[`server::tls_credentials`](native-tls-build.md#public-certificate-credentials-and-native-tcp-tls).
181+
Associate a shared store with each TLS listener and publish PEM values with
182+
`replace`; accepted sessions retain immutable selections during replacement.
183+
Managed and external loops support the same HTTP/1 routes. Public mTLS/PSK/ACME
184+
profiles and H2/H3 listeners remain subsequent repair tasks; unsupported requests
185+
still return typed pre-bind outcomes.

‎scripts/audit-v3-native-linkage.sh‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ src/httpserver/concurrency/cancellation.hpp
6060
src/httpserver/concurrency/resume_signal.hpp
6161
src/httpserver/server/budgets.hpp
6262
src/httpserver/server/options.hpp
63+
src/httpserver/server/tls_credentials.hpp
6364
src/httpserver/server/routes.hpp
6465
src/httpserver/server/configuration.hpp
6566
src/httpserver/server/server.hpp
@@ -81,6 +82,7 @@ src/httpserver/websocket/session.hpp
8182

8283
V3CORE_SOURCES="
8384
src/detail/features.cpp
85+
src/detail/server_tls_credentials.cpp
8486
src/detail/io_operation.cpp
8587
src/detail/io_udp_backend.cpp
8688
src/detail/quic_invariant_header.cpp

‎scripts/check-v3-installed-consumer.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ def package_lane(args, source, output, mode, inventory):
9999
private_libraries = libraries + (shlex.split(env['V3_TLS_LIBS']) if mode == 'yes' else [])
100100
integrations[linkage] = run_suites(inventory, platform.system(), mode, linkage, private_dir, prefix,
101101
source, build, args.compiler, cflags, private_libraries, command, runtime_env, audit)
102+
command([sys.executable, str(source / 'scripts/check-v3-public-tls.py'), '--prefix', str(prefix),
103+
'--provider-prefix', str(args.provider_prefix), '--build', str(build), '--tls', mode,
104+
'--compiler', args.compiler, '--output', str(lane / 'public-tls')], source, runtime_env)
102105
receipt = audit.audit(prefix, build, consumer_dir / ('v3_package_consumer' + suffix), mode,
103106
'strict', args.provider_prefix, args.compiler)
104107
receipt.update(consumer_metadata=metadata, public=public, integrations=integrations)

0 commit comments

Comments
 (0)