How do I serve HTTPS with simoom?
Turn on the
tlsfeature, load your certificate into aServerConfig, and calllisten_tlsandtls; simoom does TLS 1.3 itself, with no OpenSSL.
simoom has its own TLS 1.3 implementation, with its own cryptography, in the simoom-tls crate.
There is nothing to install and nothing to link. This page covers certificates, several names on
one server, rotation without restart, and client certificates.
Turn it on
simoom = { version = "0.1", features = ["tls"] } # or "full" for TLS and HTTP/3tls supports X25519 and P-256 key exchange, ECDSA P-256 and RSA certificates, and AES-GCM.
tls-extra adds P-384 and Ed25519 certificates and ChaCha20-Poly1305.
A self-signed certificate for local testing
simoom::tls::certgen makes certificates for tests:
use simoom::tls::{certgen, ServerConfig, CertifiedKey};
use simoom::{Reply, Request, Server};
fn main() {
let cert = certgen::self_signed("localhost");
let key = CertifiedKey::from_pem(cert.cert_pem.as_bytes(), cert.key_pem.as_bytes()).expect("certificate and key");
let server = Server::builder()
.listen_tls("127.0.0.1:8443")
.tls(ServerConfig::new(key))
.service(|_: &mut Request<'_>, r: Reply<'_>| r.body_static(b"Hello over TLS"))
.start()
.unwrap();
server.join();
}curl -k https://localhost:8443/ connects (-k because the certificate is self-signed). The
localhost certificate also covers 127.0.0.1 and ::1.
Your own certificate
Load a PEM chain and key, for example from Let's Encrypt. Keys in PKCS#8, SEC1 or PKCS#1 form work. Plain and TLS listeners can live on one server:
use simoom::tls::ServerConfig;
use simoom::{Reply, Request, Server};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let cert = std::fs::read("/etc/simoom/fullchain.pem")?;
let key = std::fs::read("/etc/simoom/privkey.pem")?;
let tls = ServerConfig::from_pem(&cert, &key).map_err(|e| format!("{e:?}"))?;
let server = Server::builder()
.listen("0.0.0.0:80") // plain HTTP on one port
.listen_tls("0.0.0.0:443") // TLS on another
.tls(tls)
.service(|req: &mut Request<'_>, r: Reply<'_>| r.body(format!("secure: {}", req.is_secure())))
.start()?;
server.join();
Ok(())
}On a TLS listener, client and server agree on HTTP/2 or HTTP/1.1 during the handshake (ALPN).
Builder::tls sets the ALPN list for you. For HTTP/3, see HTTP/2 and HTTP/3.
Several names on one server
add_sni_cert adds a certificate that is chosen when the client asks for one of its names (SNI),
wildcards included. Clients asking for other names get the default certificate:
use simoom::tls::{CertifiedKey, ServerConfig};
fn config(a: CertifiedKey, b: CertifiedKey) -> ServerConfig {
let mut tls = ServerConfig::new(a); // the default certificate
tls.add_sni_cert(b); // chosen when the client asks for one of its names
tls
}Set tls.strict_sni = true to refuse clients whose name matches no certificate.
Rotating certificates without a restart
ServerHandle::set_tls swaps the configuration. New connections use it; open ones keep theirs:
use simoom::tls::ServerConfig;
use simoom::ServerHandle;
fn reload(server: &ServerHandle) -> Result<(), Box<dyn std::error::Error>> {
let cert = std::fs::read("/etc/simoom/fullchain.pem")?;
let key = std::fs::read("/etc/simoom/privkey.pem")?;
let tls = ServerConfig::from_pem(&cert, &key).map_err(|e| format!("{e:?}"))?;
server.set_tls(tls)?; // new connections use it; open ones keep theirs
Ok(())
}set_quic_tls does the same for HTTP/3. Session tickets issued before the swap resume only if
the new configuration shares the old one's ticketer.
Client certificates (mutual TLS)
Require or request client certificates with ServerConfig::client_auth. The ClientAuth and
RootStore types live in simoom-tls, which simoom doesn't re-export in 0.1.0, so add it
next to simoom (cargo add simoom-tls, same version):
use simoom::tls::ServerConfig;
use simoom::{Outcome, Reply, Request};
use simoom_tls::x509::RootStore;
use simoom_tls::ClientAuth;
fn config(mut tls: ServerConfig) -> Result<ServerConfig, String> {
let mut roots = RootStore::empty();
roots.add_pem_file("/etc/simoom/client-ca.pem")?;
tls.client_auth = ClientAuth::Required(roots);
Ok(tls)
}
fn whoami(req: &mut Request<'_>, reply: Reply<'_>) -> Outcome {
match req.tls() {
Some(info) if !info.peer_certificates().is_empty() => {
reply.body(format!("client certificate of {} bytes, {}", info.peer_certificates()[0].len(), info.cipher_suite_name()))
}
_ => reply.status(401).empty(),
}
}ClientAuth::Optional(roots) asks for a certificate but accepts clients without one; the ones
that send one are still verified. req.tls() also gives the protocol version, cipher suite, the
SNI name and the negotiated ALPN protocol.
What simoom's TLS does and doesn't do
- TLS 1.3 only on simoom servers. TLS 1.2 exists in
simoom-tlsbehind itstls12feature, but simoom's features don't turn it on. Clients that only speak TLS 1.2 fail the handshake. - 0-RTT is never accepted, by design: early data can be replayed.
- Session resumption with tickets is on by default, with rotating keys.
- Constant-time crypto: hardware AES-GCM where the CPU has it, a bitsliced fallback elsewhere, constant-time comparison of secrets.
- The handshake deadline is
Limits::head_timeout(10 s by default). - Not independently audited yet. The TLS stack passed testssl.sh and interop with OpenSSL, curl, Python, GnuTLS and Schannel (SECURITY.md), but no outside firm has audited it. If your rules require an audited stack, terminate TLS in front of simoom.
Common questions
Do I need OpenSSL, a C compiler or cmake?
No. simoom's TLS and cryptography are Rust code in the repository, so cross-compiling for another target needs only the Rust target.
Can I use rustls or OpenSSL instead?
Not inside simoom's listeners. You can put a TLS terminator (nginx, haproxy, a cloud load balancer) in front of a plain simoom listener; HTTP/2 with prior knowledge (h2c) is a good protocol between them.
How do I redirect HTTP to HTTPS?
Listen on both ports and answer plain requests with a redirect:
if !req.is_secure() { return r.status(308).header("location", url).empty(); }.
Does TLS make start-up slower?
A little. In benchmark campaign 4 (2026-11-12), the full server, with TLS 1.3 and HTTP/3, gave its first answer 1.49 ms after start, against 0.94 ms for the default build without them; hyper, without TLS, took 1.48 ms on the same droplet. Part of the difference is HTTP/3, which starts every event loop at once (benchmarks).