Trusty: provenance library workspace (initial import)
This commit is contained in:
222
crates/extensions/trusty-sqlite-ext/src/lib.rs
Normal file
222
crates/extensions/trusty-sqlite-ext/src/lib.rs
Normal file
@@ -0,0 +1,222 @@
|
||||
//! Trusty as a SQLite loadable extension (`cdylib`).
|
||||
//!
|
||||
//! Load into any SQLite database to get tamper-evidence without changing your
|
||||
//! schema or language stack. Exposes SQL scalar functions backed by a shared,
|
||||
//! lazily-initialized [`Trusty`](trusty_sqlite::Trusty) instance.
|
||||
//!
|
||||
//! # Loading
|
||||
//! ```sql
|
||||
//! .load ./trusty_sqlite_ext
|
||||
//! SELECT trusty_init('/path/to/trusty.db', 'hex-encoded-secret-key');
|
||||
//! ```
|
||||
//!
|
||||
//! # SQL functions
|
||||
//! - `trusty_init(db_path, secret_key_hex)` -- initialize (call once)
|
||||
//! - `trusty_append(entity_id, data_json, source_url)` -- append a record
|
||||
//! - `trusty_verify(entity_id)` -- verify chain integrity, returns JSON report
|
||||
//! - `trusty_anchor(entity_id)` -- write a SHA-256 anchor commitment
|
||||
//! - `trusty_version()` -- returns crate version string
|
||||
//!
|
||||
//! # Safety invariants
|
||||
//! Never panics across the FFI boundary. All fallible paths use rusqlite error
|
||||
//! propagation. The only permitted unwraps are `Mutex::lock()` (poisoning is
|
||||
//! terminal by design) and `serde_json::to_string` of always-serializable types.
|
||||
// - Global Trusty state guarded by a Mutex; append is mutable.
|
||||
// - Extension is non-persistent (re-registers per connection).
|
||||
// ============================================================
|
||||
|
||||
use std::os::raw::{c_char, c_int};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use rusqlite::ffi;
|
||||
use rusqlite::functions::FunctionFlags;
|
||||
use rusqlite::{Connection, Error, Result};
|
||||
|
||||
use trusty_core::SecretKey;
|
||||
use trusty_sqlite::Trusty;
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Global extension state.
|
||||
// One Trusty per process. Mutex because SQLite function callbacks
|
||||
// are invoked in arbitrary threading contexts and append mutates.
|
||||
// ------------------------------------------------------------
|
||||
static STATE: Mutex<Option<Trusty>> = Mutex::new(None);
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Extension entry point.
|
||||
//
|
||||
// SQLite's auto-loader for a library named `trusty_sqlite_ext`
|
||||
// looks for `sqlite3_trustysqliteext_init` (underscores stripped,
|
||||
// lowercased). We hand-write the entry point rather than rely on
|
||||
// a macro that varies across rusqlite versions.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
/// # Safety
|
||||
/// Called by sqlite3 at load time with valid, non-null `db` and
|
||||
/// `p_api` pointers. `_pz_err_msg` is unused — we return
|
||||
/// SQLITE_ERROR on failure and let sqlite3 surface a generic msg.
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sqlite3_trustysqliteext_init(
|
||||
db: *mut ffi::sqlite3,
|
||||
_pz_err_msg: *mut *mut c_char,
|
||||
p_api: *mut ffi::sqlite3_api_routines,
|
||||
) -> c_int {
|
||||
extension_entry(db, p_api)
|
||||
}
|
||||
|
||||
/// Alias entry point so the extension also loads under the short
|
||||
/// name `trusty_ext` (e.g. if a user renames the artifact). SQLite
|
||||
/// would derive `sqlite3_trustyext_init` from `trusty_ext`.
|
||||
///
|
||||
/// # Safety
|
||||
/// Same contract as [`sqlite3_trustysqliteext_init`].
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn sqlite3_trustyext_init(
|
||||
db: *mut ffi::sqlite3,
|
||||
_pz_err_msg: *mut *mut c_char,
|
||||
p_api: *mut ffi::sqlite3_api_routines,
|
||||
) -> c_int {
|
||||
extension_entry(db, p_api)
|
||||
}
|
||||
|
||||
/// Shared entry-point body. rusqlite 0.31's `extension_init2`
|
||||
/// installs the SQLITE_EXTENSION_INIT2 api-routines table and
|
||||
/// returns a non-owning `Connection`. We then register our
|
||||
/// functions on it. The connection is `forget`-ed because SQLite
|
||||
/// owns the underlying handle — dropping would double-close.
|
||||
///
|
||||
/// # Safety
|
||||
/// `db` and `p_api` must be the valid pointers passed by SQLite at
|
||||
/// extension-load time.
|
||||
unsafe fn extension_entry(
|
||||
db: *mut ffi::sqlite3,
|
||||
p_api: *mut ffi::sqlite3_api_routines,
|
||||
) -> c_int {
|
||||
match Connection::extension_init2(db, p_api) {
|
||||
Ok(conn) => match register_functions(&conn) {
|
||||
Ok(()) => {
|
||||
std::mem::forget(conn);
|
||||
ffi::SQLITE_OK
|
||||
}
|
||||
Err(_) => ffi::SQLITE_ERROR,
|
||||
},
|
||||
Err(_) => ffi::SQLITE_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Function registration.
|
||||
// ------------------------------------------------------------
|
||||
fn register_functions(db: &Connection) -> Result<()> {
|
||||
// trusty_init(db_path TEXT, secret_key_hex TEXT) -> TEXT
|
||||
db.create_scalar_function(
|
||||
"trusty_init",
|
||||
2,
|
||||
FunctionFlags::SQLITE_UTF8,
|
||||
|ctx| {
|
||||
let path: String = ctx.get(0)?;
|
||||
let key_hex: String = ctx.get(1)?;
|
||||
let key_bytes = hex_decode_16(&key_hex).map_err(sqlite_err)?;
|
||||
let key = SecretKey::new(key_bytes);
|
||||
let trusty = Trusty::open(&path, key).map_err(sqlite_err)?;
|
||||
*STATE.lock().unwrap() = Some(trusty);
|
||||
Ok("ok".to_string())
|
||||
},
|
||||
)?;
|
||||
|
||||
// trusty_append(entity_id TEXT, data_json TEXT, source_url TEXT) -> TEXT
|
||||
// NOT deterministic — each call mutates chain state.
|
||||
db.create_scalar_function(
|
||||
"trusty_append",
|
||||
3,
|
||||
FunctionFlags::SQLITE_UTF8,
|
||||
|ctx| {
|
||||
let entity_id: String = ctx.get(0)?;
|
||||
let data_json: String = ctx.get(1)?;
|
||||
let source_url: String = ctx.get(2)?;
|
||||
let data: serde_json::Value =
|
||||
serde_json::from_str(&data_json).map_err(sqlite_err)?;
|
||||
let mut guard = STATE.lock().unwrap();
|
||||
let trusty = guard.as_mut().ok_or_else(not_initialized)?;
|
||||
let result = trusty
|
||||
.append(&entity_id, &data, &source_url)
|
||||
.map_err(sqlite_err)?;
|
||||
// AppendResult is defined by trusty-sqlite and is always
|
||||
// serializable; unwrap is documented-safe.
|
||||
Ok(serde_json::to_string(&result).unwrap())
|
||||
},
|
||||
)?;
|
||||
|
||||
// trusty_verify(entity_id TEXT) -> TEXT
|
||||
// Deterministic for a given chain state (read-only).
|
||||
db.create_scalar_function(
|
||||
"trusty_verify",
|
||||
1,
|
||||
FunctionFlags::SQLITE_UTF8,
|
||||
|ctx| {
|
||||
let entity_id: String = ctx.get(0)?;
|
||||
let guard = STATE.lock().unwrap();
|
||||
let trusty = guard.as_ref().ok_or_else(not_initialized)?;
|
||||
let report = trusty.verify(&entity_id).map_err(sqlite_err)?;
|
||||
Ok(serde_json::to_string(&report).unwrap())
|
||||
},
|
||||
)?;
|
||||
|
||||
// trusty_anchor(entity_id TEXT) -> TEXT
|
||||
// Returns JSON of AnchorInfo, or the literal string "null" if
|
||||
// no chain exists for entity_id.
|
||||
db.create_scalar_function(
|
||||
"trusty_anchor",
|
||||
1,
|
||||
FunctionFlags::SQLITE_UTF8,
|
||||
|ctx| {
|
||||
let entity_id: String = ctx.get(0)?;
|
||||
let guard = STATE.lock().unwrap();
|
||||
let trusty = guard.as_ref().ok_or_else(not_initialized)?;
|
||||
let info = trusty.anchor(&entity_id).map_err(sqlite_err)?;
|
||||
Ok(serde_json::to_string(&info).unwrap())
|
||||
},
|
||||
)?;
|
||||
|
||||
// trusty_version() -> TEXT
|
||||
db.create_scalar_function(
|
||||
"trusty_version",
|
||||
0,
|
||||
FunctionFlags::SQLITE_UTF8 | FunctionFlags::SQLITE_DETERMINISTIC,
|
||||
|_ctx| Ok(env!("CARGO_PKG_VERSION").to_string()),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// Helpers.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
/// Decode a 32-character hex string into a 16-byte key.
|
||||
fn hex_decode_16(s: &str) -> std::result::Result<[u8; 16], String> {
|
||||
if s.len() != 32 {
|
||||
return Err(format!(
|
||||
"secret key hex must be 32 chars, got {}",
|
||||
s.len()
|
||||
));
|
||||
}
|
||||
let mut out = [0u8; 16];
|
||||
for i in 0..16 {
|
||||
out[i] = u8::from_str_radix(&s[2 * i..2 * i + 2], 16)
|
||||
.map_err(|e| format!("invalid hex at byte {}: {}", i, e))?;
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Wrap any Display error into a rusqlite user-function error.
|
||||
fn sqlite_err<E: std::fmt::Display>(e: E) -> Error {
|
||||
Error::UserFunctionError(Box::new(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
e.to_string(),
|
||||
)))
|
||||
}
|
||||
|
||||
fn not_initialized() -> Error {
|
||||
sqlite_err("trusty_init must be called before using trusty functions")
|
||||
}
|
||||
Reference in New Issue
Block a user