mod.rs (12056B)
1 //! Versioned schema authority for `private.sqlite`. 2 //! 3 //! The public descriptor surface exposes version and integrity metadata only. 4 //! Embedded SQL remains an implementation detail of this backend. 5 6 /// Lowest private schema version this package can recognize. 7 pub const MINIMUM_VERSION: u32 = 1; 8 /// Current private schema version created by this package. 9 pub const CURRENT_VERSION: u32 = 4; 10 11 const PRIVATE_V1_SQL: &str = include_str!("0001_private.up.sql"); 12 const LEGACY_PRIVATE_STAGING_V2_SQL: &str = include_str!("0002_legacy_private_staging.up.sql"); 13 const LEGACY_IMPORT_COMMITS_V3_SQL: &str = include_str!("0003_legacy_import_commits.up.sql"); 14 const CONTEXT_BOUND_ENVELOPES_V4_SQL: &str = include_str!("0004_context_bound_envelopes.up.sql"); 15 16 /// Stable, non-SQL description of one forward private migration. 17 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 18 pub struct MigrationDescriptor { 19 version: u32, 20 name: &'static str, 21 up_sha256: &'static str, 22 owned_objects: &'static [&'static str], 23 } 24 25 impl MigrationDescriptor { 26 pub const fn version(self) -> u32 { 27 self.version 28 } 29 30 pub const fn name(self) -> &'static str { 31 self.name 32 } 33 34 pub const fn up_sha256(self) -> &'static str { 35 self.up_sha256 36 } 37 38 pub const fn owned_objects(self) -> &'static [&'static str] { 39 self.owned_objects 40 } 41 } 42 43 const PRIVATE_V1_OBJECTS: &[&str] = &[ 44 "radroots_private_artifacts", 45 "radroots_private_artifacts_delete_guard", 46 "radroots_private_artifacts_envelope_guard", 47 "radroots_private_artifacts_expiry_idx", 48 "radroots_private_artifacts_identity_guard", 49 "radroots_private_artifacts_key_version_idx", 50 "radroots_private_artifacts_kind_idx", 51 ]; 52 53 const PRIVATE_V2_OBJECTS: &[&str] = &[ 54 "radroots_private_artifacts", 55 "radroots_private_artifacts_delete_guard", 56 "radroots_private_artifacts_envelope_guard", 57 "radroots_private_artifacts_expiry_idx", 58 "radroots_private_artifacts_identity_guard", 59 "radroots_private_artifacts_key_version_idx", 60 "radroots_private_artifacts_kind_idx", 61 "radroots_private_legacy_import_staging", 62 "radroots_private_legacy_import_staging_delete_guard", 63 "radroots_private_legacy_import_staging_insert_guard", 64 "radroots_private_legacy_import_staging_parent_idx", 65 "radroots_private_legacy_import_staging_update_guard", 66 ]; 67 68 const PRIVATE_V3_OBJECTS: &[&str] = &[ 69 "radroots_private_artifacts", 70 "radroots_private_artifacts_delete_guard", 71 "radroots_private_artifacts_envelope_guard", 72 "radroots_private_artifacts_expiry_idx", 73 "radroots_private_artifacts_identity_guard", 74 "radroots_private_artifacts_key_version_idx", 75 "radroots_private_artifacts_kind_idx", 76 "radroots_private_legacy_import_commit_delete_guard", 77 "radroots_private_legacy_import_commit_update_guard", 78 "radroots_private_legacy_import_commits", 79 "radroots_private_legacy_import_staging", 80 "radroots_private_legacy_import_staging_delete_guard", 81 "radroots_private_legacy_import_staging_insert_guard", 82 "radroots_private_legacy_import_staging_parent_idx", 83 "radroots_private_legacy_import_staging_update_guard", 84 ]; 85 86 const PRIVATE_V4_OBJECTS: &[&str] = &[ 87 "radroots_private_artifacts", 88 "radroots_private_artifacts_delete_guard", 89 "radroots_private_artifacts_envelope_guard", 90 "radroots_private_artifacts_expiry_idx", 91 "radroots_private_artifacts_identity_guard", 92 "radroots_private_artifacts_insert_envelope_guard", 93 "radroots_private_artifacts_key_version_idx", 94 "radroots_private_artifacts_kind_idx", 95 "radroots_private_artifacts_reseal_audit", 96 "radroots_private_envelope_reseals", 97 "radroots_private_envelope_reseals_artifact_idx", 98 "radroots_private_envelope_reseals_delete_guard", 99 "radroots_private_envelope_reseals_update_guard", 100 "radroots_private_legacy_import_commit_delete_guard", 101 "radroots_private_legacy_import_commit_update_guard", 102 "radroots_private_legacy_import_commits", 103 "radroots_private_legacy_import_staging", 104 "radroots_private_legacy_import_staging_delete_guard", 105 "radroots_private_legacy_import_staging_insert_guard", 106 "radroots_private_legacy_import_staging_parent_idx", 107 "radroots_private_legacy_import_staging_update_guard", 108 ]; 109 110 /// Ordered, immutable private migration plan. 111 pub const MIGRATIONS: &[MigrationDescriptor] = &[ 112 MigrationDescriptor { 113 version: 1, 114 name: "private_artifacts", 115 up_sha256: "07050386292ff8ce9ec0e756c9ac88e458a249d53e8654e1102caa2e361f11ab", 116 owned_objects: PRIVATE_V1_OBJECTS, 117 }, 118 MigrationDescriptor { 119 version: 2, 120 name: "legacy_private_staging", 121 up_sha256: "299ec0c476b2f5ab995f245d36603969af345827c9ecdf9490cfe0b0dbe4b9f9", 122 owned_objects: PRIVATE_V2_OBJECTS, 123 }, 124 MigrationDescriptor { 125 version: 3, 126 name: "legacy_import_commits", 127 up_sha256: "9377f0af8f070d977a5237e2a1294e6977f5b704e7a8434d97a3dc5f4ae75e86", 128 owned_objects: PRIVATE_V3_OBJECTS, 129 }, 130 MigrationDescriptor { 131 version: 4, 132 name: "context_bound_envelopes", 133 up_sha256: "dd6bb42471db47fcd9c62f8d59b66ddba381776d879cfbffd94993f7ef7409de", 134 owned_objects: PRIVATE_V4_OBJECTS, 135 }, 136 ]; 137 138 pub(crate) const fn migration_sql(version: u32) -> Option<&'static str> { 139 match version { 140 1 => Some(PRIVATE_V1_SQL), 141 2 => Some(LEGACY_PRIVATE_STAGING_V2_SQL), 142 3 => Some(LEGACY_IMPORT_COMMITS_V3_SQL), 143 4 => Some(CONTEXT_BOUND_ENVELOPES_V4_SQL), 144 _ => None, 145 } 146 } 147 148 #[cfg(test)] 149 #[cfg_attr(coverage_nightly, coverage(off))] 150 mod tests { 151 use super::{CURRENT_VERSION, MIGRATIONS, MINIMUM_VERSION, migration_sql}; 152 use serde::Deserialize; 153 use sha2::{Digest, Sha256}; 154 use sqlx::{Connection, Row, SqliteConnection}; 155 156 const PLAN_SNAPSHOT: &str = 157 include_str!("../../../../../contracts/storage/private_schema_v1.toml"); 158 159 #[derive(Debug, Deserialize)] 160 struct PlanSnapshot { 161 schema_version: u32, 162 database: String, 163 application_id: u32, 164 minimum_version: u32, 165 current_version: u32, 166 migration_name: String, 167 migration_sha256: String, 168 forward_only: bool, 169 raw_sql_public: bool, 170 encrypted_envelopes: bool, 171 authorities: Vec<String>, 172 forbidden_tables: Vec<String>, 173 migrations: Vec<MigrationSnapshot>, 174 } 175 176 #[derive(Debug, Deserialize)] 177 struct MigrationSnapshot { 178 version: u32, 179 name: String, 180 sha256: String, 181 owned_objects: Vec<String>, 182 } 183 184 #[test] 185 fn migration_plan_matches_governed_snapshot() { 186 let snapshot = toml::from_str::<PlanSnapshot>(PLAN_SNAPSHOT).expect("valid snapshot"); 187 let migration = MIGRATIONS[3]; 188 assert_eq!(snapshot.schema_version, 1); 189 assert_eq!(snapshot.database, "private.sqlite"); 190 assert_eq!(snapshot.application_id, 1_380_208_722); 191 assert_eq!(snapshot.minimum_version, MINIMUM_VERSION); 192 assert_eq!(snapshot.current_version, CURRENT_VERSION); 193 assert_eq!(snapshot.migration_name, migration.name()); 194 assert_eq!(snapshot.migration_sha256, migration.up_sha256()); 195 assert!(snapshot.forward_only); 196 assert!(!snapshot.raw_sql_public); 197 assert!(snapshot.encrypted_envelopes); 198 assert_eq!(snapshot.authorities.len(), 7); 199 assert_eq!(snapshot.forbidden_tables, ["studio", "ui_state"]); 200 assert_eq!(snapshot.migrations.len(), MIGRATIONS.len()); 201 for (expected, actual) in snapshot.migrations.iter().zip(MIGRATIONS) { 202 assert_eq!(expected.version, actual.version()); 203 assert_eq!(expected.name, actual.name()); 204 assert_eq!(expected.sha256, actual.up_sha256()); 205 assert_eq!(expected.owned_objects, actual.owned_objects()); 206 } 207 } 208 209 #[test] 210 fn embedded_migration_checksum_is_pinned() { 211 for migration in MIGRATIONS { 212 let sql = migration_sql(migration.version()).expect("registered SQL"); 213 assert_eq!(format!("{:x}", Sha256::digest(sql)), migration.up_sha256()); 214 } 215 assert_eq!(migration_sql(5), None); 216 } 217 218 #[tokio::test] 219 async fn fresh_database_has_exact_private_schema_and_no_studio_authority() { 220 let mut connection = SqliteConnection::connect("sqlite::memory:") 221 .await 222 .expect("open memory SQLite"); 223 for migration in MIGRATIONS { 224 sqlx::raw_sql(migration_sql(migration.version()).expect("registered SQL")) 225 .execute(&mut connection) 226 .await 227 .expect("apply private schema"); 228 } 229 let rows = sqlx::query( 230 "SELECT name FROM sqlite_schema 231 WHERE name LIKE 'radroots_private_%' 232 ORDER BY name", 233 ) 234 .fetch_all(&mut connection) 235 .await 236 .expect("inspect private schema"); 237 let actual = rows 238 .iter() 239 .map(|row| row.get::<String, _>("name")) 240 .collect::<Vec<_>>(); 241 assert_eq!(actual, MIGRATIONS[3].owned_objects()); 242 let forbidden = sqlx::query_scalar::<_, i64>( 243 "SELECT COUNT(*) FROM sqlite_schema 244 WHERE lower(name) LIKE '%studio%' OR lower(name) LIKE '%ui_state%'", 245 ) 246 .fetch_one(&mut connection) 247 .await 248 .expect("inspect forbidden tables"); 249 assert_eq!(forbidden, 0); 250 assert!( 251 sqlx::query("DELETE FROM radroots_private_artifacts") 252 .execute(&mut connection) 253 .await 254 .is_ok() 255 ); 256 let integrity = sqlx::query_scalar::<_, String>("PRAGMA integrity_check") 257 .fetch_one(&mut connection) 258 .await 259 .expect("inspect integrity"); 260 assert_eq!(integrity, "ok"); 261 } 262 263 #[tokio::test] 264 async fn v4_preflight_rejects_unfingerprinted_v2_without_partial_schema() { 265 let mut connection = SqliteConnection::connect("sqlite::memory:") 266 .await 267 .expect("open memory SQLite"); 268 for migration in &MIGRATIONS[..3] { 269 sqlx::raw_sql(migration_sql(migration.version()).expect("registered SQL")) 270 .execute(&mut connection) 271 .await 272 .expect("apply private schema"); 273 } 274 sqlx::query( 275 "INSERT INTO radroots_private_artifacts ( 276 artifact_id, artifact_kind, schema_id, commitment, protected_size_bytes, 277 secret_provider, secret_reference, key_version, envelope_version, 278 encrypted_envelope, revision, stage, created_at_unix_ms, updated_at_unix_ms 279 ) VALUES (?, 'trade.private_terms', 'trade.private_terms.v1', ?, 1, 280 'memory', 'key', 1, 2, ?, 1, 'active', 1, 1)", 281 ) 282 .bind([1_u8; 16].as_slice()) 283 .bind([2_u8; 32].as_slice()) 284 .bind([3_u8].as_slice()) 285 .execute(&mut connection) 286 .await 287 .expect("unfingerprinted v2 fixture"); 288 289 let mut transaction = connection.begin().await.expect("migration transaction"); 290 assert!( 291 sqlx::raw_sql(migration_sql(4).expect("v4 SQL")) 292 .execute(&mut *transaction) 293 .await 294 .is_err() 295 ); 296 transaction.rollback().await.expect("rollback preflight"); 297 let context_column = sqlx::query_scalar::<_, i64>( 298 "SELECT COUNT(*) FROM pragma_table_info('radroots_private_artifacts') 299 WHERE name = 'context_fingerprint'", 300 ) 301 .fetch_one(&mut connection) 302 .await 303 .expect("inspect columns"); 304 let preflight_table = sqlx::query_scalar::<_, i64>( 305 "SELECT COUNT(*) FROM sqlite_schema 306 WHERE name = 'radroots_private_envelope_v2_preflight'", 307 ) 308 .fetch_one(&mut connection) 309 .await 310 .expect("inspect catalog"); 311 assert_eq!(context_column, 0); 312 assert_eq!(preflight_table, 0); 313 } 314 }