solti-tls
Shared TLS / mTLS configuration for Solti network-facing crates.
Builders that hold intent (paths or in-memory PEM bytes) and produce rustls::ServerConfig / rustls::ClientConfig on demand.
Architecture
┌──────────────────────┐ ┌──────────────────────┐
│ PemSource::Path │ │ PemSource::Bytes │
│ (read at into_*()) │ │ (already in memory) │
└─────────┬────────────┘ └──────────┬───────────┘
│ │
▼ ▼
┌───────────────────────────────────────┐
│ ServerTlsConfig / ClientTlsConfig │
│ (Builder → built struct) │
└───────────────────┬───────────────────┘
│ into_rustls_config()
▼
┌───────────────────────────────────────┐
│ rustls::ServerConfig / ClientConfig │
│ (ALPN applied, mTLS verifier set) │
└───────────────────────────────────────┘
Why this crate
- Single source of truth: one TLS config shape for every solti network-facing crate (
solti-api,solti-discover, ...). - Lazy I/O: paths are validated and read only at
into_rustls_config(),DiscoverConfigcan be cloned around freely before any cert is touched. - Both paths and bytes: cert may live on disk or arrive in memory.
- Auto-installs
ring: callsrustls::crypto::ring::default_provider().install_default()if noCryptoProvideris set process-wide. - mTLS first-class: server and client builders both have a single
require_client_ca…/client_cert…knob.
Quick start
TLS-terminated server
use ServerTlsConfig;
let server_cfg = builder
.cert_pem_file
.key_pem_file
.with_alpn // gRPC; ["h2", "http/1.1"] for HTTP
.build?
.into_rustls_config?; // -> rustls::ServerConfig
// Plug into axum-server:
// axum_server::bind_rustls(addr, RustlsConfig::from_config(Arc::new(server_cfg)))
// .serve(router.into_make_service()).await?;
TLS-validating client
use ClientTlsConfig;
let client_cfg = builder
.ca_pem_file
.with_alpn
.build?
.into_rustls_config?; // -> rustls::ClientConfig
// Plug into reqwest:
// let http = reqwest::Client::builder().use_preconfigured_tls(client_cfg).build()?;
mTLS
Server requires a client certificate signed by clients-ca.crt:
use ServerTlsConfig;
let server_cfg = builder
.cert_pem_file
.key_pem_file
.require_client_ca_pem_file
.build?
.into_rustls_config?;
Client presents its own certificate:
use ClientTlsConfig;
let client_cfg = builder
.ca_pem_file
.client_cert_pem_file
.client_key_pem_file
.build?
.into_rustls_config?;
client_cert and client_key must be set together (or neither). Setting only
one returns TlsError::MissingField("client_key") / MissingField("client_cert").
Key types
| Type | Role |
|---|---|
ServerTlsConfig |
Server-side intent (cert, key, optional client-CA, ALPN) |
ServerTlsConfigBuilder |
Validated builder; missing required fields fail at build() |
ClientTlsConfig |
Client-side intent (CA, optional client cert+key, ALPN) |
ClientTlsConfigBuilder |
Validated builder; client cert/key must be paired |
PemSource |
Path(PathBuf) or Bytes(Vec<u8>) — lazy read via read() |
TlsError |
#[non_exhaustive] error enum (see Error model) |
ensure_default_provider |
Idempotently installs ring as the rustls CryptoProvider |
load_certs_from_pem |
Pure parser: PEM bytes → Vec<CertificateDer<'static>> |
load_key_from_pem |
Pure parser: PEM bytes → PrivateKeyDer<'static> |
PemSource: paths vs bytes
use PemSource;
// File on disk: read at into_rustls_config() time
let from_disk = Path;
// In-memory blob (e.g. from Vault, AWS Secrets Manager, env var)
let from_memory = Bytes;
The builder convenience methods cover both forms:
| Path form | Bytes form |
|---|---|
cert_pem_file(path) |
cert_pem_bytes(bytes) |
key_pem_file(path) |
key_pem_bytes(bytes) |
ca_pem_file(path) |
ca_pem_bytes(bytes) |
require_client_ca_pem_file(p) |
require_client_ca_pem_bytes(b) |
client_cert_pem_file(path) |
client_cert_pem_bytes(bytes) |
client_key_pem_file(path) |
client_key_pem_bytes(bytes) |
ALPN
ALPN protocols are stored in preference order and copied verbatim into rustls::*Config::alpn_protocols, as default is empty (no ALPN).
| Use case | with_alpn(...) |
|---|---|
| gRPC only | ["h2"] |
| HTTP/2 + 1 | ["h2", "http/1.1"] |
| HTTP/1.1 | ["http/1.1"] |
Integration patterns
solti-api server (gRPC + tonic)
use ;
use ServerTlsConfig;
let server_tls = builder
.cert_pem_file
.key_pem_file
.build?;
let tls_cfg = to_tonic_server_tls?;
builder
.tls_config?
.add_service
.serve
.await?;
Requires solti-api features grpc + tls.
solti-api server (HTTP + axum-server)
axum does not terminate TLS itself; bind through axum-server:
use RustlsConfig;
use HttpApi;
use ServerTlsConfig;
let rustls_cfg = builder
.cert_pem_file
.key_pem_file
.with_alpn
.build?
.into_rustls_config?;
let router = new.router;
bind_rustls
.serve.await?;
solti-tls does not depend on axum-server; add it in your binary.
solti-discover client (gRPC + HTTP)
with_tls accepts a solti_tls::ClientTlsConfig directly:
use DiscoverConfig;
use ClientTlsConfig;
let client_tls = builder
.ca_pem_file
.client_cert_pem_file
.client_key_pem_file
.build?;
let cfg = builder
.with_tls
.build?;
Internally, solti-discover:
- For HTTP (reqwest) uses
use_preconfigured_tls(rustls_config): passes the builtrustls::ClientConfigstraight in. - For gRPC (tonic) converts to
tonic::transport::ClientTlsConfig: tonic builds its own internal rustls config from PEM blobs.
Requires solti-discover features tls + (http or grpc).
Cryptography
- Provider:
ring(selected via therustlsfeaturering). - Roots: no
rustls-native-certsintegration. Trust roots come exclusively from PEM bundles you provide. This is intentional: for service-to-service TLS in a controlled environment, the trust set is tightly defined. - Auto-install:
into_rustls_config()callsensure_default_provider()which installsringif no provider is set process-wide.
Error model
| Variant | Cause |
|---|---|
Io |
PemSource::Path::read() failed (std::io::Error is the source) |
NoCertificates |
PEM input parsed cleanly but contained no CERTIFICATE blocks |
NoPrivateKey |
PEM input parsed cleanly but contained no PRIVATE KEY block |
MissingField |
Builder rejected on build() (e.g. cert, key, paired client_cert) |
Rustls |
rustls rejected the configuration (cert/key mismatch, etc.) |
ClientVerifier |
WebPkiClientVerifier::builder(...).build() rejected the trust roots |
All variants implement std::error::Error. The enum is #[non_exhaustive].
Feature flags
This crate has no public feature flags: default = [], all functionality is always built.
Consumers of solti-tls (e.g. solti-api, solti-discover) gate their integration behind their own tls feature.
Notes
into_rustls_config()consumesself. Clone the config first if you need to keep the builder-side struct around (e.g. for diagnostics).solti-tlsdoes not configure session resumption, ticket keys, or OCSP stapling: defaults fromrustlsapply.- The runnable demo lives at
examples/tls-roundtrip: generates an in-memory PKI, brings up an HTTPS server (axum-server) and a gRPC server (tonic+tonic-health) with mTLS, then makes a client round trip through each.