docs(listener): where the selfSigned QUIC certificate lives now, and that it needs no CLI

The README's self-signed section, its dependency list, the consumer
notes and the agent notes described the openssl CLI and the shared
/tmp/crafter-network-quic-cert/. They now give the per-user location
and its fallbacks, the day-before-expiry rollover (which is when a
pinned hash changes), what keeps processes that start together on one
pair, and that nothing has to be installed. The agent notes' dev
environment says where the Flatpak keeps the certificate between runs,
drops the two gotchas the move resolved (the CLI, and the comments in
-ListenerQUIC.cpp and ClientQUIC.cppm that disagreed with the code),
and notes the module-internal functions :TLS declares for the listener.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jorijn van der Graaf 2026-11-01 01:57:57 +01:00
commit 76e927595d
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
3 changed files with 11 additions and 11 deletions

View file

@ -84,7 +84,7 @@ A route map is `std::unordered_map<std::string, std::function<HTTPResponse(const
**`ListenerQUIC(port, alpn, QUICServerCredentials, connectCallback(ClientQUIC*), concurrentClientLimit, totalClientLimit)`** listens on every address of both families and throws `QUICException` when it cannot. **`ListenerQUIC(bindAddress, port, alpn, ...)`**, same arguments after the address, listens on one: numeric only (`127.0.0.1` or `::1` for this machine alone, `0.0.0.0` or `::` for every address of one family; `localhost` throws `QUICException`). msquic still binds the UDP port on every address and drops, at its own layer, a connection sent to another one: the client fails within milliseconds with `QUIC_STATUS_ALPN_NEG_FAILURE` (0x5c) and connectCallback never hears of it. A listener on `127.0.0.1` is not reachable through `::1`, so its clients dial `127.0.0.1`, not `localhost`. `Port()` is the UDP port it bound: pass port 0 to let msquic pick a free one and read it back (still valid after `Stop()`, 0 on a moved-from listener). The callback's `ClientQUIC*` is heap-allocated and yours: set `OnStream` / `OnDatagram` on it, keep it as long as the connection should live, `Stop()` or `delete` it to shut the connection down, and `delete` it in any case, since only that frees it (a connection the peer or the idle timeout ended has its msquic handle closed for it, but the object stays). `ListenSyncSync()` runs the accept loop on the calling thread and the callback there; `ListenSyncAsync()` runs the callback as a pool task; `ListenAsyncSync()` and `ListenAsyncAsync()` run the loop on a thread of their own, the second with the callback on the pool. `Stop()` stops accepting; the destructor stops, joins, and deletes peers accepted but not yet handed out. `totalClientLimit` ends the hand-off loop after that many connections (later ones queue until the listener is destroyed); `concurrentClientLimit` is stored and not enforced.
- `selfSigned` shells out to the `openssl` CLI (install it, or pass `certPath`/`keyPath`; without it the constructor throws `QUICException`) and caches the pair at `/tmp/crafter-network-quic-cert/{cert,key}.pem`, reused across restarts while valid (inside crafter-build's Flatpak, `/tmp` is a per-app directory discarded once no instance is running, so there the pair rarely survives between runs; pass `certPath`/`keyPath` for a stable certificate): ECDSA P-256, `CN=localhost`, SANs `localhost`, `127.0.0.1`, `::1`, 10 days, with the extensions Chromium's `serverCertificateHashes` demands. `GetSelfSignedCertificatePath()` is that file, and `ComputeCertificateHashSHA256(certPath)` (also through `openssl`) is the 32-byte digest a browser pins, see "Browser build". A different certificate from the TLS partition's: a client of both needs both.
- `selfSigned` serves one development pair per user, made in-process (nothing to install) and kept at `$XDG_CACHE_HOME/crafter-network/quic-cert/{cert,key}.pem` (else `~/.cache/...`, else `$XDG_RUNTIME_DIR/...`; inside crafter-build's Flatpak `XDG_CACHE_HOME` is `~/.var/app/net.catcrafts.CrafterBuild/cache`). The directory is made 0700 and refused, with `QUICException`, if another user owns it; with none of those variables set the constructor throws too. The pair is reused across restarts until it has less than a day of its 10 left, then the next process that needs it makes a new one, so a browser's pinned hash survives restarts but not that rollover; a process keeps the pair it loaded for all its listeners until then. Processes starting together share one pair (a lock covers check and replacement; the listener loads it from memory), and a pair that does not parse or match is replaced. ECDSA P-256, `CN=localhost`, SANs `localhost`, `127.0.0.1`, `::1`, with the extensions Chromium's `serverCertificateHashes` demands. `GetSelfSignedCertificatePath()` is that file, and `ComputeCertificateHashSHA256(certPath)` is the 32-byte SHA-256 of a PEM file's certificate, computed in-process (`QUICException` for a file it cannot read or parse), the digest a browser pins, see "Browser build". A different certificate from the TLS partition's: a client of both needs both.
### HTTP/3 and WebTransport
@ -136,5 +136,5 @@ On a `wasm32-*` target the same `ClientHTTP` and `ClientQUIC` compile against th
- Start the pool first, and end a QUIC test with `std::fflush(stdout); std::_Exit(code);`, or delete every accepted peer and `Stop()` every client before `main` returns; otherwise the exit waits for the idle timeout.
- crafter-build runs tests in parallel, so each test binds a port of its own. `ListenerTCP` and `ListenerHTTP1` set `SO_REUSEADDR`, so a port in `TIME_WAIT` from the previous run is fine; one held by a stray server from a crashed run is `Could not bind port`. Simpler is no fixed port at all: `ListenerTCP(address, 0, ...)` and `ListenerQUIC(0, ...)` take whatever port is free and report it with `Port()`.
- `QUICServerCredentials{ .selfSigned = true }` against `QUICClientCredentials{ .insecureNoServerValidation = true }` is the local pair; `TLSServerCredentials{ .selfSigned = true }` against `TLSClientCredentials{ .caPem = GetSelfSignedCertificatePem().certificate }` exercises real verification. Neither needs anything installed except, for QUIC, the `openssl` CLI (the Flatpak has it).
- `QUICServerCredentials{ .selfSigned = true }` against `QUICClientCredentials{ .insecureNoServerValidation = true }` is the local pair; `TLSServerCredentials{ .selfSigned = true }` against `TLSClientCredentials{ .caPem = GetSelfSignedCertificatePem().certificate }` exercises real verification. Neither needs anything installed.
- Give a blocked reader (`RecieveSync` in a loop) a `std::thread`, not a pool task, and a test that could hang a watchdog thread that calls `std::_Exit(1)` sooner than the runner's 60 s timeout, as `tests/ShouldNotDropEarlyStreams` does at 8 s (the churn test's 90 s watchdog never fires first).

View file

@ -29,7 +29,7 @@ Partitions and what they own:
- `:HTTP1`: the RFC 9112 codec, inline: `SerializeRequest`, `SerializeResponse`, `SerializeContinue`, `MessageParser`, `MessageLimits`, `HTTP1ProtocolError`, and the framing it refuses to guess at.
- `:ClientHTTP1`, `:ListenerHTTP1`: HTTP/1.1 over a `ByteStream&`. The listener is a `ListenerTCP` with a thread per accepted connection; the client keeps one pooled connection with a replay-once rule, and `ClientHTTP1::Fetch` is the URL fetch that follows redirects by RFC 9110 §15.4 with a client per hop (`FetchOptions`, `FetchResult`, `FetchStop`).
- `:ClientQUIC`: `ClientQUIC` and `QUICStream` for both ends of a connection, `QUICSettings`, both QUIC credential structs, `QUICException`, `QUICClosedException`. The native unit holds the process-wide `MsQuicRuntime` (API table and registration, closed by a static destructor at exit), the stream registry with its refcounted handles, and the drain in `~ClientQUIC`.
- `:ListenerQUIC`: the msquic listener, its four `Listen*` variants, the self-signed certificate (made by the `openssl` CLI and cached under `/tmp/crafter-network-quic-cert/`), `ComputeCertificateHashSHA256`, `GetSelfSignedCertificatePath`.
- `:ListenerQUIC`: the msquic listener, its four `Listen*` variants, the self-signed certificate's on-disk cache (per user under `$XDG_CACHE_HOME/crafter-network/quic-cert/`, behind a lock; the OpenSSL work is module-internal functions of `:TLS`), `ComputeCertificateHashSHA256`, `GetSelfSignedCertificatePath`.
- `:HTTP3`: QUIC varints, HTTP/3 frames, the QPACK static table, Huffman decoding, the control-stream preludes and the WebTransport stream prefixes. Native only: it throws for protocol errors.
- `:ClientHTTP`, `:ListenerHTTP`: HTTP/3 on `:ClientQUIC` and `:ListenerQUIC`. The listener demuxes every bidirectional stream into an HTTP/3 request or a WebTransport data stream and accepts extended CONNECT when `wtRoutes` is non-empty.
- `:WebTransport`: `WebTransportSession`, the per-session handle a WT handler gets. Bidirectional streams and session close today; datagrams and capsules are stubs.
@ -40,7 +40,7 @@ The variants are decided in `project.cpp`. Any `wasm32-*` target defines `CRAFTE
crafter-build is the only tool, from its Flatpak (`alias crafter-build='flatpak run net.catcrafts.CrafterBuild'`; this machine has its `stable` and `beta` branches, and a build installed from a Crafter.Build checkout is branch `local`, chosen with `--branch=`). The host is Linux x86_64. A native build needs network access to `forgejo.catcrafts.net` for Crafter.Thread, which is unpinned and `git pull`ed by every verb that loads `project.cpp`, `test --list`, `lint` and `format` included (`--local` takes `../Crafter.Thread` instead, and is forwarded so Crafter.Thread resolves the same way), and to `github.com` once for msquic, pinned to v2.6.3 (commit `16ca4d8d`) with its submodules and built by CMake into `~/.var/app/net.catcrafts.CrafterBuild/cache/crafter.build/external/msquic-<hash>/` (a native crafter-build uses `~/.cache/crafter.build/`): several minutes the first time, then shared by every project with the same spec. msquic is configured with `QUIC_TLS_LIB=openssl` against the OpenSSL the sandbox provides, 3.5 or newer (the Flatpak runtime has 3.5.9), as a shared `libmsquic.so` that the test binaries and consumers find through an absolute runpath into that cache (a directory named after msquic's spec hash, which `crafter-build clean` never touches). msquic links `libnuma` when the build host has it: a native crafter-build on Arch does, the Flatpak does not.
What the suite needs beyond that: the `openssl` CLI (the QUIC self-signed certificate and its hash; the TLS partition makes its own certificate in-process), `curl` and `python3` for the interop tests, and outbound UDP/443 to `cloudflare-quic.com` for `ShouldSend`. The Flatpak sandbox has `openssl`, `curl`, `python3` and `wasmtime` and no `node`; its `/tmp` is a per-app directory under `$XDG_RUNTIME_DIR/.flatpak/net.catcrafts.CrafterBuild/tmp`, shared by instances running at the same time and discarded once none is, so the QUIC certificate cache under `/tmp` rarely survives between runs; and it sees the host filesystem but not the host's `PATH` (`/run/host/usr/bin` is the host's `/usr/bin`). The interop tests print `skipping the ... half` and pass when a tool is missing: a pass that tested less, so read the log.
What the suite needs beyond that: `curl` and `python3` for the interop tests, the `openssl` CLI for the mutual-TLS half of `ShouldRequireClientCertificateHTTPS1` (both development certificates are made in-process), and outbound UDP/443 to `cloudflare-quic.com` for `ShouldSend`. The Flatpak sandbox has `openssl`, `curl`, `python3` and `wasmtime` and no `node`; its `/tmp` is a per-app directory under `$XDG_RUNTIME_DIR/.flatpak/net.catcrafts.CrafterBuild/tmp`, shared by instances running at the same time and discarded once none is; its `XDG_CACHE_HOME` is `~/.var/app/net.catcrafts.CrafterBuild/cache`, so the QUIC tests' certificate lives in `crafter-network/quic-cert/` there and survives between runs (delete the directory to start cold; `ShouldShareSelfSignedCertificateQUIC` points `XDG_CACHE_HOME` at a scratch directory of its own); and it sees the host filesystem but not the host's `PATH` (`/run/host/usr/bin` is the host's `/usr/bin`). The interop tests print `skipping the ... half` and pass when a tool is missing: a pass that tested less, so read the log.
Build outputs go to `bin/crafter-network-<target>-<march>-<mtune>-<hash>/` (the PCMs, `std.pcm` and `libcrafter-network.a`; the wasm variant also carries `network-env.js`) and `build/<the same>/`. The hash covers the variant, so the default build, `--no-quic` and the browser build keep separate directories, and `bin/` also holds one directory per test. The Flatpak's host triple is `x86_64-unknown-linux-gnu`; the README's `x86_64-pc-linux-gnu` default is a native crafter-build's, and `--target=` with either is the same build in another directory. `crafter-build clean` removes them all.
@ -51,7 +51,7 @@ crafter-build # native: TCP, TLS, HTTP/1.1, QUI
crafter-build --no-quic # native without QUIC, HTTP/3, WebTransport and msquic
crafter-build --local # Crafter.Thread from ../Crafter.Thread
crafter-build --target=wasm32-wasip1 # the browser library
crafter-build test # the suite, native: 32 tests
crafter-build test # the suite, native: 33 tests
crafter-build test 'Should*HTTP1' # a name glob
crafter-build test --no-quic # the TCP, URL, TLS and HTTP/1.1 tests plus ShouldBuildWithoutQUIC: 19
crafter-build test --list
@ -119,8 +119,8 @@ Decided deliberately; change them only with the maintainer's say-so.
## Gotchas
- **`ThreadPool::Enqueue` before `ThreadPool::Start()` is a null dereference**, and most of the library enqueues: every `*Async`, every QUIC `OnStream` and `OnDatagram` delivery, `ClientHTTP` and `ListenerHTTP` entirely. The crash has no message; in a test it shows as 💥.
- **Two self-signed certificates.** `QUICServerCredentials::selfSigned` is made by the `openssl` CLI and cached at `/tmp/crafter-network-quic-cert/`, reused across restarts while valid so a browser's pinned hash survives a restart, except inside the Flatpak, whose `/tmp` goes away with its last instance (see "Dev environment"); `TLSServerCredentials::selfSigned` is made in-process by `:TLS` and is new every start. `GetSelfSignedCertificatePath()` is the first, `GetSelfSignedCertificatePem()` the second.
- **`ComputeCertificateHashSHA256` and the QUIC certificate shell out to `openssl`** (`std::system`, `popen`) and throw `QUICException` without the CLI. `:TLS` could do both in-process; nobody has moved them yet.
- **Two self-signed certificates, both made in-process by `:TLS`.** `QUICServerCredentials::selfSigned` is kept per user at `$XDG_CACHE_HOME/crafter-network/quic-cert/` (else `~/.cache/...`, else `$XDG_RUNTIME_DIR/...`), reused across restarts until a day before it expires so a browser's pinned hash survives a restart, and shared by processes that start together through a record lock on `lock` there; the listener hands msquic the pair as an in-memory PKCS#12, not the paths. `TLSServerCredentials::selfSigned` is new every start and carries `keyEncipherment` the QUIC one lacks. `GetSelfSignedCertificatePath()` is the first, `GetSelfSignedCertificatePem()` the second. To start the QUIC one cold, delete that directory.
- **`:TLS` declares module-internal functions** (`MakeWebTransportCertificatePem`, `CertificatePairExpiry`, `EncodePkcs12`, `CertificateSha256`) without `export`: `:ListenerQUIC` imports `:TLS` for them so libssl stays behind one partition, and a consumer never sees them.
- **msquic's registration is closed by a static destructor that waits for every handle.** A test that returns from `main` with an accepted peer alive waits two minutes, the idle timeout; the tests `_Exit`. The exception, `ShouldSurviveConnectionChurn`, exists because a leaked stream handle once hung that close forever (`fix(quic)` in `git log`, with the `SHUTDOWN_ON_FAIL` story).
- **Callbacks the interfaces say run "on the msquic worker" run as pool tasks**: `ClientQUIC::OnStream` and `OnDatagram` both enqueue. What arrives before a handler is set is queued: streams are delivered on `OnStream`; datagrams go to `RecieveDatagramSync`'s queue and never to a later `OnDatagram`.
- **`ListenerQUIC::ListenAsync*` run the accept loop on a `std::thread`**, not the pool as their comments say, and `ListenerHTTP::Listen` is `ListenSyncAsync` underneath, so a `ListenerHTTP`'s per-connection setup is a pool task. A saturated pool delays a new connection's SETTINGS; the early streams are buffered meanwhile (`ShouldNotDropEarlyStreams`).
@ -128,7 +128,7 @@ Decided deliberately; change them only with the maintainer's say-so.
- **`QUICSettings::peerUnidiStreamCount` is the first thing to raise** when a peer stalls opening streams; it was a hardcoded 16 until `ShouldHonourQUICSettings`.
- **The comments about `-fno-exceptions` in `project.cpp`, the umbrella and the browser unit are history.** crafter-build 4.0.0 compiles wasm with exceptions on and refuses `-fno-exceptions`; `:HTTP3` stays out of the browser build and the browser units still report through callbacks because the JS side has no exception channel, not because `throw` would not compile.
- **The header comment of `Crafter.Network-HTTP3.cppm` is stale**: the partition is exported from the umbrella natively and has a Huffman decoder. The README is right.
- **More comments the code outgrew**: `-ListenerQUIC.cpp` says the certificate pair lives in a `mkdtemp`'d directory for the life of the process (it is the fixed `/tmp/crafter-network-quic-cert/`, reused) and is valid 13 days (`-days 10`); `ClientQUIC.cppm` calls `selfSigned` an in-memory certificate (it is on disk, made by the CLI); `ListenerHTTP.cppm` says `Listen()` dispatches on the calling thread (it is `ListenSyncAsync`); the link-flag comment in `project.cpp` still has msquic bundling quictls (it links the system `libssl`). Fix a comment when you touch its code.
- **More comments the code outgrew**: `ListenerHTTP.cppm` says `Listen()` dispatches on the calling thread (it is `ListenSyncAsync`); the link-flag comment in `project.cpp` still has msquic bundling quictls (it links the system `libssl`). Fix a comment when you touch its code.
- **`WebTransportSession::OpenStream(true)` does not throw** as its comment says; it writes the unidirectional prefix. Peer-initiated unidirectional WebTransport streams are drained by the listener and never reach a session.
- **`ListenerTCP::concurrentClientLimit` is stored and never read**; only `totalClientLimit` ends the loop.
- **`ListenerTCP(port, ...)` listens on every IPv4 address**; given an address it binds that one, IPv4 or IPv6, and port 0 asks the kernel for one. `ClientTCP` resolves for both families and tries every address; QUIC connects on either family (`QUIC_ADDRESS_FAMILY_UNSPEC`), and `ListenerQUIC(port, ...)` listens on both. `ListenerQUIC(address, port, ...)` listens on that address only, but msquic binds the dual-stack wildcard UDP socket for the port anyway and filters in `QuicBindingGetListener`: a connection to another local address is closed with `no_application_protocol` (the client sees `0x5c` in milliseconds), not ignored, and no other UDP socket can take that port on another address.

View file

@ -299,7 +299,7 @@ Browser-initiated bidi/unidi WebTransport streams arrive via `session.OnStream(.
### Self-signed certs & browser peers
Passing `QUICServerCredentials{selfSigned = true}` makes the listener generate (and cache) an ephemeral cert at `/tmp/crafter-network-quic-cert/{cert,key}.pem` and reuse it across server restarts while it's still valid. The cert is shaped to satisfy Chromium's `WebTransport.serverCertificateHashes` rules: ECDSA P-256, signed with ECDSA-SHA256, validity ≤14 days (10 in practice), `BasicConstraints CA:FALSE`, `KeyUsage digitalSignature`, `EKU serverAuth`, `SAN=DNS:localhost,IP:127.0.0.1,IP:::1`. To let a browser peer pin this cert without trusting a CA:
Passing `QUICServerCredentials{selfSigned = true}` makes the listener serve a development certificate it makes in-process (no `openssl` CLI) and keeps per user at `$XDG_CACHE_HOME/crafter-network/quic-cert/{cert,key}.pem` (`~/.cache/...` without `XDG_CACHE_HOME`, `$XDG_RUNTIME_DIR/...` without a home), in a directory only that user can enter. It is reused across server restarts until it has less than a day left, then replaced by the next process that needs it. Processes starting at the same moment share one pair: a lock covers the check and the replacement, each file is renamed into place whole, and the listener loads the pair from memory, so none ends up with one run's key and another's certificate. The cert is shaped to satisfy Chromium's `WebTransport.serverCertificateHashes` rules: ECDSA P-256, signed with ECDSA-SHA256, validity ≤14 days (10 in practice), `BasicConstraints CA:FALSE`, `KeyUsage digitalSignature`, `EKU serverAuth`, `SAN=DNS:localhost,IP:127.0.0.1,IP:::1`. To let a browser peer pin this cert without trusting a CA:
```cpp
auto certPath = Crafter::GetSelfSignedCertificatePath();
@ -341,6 +341,7 @@ The library includes tests covering:
- QUIC unreliable datagrams (`ShouldSendRecieveQUICDatagram`)
- QUIC listener ports (`ShouldReportListenerPortQUIC`) — a listener on port 0 reports the port it got and is reachable there, a second one gets another, and a fixed port reads back as itself
- QUIC loopback listeners (`ShouldBindLoopbackQUIC`) — a listener on `127.0.0.1` serves loopback clients and refuses, at once, clients through the host's LAN address or `::1`; `::1` works too, and a name is refused
- The QUIC development certificate (`ShouldShareSelfSignedCertificateQUIC`) — six processes start `selfSigned` listeners together from an empty cache, with no `openssl` on `PATH`, and all serve one certificate; its directory is private, a later process reuses it, and a mismatched pair on disk is replaced
- QUIC connection teardown (`ShouldSurviveConnectionChurn`) — 25 build-up/tear-down cycles with reader threads parked inside `RecieveSync`, the shape a reconnect loop has; the rest of the QUIC tests end in `std::_Exit(0)` and so never run a `ClientQUIC` destructor
- WebTransport echo (`ShouldEchoWebTransport`) — extended-CONNECT acceptance, draft-02 SETTINGS, bidi data stream framing (`WT_STREAM 0x41` + session-id varint), and byte-for-byte echo
- HTTP/1.1 wire format (`ShouldParseHTTP1`) — serialisation, incremental parsing fed one byte at a time, chunked bodies with trailers, pipelining, interim 1xx, HTTP/1.0 and HEAD framing, and the malformed inputs the parser is required to reject
@ -367,8 +368,7 @@ The external-interop test requires outbound UDP/443; if your network blocks it t
- **Crafter.Thread**: Thread pool management for asynchronous operations.
- **msquic** (native target only) — fetched and built automatically as a Crafter `ExternalDependency` (no system install required). The build clones `microsoft/msquic` at a pinned release (v2.6.3), with the submodules that release records, into the per-project external cache, configures it via CMake (`QUIC_TLS_LIB=openssl` against the system OpenSSL, tests/tools/perf disabled), and links the produced `libmsquic` into the QUIC and HTTP/3 modules. Skipped entirely on browser builds.
- On Linux msquic links against `libnuma` (provided by the `numactl` package on most distros).
- The self-signed-cert path used by tests/dev shells out to the `openssl` CLI; install `openssl` if you intend to use `QUICServerCredentials{selfSigned = true}`. The same path also produces the cert hash that browser peers need for `serverCertificateHashes`.
- **libssl / libcrypto** (OpenSSL 3, native target only) — a *system* package, unlike msquic, linked via `-lssl -lcrypto`. Backs `Crafter.Network:TLS`, i.e. `https://` on the HTTP/1.1 client and listener. It is not built here on purpose, and msquic uses it too, so the process has one OpenSSL: msquic's QUIC support needs OpenSSL 3.5 or newer (Arch has it, as does the Flatpak runtime crafter-build builds in). msquic's own bundled fork (quictls) is not used: it is an older OpenSSL that still reads the system's `openssl.cnf`, and a config written for the system's newer OpenSSL (the Flatpak runtime's loads a PKCS#11 engine, with `config_diagnostics` on) made its initialisation fail. Install your distro's OpenSSL development package (`openssl` on Arch, `libssl-dev` on Debian/Ubuntu). Where that OpenSSL is older than 3.5 (Debian 12 and Ubuntu 24.04 ship 3.0), msquic doesn't build against it: build with `--no-quic`, which keeps TCP, TLS and HTTP/1.1. Certificate generation for `TLSServerCredentials{selfSigned = true}` happens in-process through this library — no `openssl` CLI needed.
- **libssl / libcrypto** (OpenSSL 3, native target only) — a *system* package, unlike msquic, linked via `-lssl -lcrypto`. Backs `Crafter.Network:TLS`, i.e. `https://` on the HTTP/1.1 client and listener. It is not built here on purpose, and msquic uses it too, so the process has one OpenSSL: msquic's QUIC support needs OpenSSL 3.5 or newer (Arch has it, as does the Flatpak runtime crafter-build builds in). msquic's own bundled fork (quictls) is not used: it is an older OpenSSL that still reads the system's `openssl.cnf`, and a config written for the system's newer OpenSSL (the Flatpak runtime's loads a PKCS#11 engine, with `config_diagnostics` on) made its initialisation fail. Install your distro's OpenSSL development package (`openssl` on Arch, `libssl-dev` on Debian/Ubuntu). Where that OpenSSL is older than 3.5 (Debian 12 and Ubuntu 24.04 ship 3.0), msquic doesn't build against it: build with `--no-quic`, which keeps TCP, TLS and HTTP/1.1. Certificate generation for `TLSServerCredentials{selfSigned = true}` and `QUICServerCredentials{selfSigned = true}`, and the hash `ComputeCertificateHashSHA256` gives browser peers, happen in-process through this library — no `openssl` CLI needed.
- Plaintext HTTP/1.1 needs nothing beyond POSIX sockets: `ClientHTTP1`/`ListenerHTTP1` built without credentials pull in no msquic and touch no TLS code path.
- **Browser build** has no extra dependencies beyond Crafter.Build's `wasi-browser` runtime: HTTP delegates to the browser's `fetch()`, QUIC to its `WebTransport`. The JS glue lives in `additional/network-env.js` and is shipped alongside the produced `.wasm`.