Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

TLS

Overview

TLS involves three types:

  • SslCtx — shared configuration (certificates, ciphers, protocol version). Can be wrapped in Arc<SslCtx> and shared across threads.
  • Ssl — per-connection state. Exclusive ownership; not Clone.
  • 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:

ConstantValueEffect
HostnameFlags::NONE0x0Default OpenSSL behaviour
HostnameFlags::NO_PARTIAL_WILDCARDS0x4Disallow *.example.* style wildcards
HostnameFlags::MULTI_LABEL_WILDCARDS0x8Allow 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

MethodAvailable onDescription
min_proto_version(ver)BothMinimum TLS version
max_proto_version(ver)BothMaximum TLS version
cipher_list(list)BothTLS 1.2 cipher list
ciphersuites(list)BothTLS 1.3 ciphersuites
session_cache_mode(mode)BothSession cache mode (0 to disable)
alpn_protocols(bytes)BothALPN list in wire format
alpn_protos_list(names)BothALPN list from string slice
build()BothConsume builder, produce SslCtx
certificate(cert)ServerLeaf certificate to present
add_chain_cert(cert)ServerAppend intermediate CA to chain
private_key(key)ServerPrivate key for the certificate
check_private_key()ServerVerify cert/key consistency
verify_client(require)ServerRequest/require client certificate
default_ca_paths()ClientLoad system CA bundle
ca_bundle_file(path)ClientLoad CA certificates from PEM file
ca_cert(cert)ClientAdd one CA certificate to trust store
verify_peer()ClientEnable server certificate verification
verify_hostname(host)ClientContext-level hostname check
verify_hostname_flags(flags)ClientWildcard matching flags (combine with verify_hostname)

Design Notes

  • SslCtx methods take &self — the underlying C functions take SSL_CTX* but do not invalidate shared state. &self allows multiple threads to configure from the same Arc<SslCtx>.
  • Protocol version macrosSSL_CTX_set_min_proto_version and SSL_CTX_set_max_proto_version are C macros and cannot be exposed by bindgen. The implementation calls SSL_CTX_ctrl directly with the numeric ctrl codes.
  • SNI is also a macroSSL_set_tlsext_host_name is likewise a C macro. The implementation calls SSL_ctrl with ctrl code 55.
  • set_bio transfers ownership — after set_bio or set_bio_duplex, the BIO is owned by the SSL object. The Rust wrapper uses mem::forget to avoid a double-free.
  • Ssl is Send but !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 to ssl.set_session(...) for the next connection to the same server.
  • Borrowed vs owned sessionssl.session() calls SSL_get_session (the get0 variant), returning a BorrowedSslSession<'_> that is tied to the connection’s lifetime and does not free the pointer on drop. ssl.get1_session() calls SSL_get1_session (the get1 variant), bumps the reference count, and returns an owned SslSession that can outlive the connection. Prefer the borrow when you only need to inspect the session in place.
  • SslCtxBuilder is Send but !Sync — the builder holds an exclusive SSL_CTX*; moving it between threads is safe, but concurrent access via & reference is not (no Arc is involved).
  • SSL_CTX_set_alpn_protos inverted return — this OpenSSL function returns 0 on success and non-zero on failure, opposite to most OpenSSL functions. The builder checks for this correctly.
  • peer_cert_chain vs peer_certificatepeer_certificate wraps SSL_get0_peer_certificate and always returns the peer’s leaf certificate. peer_cert_chain wraps SSL_get_peer_cert_chain and returns the entire chain (leaf + intermediates) as a Vec<X509> with each element independently ref-counted. Note: on the server side OpenSSL’s SSL_get_peer_cert_chain omits the client leaf from the returned stack — use peer_certificate to obtain the leaf in all cases.