TLS
Overview
TLS involves three types:
SslCtx— shared configuration (certificates, ciphers, protocol version). Can be wrapped inArc<SslCtx>and shared across threads.Ssl— per-connection state. Exclusive ownership; notClone.SslSession— resumable session. Reference-counted; can be shared.
Protocol Version
#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TlsVersion {
Tls12 = 0x0303,
Tls13 = 0x0304,
}
}
Verify Mode
#![allow(unused)]
fn main() {
pub struct SslVerifyMode(i32);
impl SslVerifyMode {
pub const NONE: Self; // no peer certificate required
pub const PEER: Self; // request peer certificate
pub const FAIL_IF_NO_PEER_CERT: Self; // require peer certificate
pub fn or(self, other: Self) -> Self;
}
}
I/O Errors
#![allow(unused)]
fn main() {
pub enum SslIoError {
WantRead, // non-blocking: no data available yet
WantWrite, // non-blocking: write buffer full
ZeroReturn, // peer closed cleanly
Syscall(ErrorStack), // underlying I/O syscall error
Ssl(ErrorStack), // OpenSSL protocol error
Other(i32), // unexpected error code
}
impl std::error::Error for SslIoError {}
}
SslCtx — Shared Configuration
#![allow(unused)]
fn main() {
pub struct SslCtx { /* SSL_CTX* */ }
impl SslCtx {
/// Generic context (TLS_method) — usable for both client and server.
pub fn new() -> Result<Self, ErrorStack>;
/// Client-only context (TLS_client_method).
pub fn new_client() -> Result<Self, ErrorStack>;
/// Server-only context (TLS_server_method).
pub fn new_server() -> Result<Self, ErrorStack>;
pub fn set_min_proto_version(&self, ver: TlsVersion) -> Result<(), ErrorStack>;
pub fn set_max_proto_version(&self, ver: TlsVersion) -> Result<(), ErrorStack>;
pub fn set_verify(&self, mode: SslVerifyMode);
/// Set TLS 1.2 cipher list (colon-separated OpenSSL names).
pub fn set_cipher_list(&self, list: &CStr) -> Result<(), ErrorStack>;
/// Set TLS 1.3 ciphersuites.
pub fn set_ciphersuites(&self, list: &CStr) -> Result<(), ErrorStack>;
pub fn use_certificate(&self, cert: &X509) -> Result<(), ErrorStack>;
pub fn use_private_key<T: HasPrivate>(&self, key: &Pkey<T>) -> Result<(), ErrorStack>;
pub fn check_private_key(&self) -> Result<(), ErrorStack>;
/// Load system default CA certificates.
pub fn set_default_verify_paths(&self) -> Result<(), ErrorStack>;
/// Disable session caching.
pub fn disable_session_cache(&self);
/// Create a new connection from this context.
pub fn new_ssl(&self) -> Result<Ssl, ErrorStack>;
}
impl Clone for SslCtx { /* SSL_CTX_up_ref */ }
impl Drop for SslCtx { /* SSL_CTX_free */ }
unsafe impl Send for SslCtx {}
unsafe impl Sync for SslCtx {}
}
Ssl — Per-Connection State
#![allow(unused)]
fn main() {
pub struct Ssl { /* SSL* */ }
impl Ssl {
/// Attach a single duplex BIO (rbio == wbio, e.g. a BIO pair end).
pub fn set_bio_duplex(&mut self, bio: Bio);
/// Attach separate read and write BIOs. Transfers ownership of both.
pub fn set_bio(&mut self, rbio: Bio, wbio: Bio);
/// Set the SNI hostname (calls SSL_ctrl directly; macro workaround).
pub fn set_hostname(&mut self, hostname: &CStr) -> Result<(), ErrorStack>;
pub fn set_connect_state(&mut self);
pub fn set_accept_state(&mut self);
pub fn connect(&mut self) -> Result<(), SslIoError>;
pub fn accept(&mut self) -> Result<(), SslIoError>;
pub fn do_handshake(&mut self) -> Result<(), SslIoError>;
pub fn read(&mut self, buf: &mut [u8]) -> Result<usize, SslIoError>;
pub fn write(&mut self, buf: &[u8]) -> Result<usize, SslIoError>;
pub fn shutdown(&mut self) -> Result<ShutdownResult, ErrorStack>;
/// Peer's certificate (owned; X509_up_ref'd).
pub fn peer_certificate(&self) -> Option<X509>;
/// Peer's full certificate chain — leaf + intermediates (each X509_up_ref'd).
/// Returns None before the handshake completes or when no peer cert was presented.
pub fn peer_cert_chain(&self) -> Option<Vec<X509>>;
/// Owned session (refcount bumped); suitable for storing beyond the connection.
pub fn get1_session(&self) -> Option<SslSession>;
/// Borrowed session (no refcount bump); valid only for the lifetime of `self`.
pub fn session(&self) -> Option<BorrowedSslSession<'_>>;
pub fn set_session(&mut self, session: &SslSession) -> Result<(), ErrorStack>;
}
impl Drop for Ssl { /* SSL_free */ }
unsafe impl Send for Ssl {}
// Ssl is !Clone and !Sync — exclusive ownership only
}
ShutdownResult
#![allow(unused)]
fn main() {
pub enum ShutdownResult {
Sent, // first close_notify sent; call shutdown() again to complete
Complete, // bidirectional shutdown finished
}
}
SslSession
#![allow(unused)]
fn main() {
pub struct SslSession { /* SSL_SESSION* */ }
impl Clone for SslSession { /* SSL_SESSION_up_ref */ }
impl Drop for SslSession { /* SSL_SESSION_free */ }
unsafe impl Send for SslSession {}
unsafe impl Sync for SslSession {}
}
BorrowedSslSession<'ssl>
A borrowed view of the current TLS session, obtained from [Ssl::session].
#![allow(unused)]
fn main() {
pub struct BorrowedSslSession<'ssl> { /* ... */ }
// Derefs to SslSession — all read-only methods are available.
impl Deref for BorrowedSslSession<'_> { type Target = SslSession; }
unsafe impl Send for BorrowedSslSession<'_> {}
}
Unlike [Ssl::get1_session], which increments the reference count and returns
an owned SslSession, Ssl::session calls SSL_get_session (the get0
variant) and wraps the result in a BorrowedSslSession that does not free
the pointer when dropped. Use it when you only need to inspect the session
within the scope of the connection.
#![allow(unused)]
fn main() {
// Borrow — no refcount bump, tied to the lifetime of `ssl`.
if let Some(sess) = ssl.session() {
// use sess ...
} // sess is dropped here; the session pointer is NOT freed.
// Owned — refcount bumped; can be stored and passed to set_session later.
if let Some(sess) = ssl.get1_session() {
new_ssl.set_session(&sess)?;
}
}
Examples
TLS Client
#![allow(unused)]
fn main() {
use native_ossl::ssl::{SslCtx, TlsVersion, SslVerifyMode};
use native_ossl::bio::Bio;
use std::net::TcpStream;
let ctx = SslCtx::new_client()?;
ctx.set_min_proto_version(TlsVersion::Tls12)?;
ctx.set_verify(SslVerifyMode::PEER);
ctx.set_default_verify_paths()?;
let mut ssl = ctx.new_ssl()?;
ssl.set_hostname(c"example.com")?;
// Wrap a TCP stream with a memory BIO
// (attach the stream-backed BIO for a real connection)
ssl.set_connect_state();
ssl.connect()?; // performs handshake
let request = b"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n";
ssl.write(request)?;
let mut buf = vec![0u8; 4096];
let n = ssl.read(&mut buf)?;
println!("{}", std::str::from_utf8(&buf[..n]).unwrap_or("(non-UTF-8)"));
ssl.shutdown()?;
}
TLS Server
#![allow(unused)]
fn main() {
use native_ossl::ssl::{SslCtx, TlsVersion, SslVerifyMode};
use native_ossl::x509::X509;
use native_ossl::pkey::Pkey;
let cert = X509::from_pem(&std::fs::read("server.crt")?)?;
let key = Pkey::<native_ossl::pkey::Private>::from_pem(&std::fs::read("server.key")?)?;
let ctx = SslCtx::new_server()?;
ctx.set_min_proto_version(TlsVersion::Tls12)?;
ctx.use_certificate(&cert)?;
ctx.use_private_key(&key)?;
ctx.check_private_key()?;
// For each accepted connection:
let mut ssl = ctx.new_ssl()?;
// ssl.set_bio(...); // attach the accepted socket's BIO
ssl.set_accept_state();
ssl.accept()?; // performs handshake
}
In-Memory TLS Handshake (Testing)
Use BIO_new_bio_pair to create two linked in-memory BIOs for testing without a
real socket:
#![allow(unused)]
fn main() {
use native_ossl::ssl::{SslCtx, TlsVersion};
use native_ossl::bio::Bio;
// Set up client and server contexts
let client_ctx = SslCtx::new_client()?;
client_ctx.set_min_proto_version(TlsVersion::Tls12)?;
let server_ctx = SslCtx::new_server()?;
server_ctx.use_certificate(&cert)?;
server_ctx.use_private_key(&key)?;
server_ctx.check_private_key()?;
let mut client = client_ctx.new_ssl()?;
let mut server = server_ctx.new_ssl()?;
// Create paired in-memory BIOs.
// Data written to bio1 is readable from bio2, and vice versa.
let (bio1, bio2) = Bio::new_bio_pair()?;
client.set_bio_duplex(bio1);
server.set_bio_duplex(bio2);
// Drive the handshake until both sides report success.
loop {
let c = client.do_handshake();
let s = server.do_handshake();
match (c, s) {
(Ok(()), Ok(())) => break,
(Err(native_ossl::ssl::SslIoError::WantRead), _) => continue,
(_, Err(native_ossl::ssl::SslIoError::WantRead)) => continue,
(Err(e), _) | (_, Err(e)) => return Err(e.into()),
}
}
// Exchange application data.
client.write(b"ping")?;
let mut buf = [0u8; 4];
server.read(&mut buf)?;
assert_eq!(&buf, b"ping");
}
Fluent Builders
SslCtxBuilder<R> is a typestate fluent builder for constructing a [SslCtx].
The type parameter R (either Server or Client) restricts which methods are
available, ensuring that server-only methods (e.g. certificate, private_key)
cannot be called on a client builder and vice versa.
Server Builder
#![allow(unused)]
fn main() {
use native_ossl::ssl::{SslCtxBuilder, Server, TlsVersion};
use native_ossl::x509::X509;
use native_ossl::pkey::Pkey;
let cert = X509::from_pem(&std::fs::read("server.crt")?)?;
let key = Pkey::<native_ossl::pkey::Private>::from_pem(&std::fs::read("server.key")?)?;
let ctx = SslCtxBuilder::<Server>::new()?
.min_proto_version(TlsVersion::Tls12)?
.max_proto_version(TlsVersion::Tls13)?
.certificate(&cert)?
.private_key(&key)?
.check_private_key()?
// Optionally request a client certificate:
// .verify_client(false) // request but not mandatory
// .verify_client(true) // require
.build()?;
}
Client Builder
#![allow(unused)]
fn main() {
use native_ossl::ssl::{SslCtxBuilder, Client, TlsVersion, HostnameFlags};
let ctx = SslCtxBuilder::<Client>::new()?
.min_proto_version(TlsVersion::Tls12)?
.default_ca_paths()?
.verify_peer()
.verify_hostname("example.com")?
// Restrict wildcard matching — no partial wildcards like *.example.*:
.verify_hostname_flags(HostnameFlags::NO_PARTIAL_WILDCARDS)?
// Advertise HTTP/2 and HTTP/1.1 via ALPN:
.alpn_protos_list(&["h2", "http/1.1"])?
.build()?;
}
ALPN: alpn_protos_list vs alpn_protocols
alpn_protos_list takes a &[&str] and encodes the protocol names into the
OpenSSL wire format (each name prefixed by its length as a single byte) before
calling SSL_CTX_set_alpn_protos. Protocol names longer than 255 bytes are
rejected.
alpn_protocols is the lower-level variant that accepts the pre-encoded wire
format directly (e.g. b"\x02h2\x08http/1.1"). Use it when you need to share
a pre-built buffer across multiple contexts.
Hostname verification: builder vs per-connection
SslCtxBuilder::<Client>::verify_hostname("host") sets the hostname on the
context-level X509_VERIFY_PARAM. Every Ssl connection derived from that
context inherits the check automatically.
When one SslCtx is reused across connections to different servers, call
Ssl::set_verify_hostname on each per-connection object instead:
#![allow(unused)]
fn main() {
let ctx = SslCtxBuilder::<Client>::new()?
.default_ca_paths()?
.verify_peer()
.build()?;
// Connection to server A
let mut ssl_a = ctx.new_ssl()?;
ssl_a.set_verify_hostname("a.example.com")?;
// Connection to server B
let mut ssl_b = ctx.new_ssl()?;
ssl_b.set_verify_hostname("b.example.com")?;
}
Wildcard control with verify_hostname_flags
verify_hostname_flags fine-tunes how wildcard certificates are matched.
It is typically called after verify_hostname and applies to all connections
derived from the context.
#![allow(unused)]
fn main() {
use native_ossl::ssl::{SslCtxBuilder, Client, HostnameFlags};
let ctx = SslCtxBuilder::<Client>::new()?
.default_ca_paths()?
.verify_peer()
.verify_hostname("example.com")?
// Disallow *.example.* style multi-label wildcards.
.verify_hostname_flags(HostnameFlags::NO_PARTIAL_WILDCARDS)?
.build()?;
}
Available flags:
| Constant | Value | Effect |
|---|---|---|
HostnameFlags::NONE | 0x0 | Default OpenSSL behaviour |
HostnameFlags::NO_PARTIAL_WILDCARDS | 0x4 | Disallow *.example.* style wildcards |
HostnameFlags::MULTI_LABEL_WILDCARDS | 0x8 | Allow wildcards matching multiple labels |
Flags can be combined with .or():
#![allow(unused)]
fn main() {
let flags = HostnameFlags::NO_PARTIAL_WILDCARDS.or(HostnameFlags::MULTI_LABEL_WILDCARDS);
builder.verify_hostname_flags(flags)?;
}
Builder API reference
| Method | Available on | Description |
|---|---|---|
min_proto_version(ver) | Both | Minimum TLS version |
max_proto_version(ver) | Both | Maximum TLS version |
cipher_list(list) | Both | TLS 1.2 cipher list |
ciphersuites(list) | Both | TLS 1.3 ciphersuites |
session_cache_mode(mode) | Both | Session cache mode (0 to disable) |
alpn_protocols(bytes) | Both | ALPN list in wire format |
alpn_protos_list(names) | Both | ALPN list from string slice |
build() | Both | Consume builder, produce SslCtx |
certificate(cert) | Server | Leaf certificate to present |
add_chain_cert(cert) | Server | Append intermediate CA to chain |
private_key(key) | Server | Private key for the certificate |
check_private_key() | Server | Verify cert/key consistency |
verify_client(require) | Server | Request/require client certificate |
default_ca_paths() | Client | Load system CA bundle |
ca_bundle_file(path) | Client | Load CA certificates from PEM file |
ca_cert(cert) | Client | Add one CA certificate to trust store |
verify_peer() | Client | Enable server certificate verification |
verify_hostname(host) | Client | Context-level hostname check |
verify_hostname_flags(flags) | Client | Wildcard matching flags (combine with verify_hostname) |
Design Notes
SslCtxmethods take&self— the underlying C functions takeSSL_CTX*but do not invalidate shared state.&selfallows multiple threads to configure from the sameArc<SslCtx>.- Protocol version macros —
SSL_CTX_set_min_proto_versionandSSL_CTX_set_max_proto_versionare C macros and cannot be exposed by bindgen. The implementation callsSSL_CTX_ctrldirectly with the numeric ctrl codes. - SNI is also a macro —
SSL_set_tlsext_host_nameis likewise a C macro. The implementation callsSSL_ctrlwith ctrl code 55. set_biotransfers ownership — afterset_bioorset_bio_duplex, the BIO is owned by theSSLobject. The Rust wrapper usesmem::forgetto avoid a double-free.SslisSendbut!Sync— exclusive ownership makes moving it to another thread safe, but sharing a reference without a mutex is not.- Session resumption — save the session with
ssl.get1_session()after a successful handshake, then pass it tossl.set_session(...)for the next connection to the same server. - Borrowed vs owned session —
ssl.session()callsSSL_get_session(theget0variant), returning aBorrowedSslSession<'_>that is tied to the connection’s lifetime and does not free the pointer on drop.ssl.get1_session()callsSSL_get1_session(theget1variant), bumps the reference count, and returns an ownedSslSessionthat can outlive the connection. Prefer the borrow when you only need to inspect the session in place. SslCtxBuilderisSendbut!Sync— the builder holds an exclusiveSSL_CTX*; moving it between threads is safe, but concurrent access via&reference is not (noArcis involved).SSL_CTX_set_alpn_protosinverted return — this OpenSSL function returns0on success and non-zero on failure, opposite to most OpenSSL functions. The builder checks for this correctly.peer_cert_chainvspeer_certificate—peer_certificatewrapsSSL_get0_peer_certificateand always returns the peer’s leaf certificate.peer_cert_chainwrapsSSL_get_peer_cert_chainand returns the entire chain (leaf + intermediates) as aVec<X509>with each element independently ref-counted. Note: on the server side OpenSSL’sSSL_get_peer_cert_chainomits the client leaf from the returned stack — usepeer_certificateto obtain the leaf in all cases.