nss/
util.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5use crate::error::*;
6use nss_sys::*;
7use std::{ffi::CString, os::raw::c_char};
8
9use std::path::PathBuf;
10use std::sync::OnceLock;
11
12#[cfg(feature = "keydb")]
13use crate::pk11::slot;
14
15// This is the NSS version that this crate is claiming to be compatible with.
16// We check it at runtime using `NSS_VersionCheck`.
17pub const COMPATIBLE_NSS_VERSION: &str = "3.26";
18
19// Expect NSS has been initialized. This is usually be done via `init_rust_components`, see
20// components/init_rust_components/README.md.
21pub fn assert_nss_initialized() {
22    INITIALIZED.get().expect(
23        "NSS has not initialized.
24    Please ensure you include the initialization component and call it early in your code. See
25    https://mozilla.github.io/application-services/book/rust-docs/init_rust_components/index.html",
26    );
27}
28
29// This and many other nss init code were either taken directly from or are inspired by
30// https://github.com/mozilla/neqo/blob/b931a289eee7d62c0815535f01cfa34c5a929f9d/neqo-crypto/src/lib.rs#L73-L77
31enum NssLoaded {
32    External,
33    NoDb,
34    #[cfg(feature = "keydb")]
35    Db,
36}
37
38static INITIALIZED: OnceLock<NssLoaded> = OnceLock::new();
39
40fn assert_compatible_version() {
41    let min_ver = CString::new(COMPATIBLE_NSS_VERSION).unwrap();
42    if unsafe { NSS_VersionCheck(min_ver.as_ptr()) } == PR_FALSE {
43        panic!("Incompatible NSS version!")
44    }
45}
46
47fn init_once(profile_path: Option<PathBuf>) -> NssLoaded {
48    assert_compatible_version();
49
50    if unsafe { NSS_IsInitialized() != PR_FALSE } {
51        return NssLoaded::External;
52    }
53
54    match profile_path {
55        #[cfg(feature = "keydb")]
56        Some(path) => {
57            if !path.is_dir() {
58                panic!("missing profile directory {:?}", path);
59            }
60            let pathstr = path.to_str().expect("invalid path");
61            let dircstr = CString::new(pathstr).expect("could not build CString from path");
62            let empty = CString::new("").expect("could not build empty CString");
63            let flags = NSS_INIT_FORCEOPEN | NSS_INIT_OPTIMIZESPACE;
64
65            let context = unsafe {
66                NSS_InitContext(
67                    dircstr.as_ptr(),
68                    empty.as_ptr(),
69                    empty.as_ptr(),
70                    empty.as_ptr(),
71                    std::ptr::null_mut(),
72                    flags,
73                )
74            };
75            if context.is_null() {
76                let error = get_last_error();
77                panic!("could not initialize context: {}", error);
78            }
79
80            let slot = slot::get_internal_key_slot().expect("could not get internal key slot");
81
82            if unsafe { PK11_NeedUserInit(slot.as_mut_ptr()) } == nss_sys::PR_TRUE {
83                let result = unsafe {
84                    PK11_InitPin(
85                        slot.as_mut_ptr(),
86                        std::ptr::null_mut(),
87                        std::ptr::null_mut(),
88                    )
89                };
90                if result != SECStatus::SECSuccess {
91                    let error = get_last_error();
92                    panic!("could not initialize context: {}", error);
93                }
94            }
95
96            NssLoaded::Db
97        }
98
99        #[cfg(not(feature = "keydb"))]
100        Some(_) => panic!("Use the keydb feature to enable nss initialization with profile path"),
101
102        None => {
103            let empty = CString::default();
104            let flags = NSS_INIT_READONLY
105                | NSS_INIT_NOCERTDB
106                | NSS_INIT_NOMODDB
107                | NSS_INIT_FORCEOPEN
108                | NSS_INIT_OPTIMIZESPACE;
109            let context = unsafe {
110                NSS_InitContext(
111                    empty.as_ptr(),
112                    empty.as_ptr(),
113                    empty.as_ptr(),
114                    empty.as_ptr(),
115                    std::ptr::null_mut(),
116                    flags,
117                )
118            };
119            if context.is_null() {
120                let error = get_last_error();
121                panic!("Could not initialize NSS: {}", error);
122            }
123
124            NssLoaded::NoDb
125        }
126    }
127}
128
129/// Initialize NSS. This only executes the initialization routines once, so if there is any chance
130/// that this is invoked twice, that's OK.
131///
132/// # Errors
133///
134/// When NSS initialization fails.
135pub fn ensure_nss_initialized() {
136    INITIALIZED.get_or_init(|| init_once(None));
137}
138
139/// Use this function to initialize NSS if you want to manage keys with NSS.
140/// ensure_initialized_with_profile_dir initializes NSS with a profile directory (where key4.db
141/// will be stored) and appropriate flags to persist keys (and certificates) in its internal PKCS11
142/// software implementation.
143/// If it has been called previously with a different path, it will fail.
144/// If `ensure_initialized` has been called before, it will also fail.
145#[cfg(feature = "keydb")]
146pub fn ensure_nss_initialized_with_profile_dir<P: Into<PathBuf>>(dir: P) {
147    INITIALIZED.get_or_init(|| init_once(Some(dir.into())));
148}
149
150pub fn map_nss_secstatus<F>(callback: F) -> Result<()>
151where
152    F: FnOnce() -> SECStatus,
153{
154    if callback() == SECStatus::SECSuccess {
155        return Ok(());
156    }
157    Err(get_last_error())
158}
159
160/// Retrieve and wrap the last NSS/NSPR error in the current thread.
161#[cold]
162pub fn get_last_error() -> Error {
163    let error_code = unsafe { PR_GetError() };
164    let error_text: String = usize::try_from(unsafe { PR_GetErrorTextLength() })
165        .map(|error_text_len| {
166            let mut out_str = vec![0u8; error_text_len + 1];
167            unsafe { PR_GetErrorText(out_str.as_mut_ptr() as *mut c_char) };
168            CString::new(&out_str[0..error_text_len])
169                .unwrap_or_else(|_| CString::default())
170                .to_str()
171                .unwrap_or("")
172                .to_owned()
173        })
174        .unwrap_or_else(|_| "".to_string());
175    ErrorKind::NSSError(error_code, error_text).into()
176}
177
178pub(crate) trait ScopedPtr
179where
180    Self: std::marker::Sized,
181{
182    type RawType;
183    unsafe fn from_ptr(ptr: *mut Self::RawType) -> Result<Self>;
184    fn as_ptr(&self) -> *const Self::RawType;
185    fn as_mut_ptr(&self) -> *mut Self::RawType;
186}
187
188// The macro defines a wrapper around pointers referring to types allocated by NSS,
189// calling their NSS destructor method when they go out of scope to avoid memory leaks.
190// The `as_ptr`/`as_mut_ptr` are provided to retrieve the raw pointers to pass to
191// NSS functions that consume them.
192#[macro_export]
193macro_rules! scoped_ptr {
194    ($scoped:ident, $target:ty, $dtor:path) => {
195        pub struct $scoped {
196            ptr: *mut $target,
197        }
198
199        impl $crate::util::ScopedPtr for $scoped {
200            type RawType = $target;
201
202            #[allow(dead_code)]
203            unsafe fn from_ptr(ptr: *mut $target) -> $crate::error::Result<$scoped> {
204                if !ptr.is_null() {
205                    Ok($scoped { ptr })
206                } else {
207                    Err($crate::error::ErrorKind::InternalError.into())
208                }
209            }
210
211            #[inline]
212            fn as_ptr(&self) -> *const $target {
213                self.ptr
214            }
215
216            #[inline]
217            fn as_mut_ptr(&self) -> *mut $target {
218                self.ptr
219            }
220        }
221
222        impl Drop for $scoped {
223            fn drop(&mut self) {
224                assert!(!self.ptr.is_null());
225                unsafe { $dtor(self.ptr) };
226            }
227        }
228    };
229}
230
231/// Copies a SECItem into a slice
232///
233/// # Safety
234///
235/// The returned reference must not outlive the `sym_key`, since that owns the `SecItem` buffer.
236pub(crate) unsafe fn sec_item_as_slice(sec_item: &mut SECItem) -> Result<&mut [u8]> {
237    let sec_item_buf_len = usize::try_from(sec_item.len)?;
238    let buf = std::slice::from_raw_parts_mut(sec_item.data, sec_item_buf_len);
239    Ok(buf)
240}
241
242#[cfg(test)]
243mod test {
244    use super::*;
245    use std::thread;
246
247    #[test]
248    fn test_assert_initialized() {
249        ensure_nss_initialized();
250        assert_nss_initialized();
251    }
252
253    #[cfg(feature = "keydb")]
254    #[test]
255    fn test_assert_initialized_with_profile_dir() {
256        ensure_nss_initialized_with_profile_dir("./");
257        assert_nss_initialized();
258    }
259
260    #[test]
261    fn test_ensure_initialized_multithread() {
262        let threads: Vec<_> = (0..2)
263            .map(|_| thread::spawn(ensure_nss_initialized))
264            .collect();
265
266        for handle in threads {
267            handle.join().unwrap();
268        }
269    }
270
271    #[cfg(feature = "keydb")]
272    #[test]
273    fn test_ensure_initialized_with_profile_dir_multithread() {
274        let threads: Vec<_> = (0..2)
275            .map(|_| thread::spawn(move || ensure_nss_initialized_with_profile_dir("./")))
276            .collect();
277
278        for handle in threads {
279            handle.join().unwrap();
280        }
281    }
282}