Skip to content

Commit c2660a7

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 c2660a7

File tree

1 file changed

+91
-18
lines changed

1 file changed

+91
-18
lines changed

src/storage/nssdb/mod.rs

Lines changed: 91 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,12 @@ 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+
return Err(CKR_DEVICE_ERROR)?;
116+
}
110117
Ok(CK_ULONG::from_str_radix(&col[1..], 16)?)
111118
}
112119

@@ -153,6 +160,10 @@ pub struct NSSStorage {
153160
config: NSSConfig,
154161
/// Thread-safe connection to the underlying SQLite database(s).
155162
conn: Arc<Mutex<Connection>>,
163+
/// Columns cache for the main tables (should have the same size as
164+
/// known attributes, but could be more if we open a newer DB than we
165+
/// know of).
166+
cols: Vec<CK_ULONG>,
156167
/// Key cache and encryption/decryption context for authenticated
157168
/// attributes.
158169
keys: KeysWithCaching,
@@ -333,6 +344,72 @@ impl NSSStorage {
333344
}
334345

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

@@ -341,31 +418,26 @@ impl NSSStorage {
341418
/// Maps NSS column names (e.g., "a81") to attribute types and converts
342419
/// stored BLOBs/numbers back into `Attribute` values. Handles the
343420
/// special NULL value.
344-
fn rows_to_object(mut rows: Rows, all_cols: bool) -> Result<Object> {
421+
fn rows_to_object(&self, mut rows: Rows, attrs: &[CK_ATTRIBUTE]) -> Result<Object> {
345422
let mut obj = Object::new();
346423

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
424+
let (cols, offset) = if attrs.len() == 0 {
425+
(Cow::Borrowed(&self.cols), 1)
426+
} else {
427+
let mut c = Vec::<CK_ULONG>::with_capacity(attrs.len());
428+
for a in attrs {
429+
c.push(a.type_);
356430
}
357-
None => return Err(CKR_GENERAL_ERROR)?,
431+
(Cow::Owned(c), 0)
358432
};
359433

360-
let first = if all_cols { 1 } else { 0 };
361-
362434
if let Some(row) = rows.next()? {
363-
for i in first..cols.len() {
435+
for i in 0..cols.len() {
364436
/* skip NSS vendor attributes */
365437
if ignore_attribute(cols[i]) {
366438
continue;
367439
}
368-
let bn: Option<&[u8]> = row.get_ref(i)?.as_blob_or_null()?;
440+
let bn: Option<&[u8]> = row.get_ref(i + offset)?.as_blob_or_null()?;
369441
let blob: &[u8] = match bn {
370442
Some(ref b) => b,
371443
None => continue,
@@ -477,7 +549,7 @@ impl NSSStorage {
477549
let conn = self.conn.lock()?;
478550
let mut stmt = conn.prepare(sql)?;
479551
let rows = stmt.query(rusqlite::params_from_iter(query.params))?;
480-
Self::rows_to_object(rows, attrs.len() == 0)
552+
self.rows_to_object(rows, attrs)
481553
}
482554

483555
/* Searching for Objects:
@@ -879,7 +951,7 @@ impl Storage for NSSStorage {
879951
self.config.read_only,
880952
)?;
881953
match check_table(&mut conn, NSS_PUBLIC_SCHEMA, NSS_PUBLIC_TABLE) {
882-
Ok(_) => (),
954+
Ok(_) => Self::check_columns(&mut conn, &mut self.cols, NSS_PUBLIC_SCHEMA, NSS_PUBLIC_TABLE)?,
883955
Err(e) => ret = e.rv(),
884956
}
885957
}
@@ -892,7 +964,7 @@ impl Storage for NSSStorage {
892964
)?;
893965
match check_table(&mut conn, NSS_PRIVATE_SCHEMA, NSS_PRIVATE_TABLE)
894966
{
895-
Ok(_) => (),
967+
Ok(_) => Self::check_columns(&mut conn, &mut self.cols, NSS_PRIVATE_SCHEMA, NSS_PRIVATE_TABLE)?,
896968
Err(e) => ret = e.rv(),
897969
}
898970
}
@@ -1420,6 +1492,7 @@ impl StorageDBInfo for NSSDBInfo {
14201492
Ok(Box::new(NSSStorage {
14211493
config: config,
14221494
conn: conn,
1495+
cols: Vec::with_capacity(NSS_KNOWN_ATTRIBUTES.len()),
14231496
keys: KeysWithCaching::default(),
14241497
}))
14251498
}

0 commit comments

Comments
 (0)