Skip to content

Commit 9c17377

Browse files
committed
Automatically upgrade NSS database schema
Implement a mechanism to check and upgrade the NSS SQLite database schema upon initialization. This change introduces a check that runs when the database is opened. It compares the columns in the `nssPublic` and `nssPrivate` tables against a list of known PKCS#11 attributes. If any columns for known attributes are missing, they are automatically added using `ALTER TABLE`. This ensures that existing databases can be seamlessly upgraded to support new attributes (e.g., from PKCS#11 3.2) without manual intervention. The logic for converting database rows to objects is also refactored to use a new column cache, improving efficiency and robustness. Signed-off-by: Simo Sorce <simo@redhat.com>
1 parent c731307 commit 9c17377

File tree

1 file changed

+123
-18
lines changed

1 file changed

+123
-18
lines changed

src/storage/nssdb/mod.rs

Lines changed: 123 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
//! with the SQLite databases used by NSS for storing certificates, public
66
//! keys, private keys, and trust settings.
77
8+
use std::borrow::Cow;
89
use std::fmt::Write as _;
910
use std::path::Path;
1011
use std::sync::{Arc, Mutex, MutexGuard};
@@ -107,6 +108,13 @@ fn nss_id_parse(nssid: &str) -> Result<(String, u32)> {
107108
/// (`CK_ULONG`). The column name format is 'a' followed by the hex value of
108109
/// the attribute type.
109110
fn nss_col_to_type(col: &str) -> Result<CK_ULONG> {
111+
if match col.chars().next() {
112+
Some(c) => c,
113+
None => '_',
114+
} != 'a'
115+
{
116+
return Err(CKR_DEVICE_ERROR)?;
117+
}
110118
Ok(CK_ULONG::from_str_radix(&col[1..], 16)?)
111119
}
112120

@@ -153,6 +161,10 @@ pub struct NSSStorage {
153161
config: NSSConfig,
154162
/// Thread-safe connection to the underlying SQLite database(s).
155163
conn: Arc<Mutex<Connection>>,
164+
/// Columns cache for the main tables (should have the same size as
165+
/// known attributes, but could be more if we open a newer DB than we
166+
/// know of).
167+
cols: Vec<CK_ULONG>,
156168
/// Key cache and encryption/decryption context for authenticated
157169
/// attributes.
158170
keys: KeysWithCaching,
@@ -333,6 +345,88 @@ impl NSSStorage {
333345
}
334346

335347
tx.commit()?;
348+
/* Call check_columns for tis side effect of initializing self.cols */
349+
Self::check_columns(
350+
&mut conn,
351+
&mut self.cols,
352+
NSS_PUBLIC_SCHEMA,
353+
NSS_PUBLIC_TABLE,
354+
)?;
355+
Self::check_columns(
356+
&mut conn,
357+
&mut self.cols,
358+
NSS_PRIVATE_SCHEMA,
359+
NSS_PRIVATE_TABLE,
360+
)?;
361+
Ok(())
362+
}
363+
364+
fn pragma_columns(
365+
conn: &mut MutexGuard<'_, rusqlite::Connection>,
366+
schema: &str,
367+
table: &str,
368+
) -> Result<Vec<String>> {
369+
let mut stmt = conn.prepare(&format!(
370+
"SELECT * FROM {}.pragma_table_info('{}')",
371+
schema, table
372+
))?;
373+
let mut cols = Vec::<String>::new();
374+
let mut rows = stmt.query([])?;
375+
while let Some(row) = rows.next()? {
376+
cols.push(row.get(1)?);
377+
}
378+
if cols.len() == 0 {
379+
return Err(CKR_GENERAL_ERROR)?;
380+
}
381+
return Ok(cols);
382+
}
383+
384+
/* check that the database has all the expected columns, add missing ones
385+
* as needed. This is to support transition of existing databases to be
386+
* able to operate with the new PKCS#11 3.2 attributes */
387+
fn check_columns(
388+
conn: &mut MutexGuard<'_, rusqlite::Connection>,
389+
cols_cache: &mut Vec<CK_ULONG>,
390+
schema: &str,
391+
table: &str,
392+
) -> Result<()> {
393+
let cols = Self::pragma_columns(conn, schema, table)?;
394+
395+
if cols_cache.len() == 0 {
396+
/* initialize with db order */
397+
for val in &cols {
398+
let attr = match nss_col_to_type(val) {
399+
Ok(a) => a,
400+
Err(_) => continue, /* ignore non attribute columns */
401+
};
402+
cols_cache.push(attr);
403+
}
404+
}
405+
406+
let mut not_found = NSS_KNOWN_ATTRIBUTES.to_vec();
407+
for val in &cols {
408+
let typ = match nss_col_to_type(val) {
409+
Ok(t) => t,
410+
Err(_) => continue, /* ignore non attribute columns */
411+
};
412+
/* For now we just ignore columns that are not known */
413+
if let Some(idx) = not_found.iter().position(|i| *i == typ) {
414+
not_found.swap_remove(idx);
415+
}
416+
}
417+
418+
/* Now check if any known attribute was not found and add the
419+
* related column to the schema */
420+
for typ in not_found {
421+
let _ = conn.execute(
422+
&format!(
423+
"ALTER TABLE {}.{} ADD COLUMN a{:x}",
424+
schema, table, typ
425+
),
426+
params![],
427+
)?;
428+
cols_cache.push(typ);
429+
}
336430
Ok(())
337431
}
338432

@@ -341,31 +435,31 @@ impl NSSStorage {
341435
/// Maps NSS column names (e.g., "a81") to attribute types and converts
342436
/// stored BLOBs/numbers back into `Attribute` values. Handles the
343437
/// special NULL value.
344-
fn rows_to_object(mut rows: Rows, all_cols: bool) -> Result<Object> {
438+
fn rows_to_object(
439+
&self,
440+
mut rows: Rows,
441+
attrs: &[CK_ATTRIBUTE],
442+
) -> Result<Object> {
345443
let mut obj = Object::new();
346444

347-
/* FIXME: move sourcing columns to db open */
348-
let cols = match rows.as_ref() {
349-
Some(s) => {
350-
let cstr = s.column_names();
351-
let mut ctypes = Vec::<CK_ULONG>::with_capacity(cstr.len());
352-
for cs in &cstr {
353-
ctypes.push(nss_col_to_type(cs)?);
354-
}
355-
ctypes
445+
let (cols, offset) = if attrs.len() == 0 {
446+
(Cow::Borrowed(&self.cols), 1)
447+
} else {
448+
let mut c = Vec::<CK_ULONG>::with_capacity(attrs.len());
449+
for a in attrs {
450+
c.push(a.type_);
356451
}
357-
None => return Err(CKR_GENERAL_ERROR)?,
452+
(Cow::Owned(c), 0)
358453
};
359454

360-
let first = if all_cols { 1 } else { 0 };
361-
362455
if let Some(row) = rows.next()? {
363-
for i in first..cols.len() {
456+
for i in 0..cols.len() {
364457
/* skip NSS vendor attributes */
365458
if ignore_attribute(cols[i]) {
366459
continue;
367460
}
368-
let bn: Option<&[u8]> = row.get_ref(i)?.as_blob_or_null()?;
461+
let bn: Option<&[u8]> =
462+
row.get_ref(i + offset)?.as_blob_or_null()?;
369463
let blob: &[u8] = match bn {
370464
Some(ref b) => b,
371465
None => continue,
@@ -477,7 +571,7 @@ impl NSSStorage {
477571
let conn = self.conn.lock()?;
478572
let mut stmt = conn.prepare(sql)?;
479573
let rows = stmt.query(rusqlite::params_from_iter(query.params))?;
480-
Self::rows_to_object(rows, attrs.len() == 0)
574+
self.rows_to_object(rows, attrs)
481575
}
482576

483577
/* Searching for Objects:
@@ -879,7 +973,12 @@ impl Storage for NSSStorage {
879973
self.config.read_only,
880974
)?;
881975
match check_table(&mut conn, NSS_PUBLIC_SCHEMA, NSS_PUBLIC_TABLE) {
882-
Ok(_) => (),
976+
Ok(_) => Self::check_columns(
977+
&mut conn,
978+
&mut self.cols,
979+
NSS_PUBLIC_SCHEMA,
980+
NSS_PUBLIC_TABLE,
981+
)?,
883982
Err(e) => ret = e.rv(),
884983
}
885984
}
@@ -892,7 +991,12 @@ impl Storage for NSSStorage {
892991
)?;
893992
match check_table(&mut conn, NSS_PRIVATE_SCHEMA, NSS_PRIVATE_TABLE)
894993
{
895-
Ok(_) => (),
994+
Ok(_) => Self::check_columns(
995+
&mut conn,
996+
&mut self.cols,
997+
NSS_PRIVATE_SCHEMA,
998+
NSS_PRIVATE_TABLE,
999+
)?,
8961000
Err(e) => ret = e.rv(),
8971001
}
8981002
}
@@ -1420,6 +1524,7 @@ impl StorageDBInfo for NSSDBInfo {
14201524
Ok(Box::new(NSSStorage {
14211525
config: config,
14221526
conn: conn,
1527+
cols: Vec::with_capacity(NSS_KNOWN_ATTRIBUTES.len()),
14231528
keys: KeysWithCaching::default(),
14241529
}))
14251530
}

0 commit comments

Comments
 (0)