TLS

hopf-core::tls: an in-tree TLS 1.2 (RFC 5246) and TLS 1.3 (RFC 8446) implementation — handshake state machines and record layers built directly on AWS-LC (via aws-lc-rs), with no rustls anywhere in the path. The module implements its own TlsAcceptor / TlsConnector factory traits and produces a TlsVariant session that TcpConnection drives through the sink-based TlsRecordSink interface. Protocol handlers always see plaintext — encryption is under the Endpoint. Supports TLS-from-accept and STARTTLS, one-port 1.2/1.3 version negotiation, hybrid post-quantum key exchange, ALPN, mutual TLS, session tickets, Encrypted Client Hello, and certificate compression.

QUIC/TLS 1.3 for HTTP/3 lives in hopf-quic and is PQC-first (TLS 1.3 only, hybrid X25519MLKEM768 offered first) — it also runs on the same in-tree AWS-LC foundation, with no separate TLS record layer (QUIC packet protection replaces it). This page covers TCP TLS (1.2 + 1.3, same crypto provider and key-exchange policy).

Standards

Topic Detail
TLS 1.2 (RFC 5246) + 1.3 (RFC 8446), in-tree handshake + record engines on AWS-LC (aws-lc-rs); AEAD-only cipher suites (AES-GCM, ChaCha20-Poly1305) — no CBC, permanent exclusion
Version selection TcpTlsVersionPolicy: Negotiate (default — buffers the first flight, prefers 1.3, falls back to 1.2), Tls13Only, Tls12Only
Key exchange Hybrid PQC first (RFC 10024): X25519MLKEM768 preferred, SecP256r1MLKEM768 / SecP384r1MLKEM1024 as alternatives, classical X25519/P-256/P-384 fallback
ALPN RFC 7301, e.g. h2, http/1.1 — TLS 1.2 and TLS 1.3 alike
Session resumption RFC 5077 / RFC 8446 §2.2 tickets on both versions, PSK-DHE only on TLS 1.3 (forward secrecy on resumption); TicketKeys keyring with in-place rotation
Encrypted Client Hello RFC 9849, TLS 1.3 only, shared mode (client + server)
Certificate compression RFC 8879, Brotli, TLS 1.3 only
PEM PKCS#8 private keys only (BEGIN PRIVATE KEY); RSA, ECDSA P-256/P-384, or Ed25519 certificates. Re-encode legacy PKCS#1/SEC1 keys with openssl pkcs8 -topk8 -nocrypt
Patterns TLS-from-accept; STARTTLS (Endpoint::start_tls / start_client_tls)

Full requirement-by-requirement status, including gaps: Conformance → TLS.

Architecture

TcpListenerConfig / TcpConnectorConfig
        │  SharedTlsAcceptor / SharedTlsConnector   (hopf-core::tls::pem)
        ▼
TlsVariant::V13(TlsRecordEngine)        — HandshakeEngine + TLS 1.3 record layer
TlsVariant::V12(Tls12RecordEngine)      — tls12::Tls12Engine + TLS 1.2 record layer
TlsVariant::Negotiating(..)             — buffers the first flight, picks 1.3 or 1.2, then delegates
        │  driven via TlsRecordSink (TcpConnection is the sink)
        ▼
Endpoint  →  ProtocolHandler sees plaintext
             security_info() after handshake

There is no separate adapter crate in this path: PemAcceptor / SniAcceptor / TrustedConnector and their TLS-1.2 counterparts (private types in hopf-core/src/tls/pem.rs) implement the crate's own TlsAcceptor / TlsConnector traits directly; the PEM helper functions just return them as Arc<dyn TlsAcceptor> / Arc<dyn TlsConnector> (SharedTlsAcceptor / SharedTlsConnector).

TlsVariant is a thin enum, not a trait object: V13 wraps TlsRecordEngine (tls/record.rs), which frames HandshakeEngine's (tls/engine.rs) handshake messages into TLS 1.3 records and does the AEAD sealing/opening; V12 wraps tls12::Tls12RecordEngine (tls/tls12/record.rs) around tls12::Tls12Engine (tls/tls12/engine.rs) the same way for TLS 1.2/RFC 5246. Both record engines expose the same method set and consume the same TlsRecordSink, so TcpConnection pumps whichever one it got without caring which version is underneath.

The default PEM builders (acceptor_from_pem / connector_from_pem) produce TlsVariant::Negotiating: the server buffers the first flight until a complete ClientHello is seen, inspects its supported_versions (falling back to the legacy version field) to pick 1.3 or 1.2 (preferring 1.3), materializes the matching engine, and replays the buffered bytes into it — the same approach on the client side, keyed off the first ServerHello. One TCP connection speaks exactly one version for its life; there is no mid-connection renegotiation. Pin a fixed version with *_with_tcp_version_policy or the *_tls12 builders instead of negotiating. Credentials generated at run time (an ephemeral loopback CA, a test fixture) need no files: server_credentials_from_pem_bytes parses PEM from memory and acceptor_from_credentials / acceptor_from_credentials_with_client_auth build the acceptor from the resulting ServerCredentials, the latter verifying client certificates against DER CA certificates handed in directly.

hopf-core::dtls and hopf-core::dtls12 reuse this same HandshakeEngine / tls12::Tls12Engine machinery over UDP with their own DTLS record/reassembly layer and a parallel DtlsVersionPolicy — see Architecture → Security substrate and Limitations below; they are not reachable through this page's TlsAcceptor/TlsConnector/TlsVariant types.

Public API

use std::path::Path;
use hopf_core::{
    acceptor_from_pem, connector_from_pem, server_credentials_from_pem,
    acceptor_from_pem_with_client_auth, connector_from_pem_with_client_cert,
    ClientAuthPolicy,
};

// Server
let acceptor = acceptor_from_pem(
    Path::new("cert.pem"),
    Path::new("key.pem"),
    &[b"h2", b"http/1.1"],
)?;

// Client
let connector = connector_from_pem(Path::new("ca.pem"), &[b"h2", b"http/1.1"])?;

// Mutual TLS: server requires a client certificate, verified against client_roots.pem.
let acceptor = acceptor_from_pem_with_client_auth(
    Path::new("cert.pem"), Path::new("key.pem"),
    &[b"h2", b"http/1.1"], ClientAuthPolicy::Require,
    Path::new("client_roots.pem"),
)?;
// ClientAuthPolicy::Request is opportunistic mTLS — check
// SecurityInfo::peer_certificate_fingerprint() to see whether a client cert
// was actually presented on a given connection.

// Client side: present an identity certificate for mTLS.
let connector = connector_from_pem_with_client_cert(
    Path::new("ca.pem"), Path::new("client-cert.pem"), Path::new("client-key.pem"), &[],
)?;

Virtual hosting (SNI)

use hopf_core::{acceptor_from_pem_with_sni, server_credentials_from_pem};

// One listener, one cert per hostname, dispatched by client SNI.
// `default_cert_path`/`default_key_path` back an unmatched or absent SNI.
let b_creds = server_credentials_from_pem(Path::new("b-cert.pem"), Path::new("b-key.pem"))?;
let acceptor = acceptor_from_pem_with_sni(
    Path::new("a-cert.pem"), Path::new("a-key.pem"),
    [("b.example.com".to_string(), b_creds)],
    &[b"h2", b"http/1.1"],
)?;
// A hostname with no matching entry (or no SNI at all) falls back to the
// default credentials rather than failing the handshake.
Function Returns Notes
server_credentials_from_pem io::Result<ServerCredentials> Cert chain + PKCS#8 key, shared by every acceptor builder below
acceptor_from_pem SharedTlsAcceptor Default TcpTlsVersionPolicy::Negotiate (prefers 1.3)
acceptor_from_pem_with_tcp_version_policy SharedTlsAcceptor Pin Tls13Only / Tls12Only instead of negotiating
acceptor_from_pem_with_sni SharedTlsAcceptor Default + hostname-keyed credential map; virtual hosting
acceptor_from_pem_with_client_auth SharedTlsAcceptor mTLS; ClientAuthPolicy::Request/Require toggles opportunistic vs mandatory
acceptor_from_pem_tls12 / _with_client_auth SharedTlsAcceptor TLS 1.2 only, explicit legacy interop; RSA/ECDSA P-256/P-384/Ed25519
connector_from_pem SharedTlsConnector Trusts the given PEM CA/leaf; default Negotiate
connector_from_pem_with_tcp_version_policy SharedTlsConnector Pin Tls13Only / Tls12Only
connector_from_pem_with_client_cert SharedTlsConnector mTLS: presents a client identity cert when asked
connector_from_pem_tls12 / _with_client_cert SharedTlsConnector TLS 1.2 only
connector_with_verify_override SharedTlsConnector Fully custom server-chain verification (e.g. DANE TLSA) instead of a fixed root set
public_trust_connector SharedTlsConnector Public WebPKI trust — native OS store, vendored webpki-roots fallback
insecure_connector / insecure_connector_tls12 SharedTlsConnector Dangerous: accepts any certificate, no validation — opportunistic STARTTLS only
acceptor_with_alpn / connector_with_alpn Wraps a SharedTlsAcceptor/SharedTlsConnector Adds/overrides ALPN on any acceptor or connector — the only way to give the *_tls12 builders a protocol list
acceptor_with_record_size_limit / connector_with_record_size_limit Wraps a SharedTlsAcceptor/SharedTlsConnector RFC 8449 record_size_limit; TLS 1.3 only
acceptor_with_ech / connector_with_ech Wraps a SharedTlsAcceptor/SharedTlsConnector RFC 9849 Encrypted Client Hello; TLS 1.3 only
acceptor_requiring_supported_versions Wraps a SharedTlsAcceptor Refuse a TLS 1.2 ClientHello missing supported_versions (RFC 9846 §1.4); TLS 1.3 unaffected

Sessions support send_close_notify via TlsVariant's inherent methods, driven the same way on both TLS versions.

Configuration

The PEM builders take no numeric knobs beyond what's listed below; everything else is a fixed, spec-conformant default. Key exchange always prefers the hybrid post-quantum group X25519MLKEM768 when the peer supports it, falling back through SecP256r1MLKEM768 / SecP384r1MLKEM1024 to classical curves — this isn't a caller-adjustable setting via the PEM helpers. QUIC uses the same crypto provider but pins TLS 1.3, so every QUIC handshake is PQC-first with no 1.2 fallback.

Input Type Notes
Certificate PEM path &Path Server leaf (+ chain as presented); RSA, ECDSA P-256/P-384, or Ed25519
Private key PEM path &Path PKCS#8 only (BEGIN PRIVATE KEY)
CA PEM path &Path Client trust anchors
ALPN &[&[u8]] e.g. b"h2", b"http/1.1"
Version policy TcpTlsVersionPolicy Negotiate (default) / Tls13Only / Tls12Only, via the *_with_tcp_version_policy builders
Client-auth policy ClientAuthPolicy None (default) / Request / Require
Session tickets TicketKeys Not wired into the simple PEM builders — build a HandshakeConfig/Tls12Config directly (see tls::HandshakeConfig::ticket_key / tls::Tls12Config::ticket_key) to enable resumption; TicketKeys::rotate rotates the sealing key without breaking tickets minted just before rotation
Encrypted Client Hello EchClientConfig / EchServerConfig Via acceptor_with_ech / connector_with_ech, or HandshakeConfig::ech_client/ech_server directly; TLS 1.3 only
Record size limit u16 (64..=16385) Via acceptor_with_record_size_limit / connector_with_record_size_limit; RFC 8449, TLS 1.3 only

Cargo features

None. hopf-core::tls is always built in — it's not gated behind a feature flag.

Wiring listeners and connectors

use hopf_core::TcpListenerConfig;

// TLS-from-accept (HTTPS, implicit FTPS/SMTPS, …)
let cfg = TcpListenerConfig::new(addr, factory)
    .with_tls(acceptor);

// Cleartext first, then STARTTLS (SMTP, explicit FTPS, …)
let cfg = TcpListenerConfig::new(addr, factory)
    .with_starttls_acceptor(acceptor);

Client dial:

use hopf_core::TcpConnectorConfig;

let cfg = TcpConnectorConfig::new(addr, factory)
    .with_tls(connector, "server.example.com");

Protocol crates:

SecurityInfo

After handshake, Endpoint::security_info() reports (via core SecurityInfo): secure flag, ALPN protocol, TLS protocol version, cipher suite, the SNI hostname the peer requested (sni(), server side), and — when mTLS presented a client certificate — its SHA-256 fingerprint as lowercase hex (peer_certificate_fingerprint()) and full certificate chain (peer_certificate_chain()), server side. Handlers may gate AUTH or HTTP/2 on this metadata (ProtocolHandler::security_established). The fingerprint is the cert_key expected by SASL EXTERNAL's CredentialStore::authenticate_certificate.

Examples

cargo run -p tls-echo -- 127.0.0.1:8443 cert.pem key.pem
Package Demonstrates
examples/tls-echo Accept TLS, echo plaintext to handler

See Cookbook: TLS echo. No environment variables.

Limitations

Independent verification

Production TCP TLS is entirely hopf-core::tls as described above. The workspace additionally runs socket-level interop tests that complete real handshakes against rustls (TLS 1.3 and 1.2, both roles, ALPN, mTLS, resumption, STARTTLS) so wire behaviour is checked against a second implementation, not only Hopf-to-Hopf loops. DTLS gets a separate real-peer suite (OpenSSL). Status tables: Conformance → TLS.