Skip to main content

native_ossl/
lib_ctx.rs

1//! `LibCtx` — `OSSL_LIB_CTX` wrapper and `Provider` — `OSSL_PROVIDER` wrapper.
2//!
3//! Most callers use the global default library context and never need this module.
4//! Create an explicit context when:
5//! - FIPS mode is required (load `"fips"` + `"base"` providers).
6//! - Per-process isolation of algorithm state is needed.
7//!
8//! ## `Arc<LibCtx>` ownership
9//!
10//! OpenSSL does not expose `OSSL_LIB_CTX_up_ref`.  Rust manages the lifetime
11//! via `Arc<LibCtx>`.  Algorithm descriptors that use an explicit context store
12//! `Option<Arc<LibCtx>>` to extend its lifetime.
13
14use crate::error::ErrorStack;
15use native_ossl_sys as sys;
16use std::ffi::CString;
17use std::sync::atomic::{AtomicU64, Ordering};
18
19static PKCS11_CFG_COUNTER: AtomicU64 = AtomicU64::new(0);
20
21// ── LibCtx ────────────────────────────────────────────────────────────────────
22
23/// An OpenSSL library context (`OSSL_LIB_CTX*`).
24///
25/// Wrap in `Arc<LibCtx>` before passing to algorithm descriptors; they will
26/// clone the `Arc` to keep the context alive.
27#[derive(Debug)]
28pub struct LibCtx {
29    ptr: *mut sys::OSSL_LIB_CTX,
30    /// When `false` this `LibCtx` was created from an externally-owned pointer
31    /// and must **not** call `OSSL_LIB_CTX_free` on drop.
32    owned: bool,
33}
34
35impl LibCtx {
36    /// Create a new, empty library context.
37    ///
38    /// No providers are loaded by default.  Call `load_provider` at least once
39    /// before using algorithms.
40    ///
41    /// # Errors
42    ///
43    /// Returns `Err` if OpenSSL cannot allocate the context.
44    pub fn new() -> Result<Self, ErrorStack> {
45        let ptr = unsafe { sys::OSSL_LIB_CTX_new() };
46        if ptr.is_null() {
47            return Err(ErrorStack::drain());
48        }
49        Ok(LibCtx { ptr, owned: true })
50    }
51
52    /// Load a provider into this library context.
53    ///
54    /// Common provider names:
55    /// - `c"default"` — standard algorithms.
56    /// - `c"fips"` — FIPS 140-3 validated algorithms (requires OpenSSL FIPS module).
57    /// - `c"base"` — must be loaded alongside `"fips"` for PEM/DER encoders.
58    ///
59    /// The returned `Provider` keeps the provider loaded until dropped.
60    ///
61    /// # Errors
62    ///
63    /// Returns `Err` if the provider cannot be loaded.
64    pub fn load_provider(&self, name: &std::ffi::CStr) -> Result<Provider, ErrorStack> {
65        let ptr = unsafe { sys::OSSL_PROVIDER_load(self.ptr, name.as_ptr()) };
66        if ptr.is_null() {
67            return Err(ErrorStack::drain());
68        }
69        Ok(Provider { ptr })
70    }
71
72    /// Return the raw `OSSL_LIB_CTX*` pointer.  Valid while `self` is alive.
73    #[must_use]
74    pub fn as_ptr(&self) -> *mut sys::OSSL_LIB_CTX {
75        self.ptr
76    }
77
78    /// Load an `openssl.cnf`-format configuration file into this context.
79    ///
80    /// Providers and algorithm configuration declared in the file are activated
81    /// within this context only — the global default context is not affected.
82    ///
83    /// # Errors
84    ///
85    /// Returns `Err` if `path` contains a null byte, the file cannot be read,
86    /// or the configuration is syntactically invalid.
87    pub fn load_config(&self, path: &std::path::Path) -> Result<(), ErrorStack> {
88        let path_str = path.to_str().ok_or_else(ErrorStack::drain)?;
89        let path_cs = CString::new(path_str).map_err(|_| ErrorStack::drain())?;
90        // SAFETY: self.ptr is valid for the duration of this call;
91        // path_cs is NUL-terminated and lives through the FFI boundary.
92        let rc = unsafe { sys::OSSL_LIB_CTX_load_config(self.ptr, path_cs.as_ptr()) };
93        if rc == 1 {
94            Ok(())
95        } else {
96            Err(ErrorStack::drain())
97        }
98    }
99
100    /// Load and activate the `pkcs11` provider, pointing it at `module`.
101    ///
102    /// Writes a minimal `openssl.cnf` snippet to a temporary file, calls
103    /// [`load_config`](Self::load_config) on it, then loads the `pkcs11`
104    /// provider into this context.  The temporary file is deleted before this
105    /// function returns.
106    ///
107    /// The returned [`Provider`] keeps the provider active until dropped.
108    ///
109    /// # Errors
110    ///
111    /// Returns `Err` if `module` cannot be represented as a valid path string,
112    /// if the temporary file cannot be written, or if the provider fails to load.
113    pub fn load_pkcs11_provider(&self, module: &std::path::Path) -> Result<Provider, ErrorStack> {
114        let module_str = module.to_str().ok_or_else(ErrorStack::drain)?;
115        let config = format!(
116            "openssl_conf = openssl_init\n\
117             [openssl_init]\n\
118             providers = provider_sect\n\
119             [provider_sect]\n\
120             pkcs11 = pkcs11_sect\n\
121             default = default_sect\n\
122             [pkcs11_sect]\n\
123             module = pkcs11-provider.so\n\
124             pkcs11-module-path = {module_str}\n\
125             activate = 1\n\
126             [default_sect]\n\
127             activate = 1\n"
128        );
129        // Write config to a uniquely-named temp file, load it, then remove it.
130        // Using process ID + a counter ensures uniqueness in concurrent tests.
131        let seq = PKCS11_CFG_COUNTER.fetch_add(1, Ordering::Relaxed);
132        let tmp_path = std::env::temp_dir().join(format!(
133            "native-ossl-pkcs11-{}-{seq}.cnf",
134            std::process::id()
135        ));
136        std::fs::write(&tmp_path, config.as_bytes()).map_err(|_| ErrorStack::drain())?;
137        let result = self.load_config(&tmp_path);
138        let _ = std::fs::remove_file(&tmp_path);
139        result?;
140        self.load_provider(c"pkcs11")
141    }
142
143    /// Wrap a raw `OSSL_LIB_CTX*` that is owned and managed externally.
144    ///
145    /// The resulting `LibCtx` will NOT call `OSSL_LIB_CTX_free` when dropped.
146    /// Use this only when the raw pointer's lifetime is guaranteed to exceed the
147    /// `LibCtx` (e.g. a context received from a FIPS provider callback).
148    ///
149    /// # Safety
150    ///
151    /// The caller must ensure that `ptr` is a valid, non-null `OSSL_LIB_CTX*`
152    /// that remains valid for as long as the returned `LibCtx` is alive.
153    pub unsafe fn from_raw_unowned(ptr: *mut sys::OSSL_LIB_CTX) -> Self {
154        LibCtx { ptr, owned: false }
155    }
156}
157
158impl Drop for LibCtx {
159    fn drop(&mut self) {
160        if self.owned {
161            unsafe { sys::OSSL_LIB_CTX_free(self.ptr) };
162        }
163    }
164}
165
166// SAFETY: `OSSL_LIB_CTX` is designed to be shared across threads after setup.
167unsafe impl Send for LibCtx {}
168unsafe impl Sync for LibCtx {}
169
170// ── Provider ──────────────────────────────────────────────────────────────────
171
172/// A loaded OpenSSL provider (`OSSL_PROVIDER*`).
173///
174/// The provider remains loaded until this value is dropped.  Keep it alive for
175/// the lifetime of any algorithm descriptors that use the associated `LibCtx`.
176pub struct Provider {
177    ptr: *mut sys::OSSL_PROVIDER,
178}
179
180impl Drop for Provider {
181    fn drop(&mut self) {
182        unsafe { sys::OSSL_PROVIDER_unload(self.ptr) };
183    }
184}
185
186// SAFETY: `OSSL_PROVIDER*` is managed by the library context thread-safely.
187unsafe impl Send for Provider {}
188unsafe impl Sync for Provider {}
189
190// ── Tests ─────────────────────────────────────────────────────────────────────
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195    use std::sync::Arc;
196
197    #[test]
198    fn create_and_drop() {
199        let ctx = LibCtx::new().unwrap();
200        drop(ctx);
201    }
202
203    #[test]
204    fn load_default_provider() {
205        let ctx = LibCtx::new().unwrap();
206        let _prov = ctx.load_provider(c"default").unwrap();
207        // Provider unloaded when `_prov` drops.
208    }
209
210    #[test]
211    fn arc_shared_context() {
212        let ctx = Arc::new(LibCtx::new().unwrap());
213        let _prov = ctx.load_provider(c"default").unwrap();
214        let ctx2 = Arc::clone(&ctx);
215        drop(ctx);
216        // ctx2 keeps the context alive; prov is still valid.
217        drop(ctx2);
218    }
219}