mod.rs (61531B)
1 use crate::SqliteStorage; 2 use crate::backend::map_backend; 3 use radroots_secrets::{ 4 EncryptedEnvelope, 5 context::{EnvelopeContext, EnvelopePurpose, EnvelopeSubject, PayloadSchemaId}, 6 }; 7 use radroots_storage::{ 8 Error, 9 event::BoxFuture, 10 private_artifact::{ 11 ArtifactCommitment, ArtifactKind, ArtifactSchemaId, DeletionReason, DurableSecretReference, 12 EXPIRED_ARTIFACT_QUERY_LIMIT_MAX, PrivateArtifactEnvelopeMigrationStatus, 13 PrivateArtifactId, PrivateArtifactMetadata, PrivateArtifactResealReceipt, 14 PrivateArtifactResealRequest, PrivateArtifactRevision, PrivateArtifactStage, 15 PrivateArtifactStatus, PrivateArtifactStore, RetentionPolicy, 16 }, 17 }; 18 use sha2::{Digest, Sha256}; 19 use sqlx::{Row, Sqlite}; 20 21 #[cfg_attr(coverage_nightly, coverage(off))] 22 impl PrivateArtifactStore for SqliteStorage { 23 fn put_metadata( 24 &self, 25 metadata: PrivateArtifactMetadata, 26 ) -> BoxFuture<'_, Result<PrivateArtifactMetadata, Error>> { 27 Box::pin(async move { 28 self.require_private_writer()?; 29 let mut transaction = self 30 .private_pool() 31 .begin_with("BEGIN IMMEDIATE") 32 .await 33 .map_err(map_backend)?; 34 let stored = put_metadata_transaction(&mut transaction, metadata, None).await?; 35 transaction.commit().await.map_err(map_backend)?; 36 Ok(stored) 37 }) 38 } 39 40 fn metadata( 41 &self, 42 artifact_id: PrivateArtifactId, 43 ) -> BoxFuture<'_, Result<Option<PrivateArtifactMetadata>, Error>> { 44 Box::pin(async move { 45 sqlx::query("SELECT * FROM radroots_private_artifacts WHERE artifact_id = ?") 46 .bind(artifact_id.as_bytes().as_slice()) 47 .fetch_optional(self.private_pool()) 48 .await 49 .map_err(map_backend)? 50 .as_ref() 51 .map(decode_metadata) 52 .transpose() 53 }) 54 } 55 56 fn reseal_metadata( 57 &self, 58 request: PrivateArtifactResealRequest, 59 ) -> BoxFuture<'_, Result<PrivateArtifactResealReceipt, Error>> { 60 Box::pin(async move { 61 self.require_private_writer()?; 62 let mut transaction = self 63 .private_pool() 64 .begin_with("BEGIN IMMEDIATE") 65 .await 66 .map_err(map_backend)?; 67 if let Some(receipt) = load_reseal_receipt(&mut transaction, &request).await? { 68 transaction.commit().await.map_err(map_backend)?; 69 return receipt.replay(&request); 70 } 71 let current = load_metadata(&mut transaction, request.artifact_id()) 72 .await? 73 .ok_or(Error::PrivateArtifactNotFound)?; 74 let next = current.resealed(&request)?; 75 let result = sqlx::query( 76 "UPDATE radroots_private_artifacts SET 77 commitment = ?, protected_size_bytes = ?, secret_provider = ?, 78 secret_reference = ?, key_version = ?, revision = ?, 79 updated_at_unix_ms = ?, last_reseal_id = ?, last_reseal_fingerprint = ? 80 WHERE artifact_id = ? AND revision = ? AND commitment = ? 81 AND encrypted_envelope IS NULL AND envelope_version IS NULL", 82 ) 83 .bind(next.commitment().as_bytes().as_slice()) 84 .bind(i64_from_u64(next.protected_size_bytes())?) 85 .bind(next.secret_reference().provider()) 86 .bind(next.secret_reference().opaque_reference()) 87 .bind(i64::from(next.secret_reference().key_version())) 88 .bind(i64_from_u64(next.revision().get())?) 89 .bind(i64_from_u64(next.updated_at_unix_ms())?) 90 .bind(request.reseal_id().as_bytes().as_slice()) 91 .bind(request.fingerprint().as_slice()) 92 .bind(request.artifact_id().as_bytes().as_slice()) 93 .bind(i64_from_u64(request.expected_revision().get())?) 94 .bind(request.expected_commitment().as_bytes().as_slice()) 95 .execute(&mut *transaction) 96 .await 97 .map_err(map_reseal)?; 98 if result.rows_affected() != 1 { 99 return Err(Error::PrivateArtifactResealConflict); 100 } 101 let receipt = load_reseal_receipt(&mut transaction, &request) 102 .await? 103 .ok_or(Error::PrivateArtifactPersistenceIndeterminate)?; 104 transaction.commit().await.map_err(map_indeterminate)?; 105 Ok(receipt) 106 }) 107 } 108 109 fn mark_expired( 110 &self, 111 artifact_id: PrivateArtifactId, 112 expected_revision: PrivateArtifactRevision, 113 at_unix_ms: u64, 114 ) -> BoxFuture<'_, Result<PrivateArtifactMetadata, Error>> { 115 Box::pin(async move { 116 self.require_private_writer()?; 117 let mut transaction = self 118 .private_pool() 119 .begin_with("BEGIN IMMEDIATE") 120 .await 121 .map_err(map_backend)?; 122 let current = load_metadata(&mut transaction, artifact_id) 123 .await? 124 .ok_or(Error::PrivateArtifactNotFound)?; 125 let next = current.mark_expired(expected_revision, at_unix_ms)?; 126 update_metadata(&mut transaction, &next, current.revision(), false).await?; 127 transaction.commit().await.map_err(map_backend)?; 128 Ok(next) 129 }) 130 } 131 132 fn tombstone( 133 &self, 134 artifact_id: PrivateArtifactId, 135 expected_revision: PrivateArtifactRevision, 136 at_unix_ms: u64, 137 reason: DeletionReason, 138 ) -> BoxFuture<'_, Result<PrivateArtifactMetadata, Error>> { 139 Box::pin(async move { 140 self.require_private_writer()?; 141 let mut transaction = self 142 .private_pool() 143 .begin_with("BEGIN IMMEDIATE") 144 .await 145 .map_err(map_backend)?; 146 let current = load_metadata(&mut transaction, artifact_id) 147 .await? 148 .ok_or(Error::PrivateArtifactNotFound)?; 149 let next = current.tombstone(expected_revision, at_unix_ms, reason)?; 150 update_metadata(&mut transaction, &next, current.revision(), true).await?; 151 transaction.commit().await.map_err(map_backend)?; 152 Ok(next) 153 }) 154 } 155 156 fn expired( 157 &self, 158 at_unix_ms: u64, 159 limit: u16, 160 ) -> BoxFuture<'_, Result<Vec<PrivateArtifactMetadata>, Error>> { 161 Box::pin(async move { 162 if at_unix_ms == 0 || limit == 0 || limit > EXPIRED_ARTIFACT_QUERY_LIMIT_MAX { 163 return Err(Error::InvalidExpiredArtifactQueryLimit); 164 } 165 sqlx::query( 166 "SELECT * FROM radroots_private_artifacts 167 WHERE stage = 'active' AND expires_at_unix_ms <= ? 168 ORDER BY expires_at_unix_ms, artifact_id LIMIT ?", 169 ) 170 .bind(i64_from_u64(at_unix_ms)?) 171 .bind(i64::from(limit)) 172 .fetch_all(self.private_pool()) 173 .await 174 .map_err(map_backend)? 175 .iter() 176 .map(decode_metadata) 177 .collect() 178 }) 179 } 180 181 fn status(&self) -> BoxFuture<'_, Result<PrivateArtifactStatus, Error>> { 182 Box::pin(async move { 183 let row = sqlx::query( 184 "SELECT 185 COALESCE(SUM(CASE WHEN stage = 'active' THEN 1 ELSE 0 END), 0) AS active, 186 COALESCE(SUM(CASE WHEN stage = 'expired' THEN 1 ELSE 0 END), 0) AS expired, 187 COALESCE(SUM(CASE WHEN stage = 'tombstoned' THEN 1 ELSE 0 END), 0) AS tombstoned 188 FROM radroots_private_artifacts", 189 ) 190 .fetch_one(self.private_pool()) 191 .await 192 .map_err(map_backend)?; 193 Ok(PrivateArtifactStatus { 194 active: count(&row, "active")?, 195 expired: count(&row, "expired")?, 196 tombstoned: count(&row, "tombstoned")?, 197 }) 198 }) 199 } 200 } 201 202 impl SqliteStorage { 203 /// Atomically stores validated metadata with its authenticated encrypted envelope. 204 #[cfg_attr(coverage_nightly, coverage(off))] 205 pub async fn put_encrypted_private_artifact( 206 &self, 207 metadata: PrivateArtifactMetadata, 208 envelope: &EncryptedEnvelope, 209 ) -> Result<PrivateArtifactMetadata, Error> { 210 self.require_private_writer()?; 211 let encoded = validate_new_envelope(&metadata, envelope)?; 212 let context_fingerprint = metadata.envelope_context().fingerprint(); 213 let mut transaction = self 214 .private_pool() 215 .begin_with("BEGIN IMMEDIATE") 216 .await 217 .map_err(map_backend)?; 218 let stored = put_metadata_transaction( 219 &mut transaction, 220 metadata, 221 Some(( 222 envelope.version(), 223 encoded.as_slice(), 224 context_fingerprint.as_slice(), 225 )), 226 ) 227 .await?; 228 transaction.commit().await.map_err(map_backend)?; 229 Ok(stored) 230 } 231 232 /// Loads and revalidates an encrypted envelope without opening its plaintext. 233 #[cfg_attr(coverage_nightly, coverage(off))] 234 pub async fn encrypted_private_artifact( 235 &self, 236 artifact_id: PrivateArtifactId, 237 ) -> Result<Option<EncryptedEnvelope>, Error> { 238 let Some(row) = 239 sqlx::query("SELECT * FROM radroots_private_artifacts WHERE artifact_id = ?") 240 .bind(artifact_id.as_bytes().as_slice()) 241 .fetch_optional(self.private_pool()) 242 .await 243 .map_err(map_backend)? 244 else { 245 return Ok(None); 246 }; 247 let metadata = decode_metadata(&row)?; 248 let encoded = row 249 .try_get::<Option<Vec<u8>>, _>("encrypted_envelope") 250 .map_err(map_corrupt)?; 251 let version = row 252 .try_get::<Option<i64>, _>("envelope_version") 253 .map_err(map_corrupt)?; 254 match (encoded, version) { 255 (None, None) => Ok(None), 256 (Some(encoded), Some(version)) => { 257 let envelope = EncryptedEnvelope::decode(encoded.as_slice()) 258 .map_err(|_| Error::CorruptPrivateArtifactMetadata)?; 259 if u64_from_i64(version)? != u64::from(envelope.version()) { 260 return Err(Error::CorruptPrivateArtifactMetadata); 261 } 262 validate_stored_envelope(&metadata, &envelope, &row) 263 .map_err(|_| Error::CorruptPrivateArtifactMetadata)?; 264 Ok(Some(envelope)) 265 } 266 _ => Err(Error::CorruptPrivateArtifactMetadata), 267 } 268 } 269 270 /// Returns an identity-free inventory of private-envelope migration state. 271 #[cfg_attr(coverage_nightly, coverage(off))] 272 pub async fn private_artifact_envelope_migration_status( 273 &self, 274 ) -> Result<PrivateArtifactEnvelopeMigrationStatus, Error> { 275 let row = sqlx::query( 276 "SELECT 277 COALESCE(SUM(CASE WHEN envelope_version = 1 278 AND context_fingerprint IS NULL THEN 1 ELSE 0 END), 0) AS v1_pending, 279 COALESCE(SUM(CASE WHEN envelope_version = 2 280 AND context_fingerprint IS NOT NULL THEN 1 ELSE 0 END), 0) AS v2_current, 281 COALESCE(SUM(CASE WHEN envelope_version IS NOT NULL AND ( 282 envelope_version NOT IN (1, 2) 283 OR (envelope_version = 1 AND context_fingerprint IS NOT NULL) 284 OR (envelope_version = 2 AND context_fingerprint IS NULL) 285 ) THEN 1 ELSE 0 END), 0) AS corrupt 286 FROM radroots_private_artifacts", 287 ) 288 .fetch_one(self.private_pool()) 289 .await 290 .map_err(map_backend)?; 291 Ok(PrivateArtifactEnvelopeMigrationStatus { 292 v1_pending: count(&row, "v1_pending")?, 293 v2_current: count(&row, "v2_current")?, 294 corrupt: count(&row, "corrupt")?, 295 blocked_provider: 0, 296 conflicted: 0, 297 }) 298 } 299 300 /// Atomically replaces one authenticated v1 envelope with an independently 301 /// produced context-bound v2 envelope. 302 #[cfg_attr(coverage_nightly, coverage(off))] 303 pub async fn commit_private_artifact_reseal( 304 &self, 305 request: PrivateArtifactResealRequest, 306 envelope: &EncryptedEnvelope, 307 ) -> Result<PrivateArtifactResealReceipt, Error> { 308 self.require_private_writer()?; 309 let mut transaction = self 310 .private_pool() 311 .begin_with("BEGIN IMMEDIATE") 312 .await 313 .map_err(map_backend)?; 314 if let Some(receipt) = load_reseal_receipt(&mut transaction, &request).await? { 315 transaction.commit().await.map_err(map_backend)?; 316 return receipt.replay(&request); 317 } 318 let current = load_metadata(&mut transaction, request.artifact_id()) 319 .await? 320 .ok_or(Error::PrivateArtifactNotFound)?; 321 let next = current.resealed(&request)?; 322 let encoded = validate_new_envelope(&next, envelope)?; 323 let context_fingerprint = next.envelope_context().fingerprint(); 324 let result = sqlx::query( 325 "UPDATE radroots_private_artifacts SET 326 commitment = ?, protected_size_bytes = ?, secret_provider = ?, 327 secret_reference = ?, key_version = ?, envelope_version = 2, 328 encrypted_envelope = ?, context_fingerprint = ?, revision = ?, 329 updated_at_unix_ms = ?, last_reseal_id = ?, last_reseal_fingerprint = ? 330 WHERE artifact_id = ? AND revision = ? AND commitment = ? 331 AND envelope_version = 1 AND context_fingerprint IS NULL", 332 ) 333 .bind(next.commitment().as_bytes().as_slice()) 334 .bind(i64_from_u64(next.protected_size_bytes())?) 335 .bind(next.secret_reference().provider()) 336 .bind(next.secret_reference().opaque_reference()) 337 .bind(i64::from(next.secret_reference().key_version())) 338 .bind(encoded.as_slice()) 339 .bind(context_fingerprint.as_slice()) 340 .bind(i64_from_u64(next.revision().get())?) 341 .bind(i64_from_u64(next.updated_at_unix_ms())?) 342 .bind(request.reseal_id().as_bytes().as_slice()) 343 .bind(request.fingerprint().as_slice()) 344 .bind(request.artifact_id().as_bytes().as_slice()) 345 .bind(i64_from_u64(request.expected_revision().get())?) 346 .bind(request.expected_commitment().as_bytes().as_slice()) 347 .execute(&mut *transaction) 348 .await 349 .map_err(map_reseal)?; 350 if result.rows_affected() != 1 { 351 return Err(Error::PrivateArtifactResealConflict); 352 } 353 let receipt = load_reseal_receipt(&mut transaction, &request) 354 .await? 355 .ok_or(Error::PrivateArtifactPersistenceIndeterminate)?; 356 transaction.commit().await.map_err(map_indeterminate)?; 357 Ok(receipt) 358 } 359 360 fn require_private_writer(&self) -> Result<(), Error> { 361 if self.event_mode() == radroots_storage::status::EventStoreMode::ReadOnly { 362 return Err(Error::BackendUnavailable); 363 } 364 Ok(()) 365 } 366 } 367 368 #[cfg_attr(coverage_nightly, coverage(off))] 369 async fn put_metadata_transaction( 370 transaction: &mut sqlx::Transaction<'_, Sqlite>, 371 metadata: PrivateArtifactMetadata, 372 envelope: Option<(u16, &[u8], &[u8])>, 373 ) -> Result<PrivateArtifactMetadata, Error> { 374 if metadata.stage() != PrivateArtifactStage::Active 375 || metadata.revision() != PrivateArtifactRevision::INITIAL 376 { 377 return Err(Error::InvalidPrivateArtifactMetadata); 378 } 379 if let Some(row) = sqlx::query("SELECT * FROM radroots_private_artifacts WHERE artifact_id = ?") 380 .bind(metadata.artifact_id().as_bytes().as_slice()) 381 .fetch_optional(&mut **transaction) 382 .await 383 .map_err(map_backend)? 384 { 385 let existing = decode_metadata(&row)?; 386 if existing != metadata { 387 return Err(Error::PrivateArtifactConflict); 388 } 389 let stored_envelope = row 390 .try_get::<Option<Vec<u8>>, _>("encrypted_envelope") 391 .map_err(map_corrupt)?; 392 return match (stored_envelope, envelope) { 393 (None, Some((version, encoded, context_fingerprint))) => { 394 let result = sqlx::query( 395 "UPDATE radroots_private_artifacts 396 SET envelope_version = ?, encrypted_envelope = ?, context_fingerprint = ? 397 WHERE artifact_id = ? AND encrypted_envelope IS NULL", 398 ) 399 .bind(i64::from(version)) 400 .bind(encoded) 401 .bind(context_fingerprint) 402 .bind(metadata.artifact_id().as_bytes().as_slice()) 403 .execute(&mut **transaction) 404 .await 405 .map_err(map_backend)?; 406 if result.rows_affected() != 1 { 407 return Err(Error::PrivateArtifactConflict); 408 } 409 Ok(metadata) 410 } 411 (Some(stored), Some((_, encoded, _))) if stored.as_slice() == encoded => Ok(metadata), 412 (Some(_), Some(_)) => Err(Error::PrivateArtifactConflict), 413 (_, None) => Ok(metadata), 414 }; 415 } 416 insert_metadata(transaction, &metadata, envelope).await?; 417 Ok(metadata) 418 } 419 420 #[cfg_attr(coverage_nightly, coverage(off))] 421 async fn insert_metadata( 422 transaction: &mut sqlx::Transaction<'_, Sqlite>, 423 metadata: &PrivateArtifactMetadata, 424 envelope: Option<(u16, &[u8], &[u8])>, 425 ) -> Result<(), Error> { 426 let tombstone = metadata.tombstone_record(); 427 sqlx::query( 428 "INSERT INTO radroots_private_artifacts ( 429 artifact_id, artifact_kind, schema_id, commitment, protected_size_bytes, 430 secret_provider, secret_reference, key_version, envelope_version, 431 encrypted_envelope, context_fingerprint, delete_not_before_unix_ms, expires_at_unix_ms, 432 revision, stage, created_at_unix_ms, updated_at_unix_ms, 433 deleted_at_unix_ms, deletion_reason, tombstone_commitment 434 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", 435 ) 436 .bind(metadata.artifact_id().as_bytes().as_slice()) 437 .bind(metadata.kind().as_str()) 438 .bind(metadata.schema_id().as_str()) 439 .bind(metadata.commitment().as_bytes().as_slice()) 440 .bind(i64_from_u64(metadata.protected_size_bytes())?) 441 .bind(metadata.secret_reference().provider()) 442 .bind(metadata.secret_reference().opaque_reference()) 443 .bind(i64::from(metadata.secret_reference().key_version())) 444 .bind(envelope.map(|(version, _, _)| i64::from(version))) 445 .bind(envelope.map(|(_, encoded, _)| encoded)) 446 .bind(envelope.map(|(_, _, context_fingerprint)| context_fingerprint)) 447 .bind( 448 metadata 449 .retention() 450 .delete_not_before_unix_ms() 451 .map(i64_from_u64) 452 .transpose()?, 453 ) 454 .bind( 455 metadata 456 .retention() 457 .expires_at_unix_ms() 458 .map(i64_from_u64) 459 .transpose()?, 460 ) 461 .bind(i64_from_u64(metadata.revision().get())?) 462 .bind(stage_name(metadata.stage())) 463 .bind(i64_from_u64(metadata.created_at_unix_ms())?) 464 .bind(i64_from_u64(metadata.updated_at_unix_ms())?) 465 .bind( 466 tombstone 467 .map(|value| i64_from_u64(value.deleted_at_unix_ms())) 468 .transpose()?, 469 ) 470 .bind(tombstone.map(|value| deletion_name(value.reason()))) 471 .bind(tombstone.map(|value| value.commitment().as_bytes().to_vec())) 472 .execute(&mut **transaction) 473 .await 474 .map_err(map_backend)?; 475 Ok(()) 476 } 477 478 #[cfg_attr(coverage_nightly, coverage(off))] 479 async fn load_metadata( 480 transaction: &mut sqlx::Transaction<'_, Sqlite>, 481 artifact_id: PrivateArtifactId, 482 ) -> Result<Option<PrivateArtifactMetadata>, Error> { 483 sqlx::query("SELECT * FROM radroots_private_artifacts WHERE artifact_id = ?") 484 .bind(artifact_id.as_bytes().as_slice()) 485 .fetch_optional(&mut **transaction) 486 .await 487 .map_err(map_backend)? 488 .as_ref() 489 .map(decode_metadata) 490 .transpose() 491 } 492 493 #[cfg_attr(coverage_nightly, coverage(off))] 494 async fn update_metadata( 495 transaction: &mut sqlx::Transaction<'_, Sqlite>, 496 metadata: &PrivateArtifactMetadata, 497 prior_revision: PrivateArtifactRevision, 498 remove_envelope: bool, 499 ) -> Result<(), Error> { 500 let tombstone = metadata.tombstone_record(); 501 let mut query = if remove_envelope { 502 sqlx::query( 503 "UPDATE radroots_private_artifacts SET 504 revision = ?, stage = ?, updated_at_unix_ms = ?, 505 deleted_at_unix_ms = ?, deletion_reason = ?, tombstone_commitment = ?, 506 envelope_version = NULL, encrypted_envelope = NULL 507 WHERE artifact_id = ? AND revision = ?", 508 ) 509 } else { 510 sqlx::query( 511 "UPDATE radroots_private_artifacts SET 512 revision = ?, stage = ?, updated_at_unix_ms = ?, 513 deleted_at_unix_ms = ?, deletion_reason = ?, tombstone_commitment = ? 514 WHERE artifact_id = ? AND revision = ?", 515 ) 516 }; 517 query = query 518 .bind(i64_from_u64(metadata.revision().get())?) 519 .bind(stage_name(metadata.stage())) 520 .bind(i64_from_u64(metadata.updated_at_unix_ms())?) 521 .bind( 522 tombstone 523 .map(|value| i64_from_u64(value.deleted_at_unix_ms())) 524 .transpose()?, 525 ) 526 .bind(tombstone.map(|value| deletion_name(value.reason()))) 527 .bind(tombstone.map(|value| value.commitment().as_bytes().to_vec())) 528 .bind(metadata.artifact_id().as_bytes().as_slice()) 529 .bind(i64_from_u64(prior_revision.get())?); 530 let result = query 531 .execute(&mut **transaction) 532 .await 533 .map_err(map_backend)?; 534 if result.rows_affected() != 1 { 535 return Err(Error::PrivateArtifactRevisionConflict); 536 } 537 Ok(()) 538 } 539 540 fn decode_metadata(row: &sqlx::sqlite::SqliteRow) -> Result<PrivateArtifactMetadata, Error> { 541 let artifact_id = PrivateArtifactId::new(array( 542 row.try_get::<Vec<u8>, _>("artifact_id") 543 .map_err(map_corrupt)?, 544 )?) 545 .map_err(|_| Error::CorruptPrivateArtifactMetadata)?; 546 let kind = ArtifactKind::parse( 547 row.try_get::<String, _>("artifact_kind") 548 .map_err(map_corrupt)?, 549 ) 550 .map_err(|_| Error::CorruptPrivateArtifactMetadata)?; 551 let schema_id = 552 ArtifactSchemaId::parse(row.try_get::<String, _>("schema_id").map_err(map_corrupt)?) 553 .map_err(|_| Error::CorruptPrivateArtifactMetadata)?; 554 let commitment = ArtifactCommitment::new(array( 555 row.try_get::<Vec<u8>, _>("commitment") 556 .map_err(map_corrupt)?, 557 )?); 558 let secret_reference = DurableSecretReference::new( 559 row.try_get::<String, _>("secret_provider") 560 .map_err(map_corrupt)?, 561 row.try_get::<String, _>("secret_reference") 562 .map_err(map_corrupt)?, 563 u32::try_from(row.try_get::<i64, _>("key_version").map_err(map_corrupt)?) 564 .map_err(|_| Error::CorruptPrivateArtifactMetadata)?, 565 ) 566 .map_err(|_| Error::CorruptPrivateArtifactMetadata)?; 567 let retention = RetentionPolicy::new( 568 optional_u64(row, "delete_not_before_unix_ms")?, 569 optional_u64(row, "expires_at_unix_ms")?, 570 ) 571 .map_err(|_| Error::CorruptPrivateArtifactMetadata)?; 572 let stage = stage( 573 row.try_get::<String, _>("stage") 574 .map_err(map_corrupt)? 575 .as_str(), 576 )?; 577 let deleted_at = optional_u64(row, "deleted_at_unix_ms")?; 578 let deletion_reason = row 579 .try_get::<Option<String>, _>("deletion_reason") 580 .map_err(map_corrupt)? 581 .map(|value| deletion(value.as_str())) 582 .transpose()?; 583 let tombstone_commitment = row 584 .try_get::<Option<Vec<u8>>, _>("tombstone_commitment") 585 .map_err(map_corrupt)? 586 .map(|value| array(value).map(ArtifactCommitment::new)) 587 .transpose()?; 588 let tombstone = match (deleted_at, deletion_reason, tombstone_commitment) { 589 (None, None, None) => None, 590 (Some(at), Some(reason), Some(commitment)) => Some((at, reason, commitment)), 591 _ => return Err(Error::CorruptPrivateArtifactMetadata), 592 }; 593 PrivateArtifactMetadata::from_durable_parts( 594 artifact_id, 595 kind, 596 schema_id, 597 commitment, 598 u64_from_i64(row.try_get("protected_size_bytes").map_err(map_corrupt)?)?, 599 secret_reference, 600 retention, 601 PrivateArtifactRevision::new(u64_from_i64(row.try_get("revision").map_err(map_corrupt)?)?) 602 .map_err(|_| Error::CorruptPrivateArtifactMetadata)?, 603 stage, 604 u64_from_i64(row.try_get("created_at_unix_ms").map_err(map_corrupt)?)?, 605 u64_from_i64(row.try_get("updated_at_unix_ms").map_err(map_corrupt)?)?, 606 tombstone, 607 ) 608 .map_err(|_| Error::CorruptPrivateArtifactMetadata) 609 } 610 611 fn validate_new_envelope( 612 metadata: &PrivateArtifactMetadata, 613 envelope: &EncryptedEnvelope, 614 ) -> Result<Vec<u8>, Error> { 615 let expected_context = secrets_context(metadata)?; 616 if envelope.version() != 2 617 || envelope.context() != Some(&expected_context) 618 || metadata.stage() != PrivateArtifactStage::Active 619 || metadata.secret_reference().opaque_reference() != envelope.reference().id().as_str() 620 || metadata.secret_reference().key_version() != envelope.reference().key_version().get() 621 { 622 return Err(Error::InvalidPrivateArtifactMetadata); 623 } 624 let encoded = envelope 625 .encode() 626 .map_err(|_| Error::InvalidPrivateArtifactMetadata)?; 627 if metadata.protected_size_bytes() 628 != u64::try_from(encoded.len()).map_err(|_| Error::InvalidPrivateArtifactMetadata)? 629 || metadata.commitment().as_bytes() != Sha256::digest(encoded.as_slice()).as_slice() 630 { 631 return Err(Error::InvalidPrivateArtifactMetadata); 632 } 633 Ok(encoded) 634 } 635 636 fn validate_stored_envelope( 637 metadata: &PrivateArtifactMetadata, 638 envelope: &EncryptedEnvelope, 639 row: &sqlx::sqlite::SqliteRow, 640 ) -> Result<(), Error> { 641 let encoded = envelope 642 .encode() 643 .map_err(|_| Error::CorruptPrivateArtifactMetadata)?; 644 if metadata.secret_reference().opaque_reference() != envelope.reference().id().as_str() 645 || metadata.secret_reference().key_version() != envelope.reference().key_version().get() 646 || metadata.protected_size_bytes() 647 != u64::try_from(encoded.len()).map_err(|_| Error::CorruptPrivateArtifactMetadata)? 648 || metadata.commitment().as_bytes() != Sha256::digest(encoded.as_slice()).as_slice() 649 { 650 return Err(Error::CorruptPrivateArtifactMetadata); 651 } 652 let stored_fingerprint = row 653 .try_get::<Option<Vec<u8>>, _>("context_fingerprint") 654 .map_err(map_corrupt)?; 655 match envelope.version() { 656 1 if envelope.context().is_none() && stored_fingerprint.is_none() => Ok(()), 657 2 => { 658 let expected = secrets_context(metadata)?; 659 let expected_fingerprint = metadata.envelope_context().fingerprint(); 660 if envelope.context() == Some(&expected) 661 && stored_fingerprint.as_deref() == Some(expected_fingerprint.as_slice()) 662 { 663 Ok(()) 664 } else { 665 Err(Error::CorruptPrivateArtifactMetadata) 666 } 667 } 668 _ => Err(Error::CorruptPrivateArtifactMetadata), 669 } 670 } 671 672 fn secrets_context(metadata: &PrivateArtifactMetadata) -> Result<EnvelopeContext, Error> { 673 let derived = metadata.envelope_context(); 674 Ok(EnvelopeContext::new( 675 EnvelopePurpose::parse(derived.purpose()) 676 .map_err(|_| Error::CorruptPrivateArtifactMetadata)?, 677 EnvelopeSubject::parse(derived.subject_type(), derived.subject()) 678 .map_err(|_| Error::CorruptPrivateArtifactMetadata)?, 679 PayloadSchemaId::parse(derived.payload_schema()) 680 .map_err(|_| Error::CorruptPrivateArtifactMetadata)?, 681 )) 682 } 683 684 #[cfg_attr(coverage_nightly, coverage(off))] 685 async fn load_reseal_receipt( 686 transaction: &mut sqlx::Transaction<'_, Sqlite>, 687 request: &PrivateArtifactResealRequest, 688 ) -> Result<Option<PrivateArtifactResealReceipt>, Error> { 689 let Some(row) = sqlx::query( 690 "SELECT artifact_id, request_fingerprint, committed_revision 691 FROM radroots_private_envelope_reseals WHERE reseal_id = ?", 692 ) 693 .bind(request.reseal_id().as_bytes().as_slice()) 694 .fetch_optional(&mut **transaction) 695 .await 696 .map_err(map_backend)? 697 else { 698 return Ok(None); 699 }; 700 let artifact_id = row 701 .try_get::<Vec<u8>, _>("artifact_id") 702 .map_err(map_corrupt)?; 703 let fingerprint = row 704 .try_get::<Vec<u8>, _>("request_fingerprint") 705 .map_err(map_corrupt)?; 706 if artifact_id.as_slice() != request.artifact_id().as_bytes() 707 || fingerprint.as_slice() != request.fingerprint() 708 { 709 return Err(Error::PrivateArtifactResealConflict); 710 } 711 let revision = PrivateArtifactRevision::new(u64_from_i64( 712 row.try_get::<i64, _>("committed_revision") 713 .map_err(map_corrupt)?, 714 )?) 715 .map_err(|_| Error::CorruptPrivateArtifactMetadata)?; 716 Ok(Some(PrivateArtifactResealReceipt::committed( 717 request, revision, 718 ))) 719 } 720 721 fn optional_u64(row: &sqlx::sqlite::SqliteRow, column: &str) -> Result<Option<u64>, Error> { 722 row.try_get::<Option<i64>, _>(column) 723 .map_err(map_corrupt)? 724 .map(u64_from_i64) 725 .transpose() 726 } 727 728 const fn stage_name(stage: PrivateArtifactStage) -> &'static str { 729 match stage { 730 PrivateArtifactStage::Active => "active", 731 PrivateArtifactStage::Expired => "expired", 732 PrivateArtifactStage::Tombstoned => "tombstoned", 733 } 734 } 735 736 fn stage(value: &str) -> Result<PrivateArtifactStage, Error> { 737 match value.as_bytes() { 738 b"active" => Ok(PrivateArtifactStage::Active), 739 b"expired" => Ok(PrivateArtifactStage::Expired), 740 b"tombstoned" => Ok(PrivateArtifactStage::Tombstoned), 741 _ => Err(Error::CorruptPrivateArtifactMetadata), 742 } 743 } 744 745 const fn deletion_name(reason: DeletionReason) -> &'static str { 746 match reason { 747 DeletionReason::UserRequested => "user_requested", 748 DeletionReason::RetentionExpired => "retention_expired", 749 DeletionReason::KeyRevoked => "key_revoked", 750 DeletionReason::IntegrityFailure => "integrity_failure", 751 DeletionReason::OperatorRequested => "operator_requested", 752 } 753 } 754 755 fn deletion(value: &str) -> Result<DeletionReason, Error> { 756 match value.as_bytes() { 757 b"user_requested" => Ok(DeletionReason::UserRequested), 758 b"retention_expired" => Ok(DeletionReason::RetentionExpired), 759 b"key_revoked" => Ok(DeletionReason::KeyRevoked), 760 b"integrity_failure" => Ok(DeletionReason::IntegrityFailure), 761 b"operator_requested" => Ok(DeletionReason::OperatorRequested), 762 _ => Err(Error::CorruptPrivateArtifactMetadata), 763 } 764 } 765 766 fn count(row: &sqlx::sqlite::SqliteRow, column: &str) -> Result<u64, Error> { 767 u64_from_i64(row.try_get::<i64, _>(column).map_err(map_corrupt)?) 768 } 769 770 fn array<const N: usize>(bytes: Vec<u8>) -> Result<[u8; N], Error> { 771 bytes 772 .try_into() 773 .map_err(|_| Error::CorruptPrivateArtifactMetadata) 774 } 775 776 fn i64_from_u64(value: u64) -> Result<i64, Error> { 777 i64::try_from(value).map_err(|_| Error::CorruptPrivateArtifactMetadata) 778 } 779 780 fn u64_from_i64(value: i64) -> Result<u64, Error> { 781 u64::try_from(value).map_err(|_| Error::CorruptPrivateArtifactMetadata) 782 } 783 784 fn map_reseal(_: sqlx::Error) -> Error { 785 Error::PrivateArtifactResealConflict 786 } 787 788 fn map_indeterminate(_: sqlx::Error) -> Error { 789 Error::PrivateArtifactPersistenceIndeterminate 790 } 791 792 fn map_corrupt(_: sqlx::Error) -> Error { 793 Error::CorruptPrivateArtifactMetadata 794 } 795 796 #[cfg(test)] 797 #[cfg_attr(coverage_nightly, coverage(off))] 798 mod tests { 799 use super::*; 800 use crate::migration::{ 801 private::{MIGRATIONS as PRIVATE_MIGRATIONS, migration_sql as private_migration_sql}, 802 runtime::{MIGRATIONS as RUNTIME_MIGRATIONS, migration_sql as runtime_migration_sql}, 803 }; 804 use radroots_secrets::{ 805 Error as SecretError, KeyWrapping, SecretId, SecretRef, 806 envelope::{LegacyV1ResealAuthority, Nonce, SealMaterial, SealRequest}, 807 error::Operation, 808 id::{BackendKind, KeyVersion}, 809 wrapping::{ 810 BoxFuture as SecretFuture, LegacyV1UnwrapRequest, SecretMaterial, UnwrapRequest, 811 WrapRequest, WrappedSecret, 812 }, 813 }; 814 use radroots_storage::private_artifact::{ 815 PrivateArtifactResealDisposition, PrivateArtifactResealId, 816 }; 817 use radroots_storage::status::EventStoreMode; 818 use sqlx::sqlite::SqlitePoolOptions; 819 820 struct VectorWrapping; 821 822 impl KeyWrapping for VectorWrapping { 823 fn wrap<'a>( 824 &'a self, 825 request: WrapRequest<'a>, 826 ) -> SecretFuture<'a, Result<WrappedSecret, SecretError>> { 827 Box::pin(async move { 828 if !matches!( 829 request.reference().id().as_str(), 830 "private-artifact-key" | "envelope-key" 831 ) { 832 return Err(SecretError::BackendFailure { 833 backend: BackendKind::Memory, 834 operation: Operation::Wrap, 835 }); 836 } 837 WrappedSecret::from_bytes(request.plaintext().expose_secret(|bytes| { 838 bytes.iter().map(|byte| byte ^ 0xA5).collect::<Vec<_>>() 839 })) 840 }) 841 } 842 843 fn unwrap<'a>( 844 &'a self, 845 request: UnwrapRequest<'a>, 846 ) -> SecretFuture<'a, Result<SecretMaterial, SecretError>> { 847 Box::pin(async move { 848 if !matches!( 849 request.reference().id().as_str(), 850 "private-artifact-key" | "envelope-key" 851 ) { 852 return Err(SecretError::BackendFailure { 853 backend: BackendKind::Memory, 854 operation: Operation::Unwrap, 855 }); 856 } 857 let plaintext = if request.wrapped().as_bytes() == [0x4b; 32] { 858 vec![0x11; 32] 859 } else { 860 request 861 .wrapped() 862 .as_bytes() 863 .iter() 864 .map(|byte| byte ^ 0xA5) 865 .collect::<Vec<_>>() 866 }; 867 SecretMaterial::from_slice(plaintext.as_slice()) 868 }) 869 } 870 871 fn unwrap_legacy_v1<'a>( 872 &'a self, 873 request: LegacyV1UnwrapRequest<'a>, 874 ) -> SecretFuture<'a, Result<SecretMaterial, SecretError>> { 875 Box::pin(async move { 876 if request.reference().id().as_str() != "envelope-key" 877 || request.wrapped().as_bytes() != [0x4b; 32] 878 { 879 return Err(SecretError::BackendFailure { 880 backend: BackendKind::Memory, 881 operation: Operation::Unwrap, 882 }); 883 } 884 SecretMaterial::from_slice(&[0x11; 32]) 885 }) 886 } 887 } 888 889 async fn store(mode: EventStoreMode) -> SqliteStorage { 890 let runtime_pool = SqlitePoolOptions::new() 891 .max_connections(1) 892 .connect("sqlite::memory:") 893 .await 894 .expect("runtime SQLite"); 895 for migration in RUNTIME_MIGRATIONS { 896 sqlx::raw_sql(runtime_migration_sql(migration.version()).expect("runtime SQL")) 897 .execute(&runtime_pool) 898 .await 899 .expect("runtime migration"); 900 } 901 let private_pool = SqlitePoolOptions::new() 902 .max_connections(1) 903 .connect("sqlite::memory:") 904 .await 905 .expect("private SQLite"); 906 for migration in PRIVATE_MIGRATIONS { 907 sqlx::raw_sql(private_migration_sql(migration.version()).expect("private SQL")) 908 .execute(&private_pool) 909 .await 910 .expect("private migration"); 911 } 912 SqliteStorage::with_private_pool( 913 runtime_pool, 914 private_pool, 915 radroots_storage::event::SourceGeneration::new([91; 32]).expect("generation"), 916 mode, 917 ) 918 } 919 920 fn reference(version: u32) -> SecretRef { 921 SecretRef::new( 922 SecretId::parse("private-artifact-key").expect("secret id"), 923 BackendKind::Memory, 924 KeyVersion::new(version).expect("key version"), 925 ) 926 } 927 928 fn test_context(id: u8, kind: &str) -> EnvelopeContext { 929 let subject = [id; 16] 930 .iter() 931 .map(|byte| format!("{byte:02x}")) 932 .collect::<String>(); 933 EnvelopeContext::new( 934 EnvelopePurpose::parse(format!("radroots.private_artifact.{kind}")).expect("purpose"), 935 EnvelopeSubject::parse("private_artifact", subject).expect("subject"), 936 PayloadSchemaId::parse(format!("{kind}.v1")).expect("schema"), 937 ) 938 } 939 940 async fn sealed_envelope( 941 plaintext: &[u8], 942 version: u32, 943 id: u8, 944 kind: &str, 945 ) -> EncryptedEnvelope { 946 let plaintext = SecretMaterial::from_slice(plaintext).expect("plaintext"); 947 let data_key = SecretMaterial::from_slice(&[0x31; 32]).expect("data key"); 948 EncryptedEnvelope::seal( 949 &VectorWrapping, 950 SealRequest::new( 951 reference(version), 952 test_context(id, kind), 953 &plaintext, 954 SealMaterial::new(data_key, Nonce::new([0x42; 24])), 955 ), 956 ) 957 .await 958 .expect("seal envelope") 959 } 960 961 fn metadata( 962 id: u8, 963 kind: &str, 964 envelope: &EncryptedEnvelope, 965 retention: RetentionPolicy, 966 ) -> PrivateArtifactMetadata { 967 let encoded = envelope.encode().expect("encoded envelope"); 968 PrivateArtifactMetadata::new( 969 PrivateArtifactId::new([id; 16]).expect("artifact id"), 970 ArtifactKind::parse(kind).expect("artifact kind"), 971 ArtifactSchemaId::parse(format!("{kind}.v1")).expect("schema id"), 972 ArtifactCommitment::new(Sha256::digest(encoded.as_slice()).into()), 973 u64::try_from(encoded.len()).expect("encoded length"), 974 DurableSecretReference::new( 975 "memory", 976 envelope.reference().id().as_str(), 977 envelope.reference().key_version().get(), 978 ) 979 .expect("secret reference"), 980 retention, 981 100, 982 ) 983 .expect("metadata") 984 } 985 986 async fn migrated_legacy_store() -> (SqliteStorage, PrivateArtifactMetadata, EncryptedEnvelope) 987 { 988 const V1_ENVELOPE_HEX: &str = "52525331000101010100000007000c656e76656c6f70652d6b6579222222222222222222222222222222222222222222222222000000204b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b00000028f106837e33d690e7c5287abdd815ce9257b7b5b176ea9596abf3b7fe745aec5a8c2487a553d4659d"; 989 let runtime_pool = SqlitePoolOptions::new() 990 .max_connections(1) 991 .connect("sqlite::memory:") 992 .await 993 .expect("runtime SQLite"); 994 for migration in RUNTIME_MIGRATIONS { 995 sqlx::raw_sql(runtime_migration_sql(migration.version()).expect("runtime SQL")) 996 .execute(&runtime_pool) 997 .await 998 .expect("runtime migration"); 999 } 1000 let private_pool = SqlitePoolOptions::new() 1001 .max_connections(1) 1002 .connect("sqlite::memory:") 1003 .await 1004 .expect("private SQLite"); 1005 for migration in &PRIVATE_MIGRATIONS[..3] { 1006 sqlx::raw_sql(private_migration_sql(migration.version()).expect("private SQL")) 1007 .execute(&private_pool) 1008 .await 1009 .expect("private migration"); 1010 } 1011 let encoded = hex::decode(V1_ENVELOPE_HEX).expect("legacy vector"); 1012 let envelope = EncryptedEnvelope::decode(encoded.as_slice()).expect("legacy envelope"); 1013 let metadata = metadata( 1014 1, 1015 "trade.private_terms", 1016 &envelope, 1017 RetentionPolicy::indefinite(), 1018 ); 1019 sqlx::query( 1020 "INSERT INTO radroots_private_artifacts ( 1021 artifact_id, artifact_kind, schema_id, commitment, protected_size_bytes, 1022 secret_provider, secret_reference, key_version, envelope_version, 1023 encrypted_envelope, revision, stage, created_at_unix_ms, updated_at_unix_ms 1024 ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, ?, 1, 'active', 100, 100)", 1025 ) 1026 .bind(metadata.artifact_id().as_bytes().as_slice()) 1027 .bind(metadata.kind().as_str()) 1028 .bind(metadata.schema_id().as_str()) 1029 .bind(metadata.commitment().as_bytes().as_slice()) 1030 .bind(i64_from_u64(metadata.protected_size_bytes()).unwrap()) 1031 .bind(metadata.secret_reference().provider()) 1032 .bind(metadata.secret_reference().opaque_reference()) 1033 .bind(i64::from(metadata.secret_reference().key_version())) 1034 .bind(encoded) 1035 .execute(&private_pool) 1036 .await 1037 .expect("legacy row"); 1038 sqlx::raw_sql(private_migration_sql(4).expect("v4 SQL")) 1039 .execute(&private_pool) 1040 .await 1041 .expect("v4 migration"); 1042 ( 1043 SqliteStorage::with_private_pool( 1044 runtime_pool, 1045 private_pool, 1046 radroots_storage::event::SourceGeneration::new([91; 32]).expect("generation"), 1047 EventStoreMode::ReadWrite, 1048 ), 1049 metadata, 1050 envelope, 1051 ) 1052 } 1053 1054 #[tokio::test] 1055 async fn encrypted_envelopes_round_trip_with_exact_commitment_and_key_version() { 1056 let store = store(EventStoreMode::ReadWrite).await; 1057 let envelope = sealed_envelope(b"private farm coordinates", 7, 1, "farm.location").await; 1058 let metadata = metadata(1, "farm.location", &envelope, RetentionPolicy::indefinite()); 1059 let stored = store 1060 .put_encrypted_private_artifact(metadata.clone(), &envelope) 1061 .await 1062 .expect("store encrypted artifact"); 1063 assert_eq!(stored, metadata); 1064 store 1065 .put_encrypted_private_artifact(metadata.clone(), &envelope) 1066 .await 1067 .expect("exact replay"); 1068 let loaded = store 1069 .encrypted_private_artifact(metadata.artifact_id()) 1070 .await 1071 .expect("load envelope") 1072 .expect("encrypted envelope"); 1073 assert_eq!( 1074 loaded.encode().expect("loaded bytes"), 1075 envelope.encode().expect("expected bytes") 1076 ); 1077 let opened = loaded 1078 .open(&VectorWrapping, &test_context(1, "farm.location")) 1079 .await 1080 .expect("open envelope"); 1081 opened.expose_secret(|bytes| assert_eq!(bytes, b"private farm coordinates")); 1082 1083 let row = sqlx::query( 1084 "SELECT key_version, envelope_version, encrypted_envelope, context_fingerprint 1085 FROM radroots_private_artifacts WHERE artifact_id = ?", 1086 ) 1087 .bind(metadata.artifact_id().as_bytes().as_slice()) 1088 .fetch_one(store.private_pool()) 1089 .await 1090 .expect("private row"); 1091 assert_eq!(row.get::<i64, _>("key_version"), 7); 1092 assert_eq!(row.get::<i64, _>("envelope_version"), 2); 1093 let encrypted = row.get::<Vec<u8>, _>("encrypted_envelope"); 1094 assert!( 1095 !encrypted 1096 .windows(24) 1097 .any(|bytes| bytes == b"private farm coordinates") 1098 ); 1099 assert!(validate_stored_envelope(&metadata, &envelope, &row).is_ok()); 1100 1101 let wrong_key_envelope = 1102 sealed_envelope(b"private farm coordinates", 8, 1, "farm.location").await; 1103 assert_eq!( 1104 validate_stored_envelope(&metadata, &wrong_key_envelope, &row), 1105 Err(Error::CorruptPrivateArtifactMetadata) 1106 ); 1107 assert_eq!( 1108 store 1109 .put_encrypted_private_artifact(metadata.clone(), &wrong_key_envelope) 1110 .await, 1111 Err(Error::InvalidPrivateArtifactMetadata) 1112 ); 1113 1114 let stored_validation_metadata = |commitment, protected_size, secret_reference| { 1115 PrivateArtifactMetadata::new( 1116 metadata.artifact_id(), 1117 metadata.kind().clone(), 1118 metadata.schema_id().clone(), 1119 commitment, 1120 protected_size, 1121 secret_reference, 1122 metadata.retention(), 1123 metadata.created_at_unix_ms(), 1124 ) 1125 .expect("structurally valid stored metadata") 1126 }; 1127 for invalid in [ 1128 stored_validation_metadata( 1129 metadata.commitment(), 1130 metadata.protected_size_bytes(), 1131 DurableSecretReference::new( 1132 "memory", 1133 "different-private-artifact-key", 1134 metadata.secret_reference().key_version(), 1135 ) 1136 .expect("different reference"), 1137 ), 1138 stored_validation_metadata( 1139 metadata.commitment(), 1140 metadata.protected_size_bytes() + 1, 1141 metadata.secret_reference().clone(), 1142 ), 1143 stored_validation_metadata( 1144 ArtifactCommitment::new([0; 32]), 1145 metadata.protected_size_bytes(), 1146 metadata.secret_reference().clone(), 1147 ), 1148 ] { 1149 assert_eq!( 1150 validate_stored_envelope(&invalid, &envelope, &row), 1151 Err(Error::CorruptPrivateArtifactMetadata) 1152 ); 1153 } 1154 1155 let wrong_context_envelope = 1156 sealed_envelope(b"private farm coordinates", 7, 2, "farm.location").await; 1157 let wrong_context_encoded = wrong_context_envelope 1158 .encode() 1159 .expect("wrong context bytes"); 1160 let wrong_context_metadata = stored_validation_metadata( 1161 ArtifactCommitment::new(Sha256::digest(wrong_context_encoded.as_slice()).into()), 1162 u64::try_from(wrong_context_encoded.len()).expect("wrong context length"), 1163 metadata.secret_reference().clone(), 1164 ); 1165 assert_eq!( 1166 validate_stored_envelope(&wrong_context_metadata, &wrong_context_envelope, &row), 1167 Err(Error::CorruptPrivateArtifactMetadata) 1168 ); 1169 let wrong_fingerprint_row = sqlx::query("SELECT X'00' AS context_fingerprint") 1170 .fetch_one(store.private_pool()) 1171 .await 1172 .expect("wrong fingerprint row"); 1173 assert_eq!( 1174 validate_stored_envelope(&metadata, &envelope, &wrong_fingerprint_row), 1175 Err(Error::CorruptPrivateArtifactMetadata) 1176 ); 1177 1178 let (_, legacy_metadata, legacy_envelope) = migrated_legacy_store().await; 1179 assert_eq!( 1180 validate_new_envelope(&legacy_metadata, &legacy_envelope), 1181 Err(Error::InvalidPrivateArtifactMetadata) 1182 ); 1183 1184 let envelope = sealed_envelope(b"validation matrix", 9, 9, "test.validation_matrix").await; 1185 let valid = self::metadata( 1186 9, 1187 "test.validation_matrix", 1188 &envelope, 1189 RetentionPolicy::new(Some(100), Some(100)).expect("retention"), 1190 ); 1191 assert!(validate_new_envelope(&valid, &envelope).is_ok()); 1192 let expired = valid 1193 .mark_expired(valid.revision(), 100) 1194 .expect("expired metadata"); 1195 assert_eq!( 1196 validate_new_envelope(&expired, &envelope), 1197 Err(Error::InvalidPrivateArtifactMetadata) 1198 ); 1199 for (commitment, protected_size, secret_reference) in [ 1200 ( 1201 ArtifactCommitment::new([0; 32]), 1202 valid.protected_size_bytes(), 1203 valid.secret_reference().clone(), 1204 ), 1205 ( 1206 valid.commitment(), 1207 valid.protected_size_bytes() + 1, 1208 valid.secret_reference().clone(), 1209 ), 1210 ( 1211 valid.commitment(), 1212 valid.protected_size_bytes(), 1213 DurableSecretReference::new( 1214 "memory", 1215 "different-private-artifact-key", 1216 valid.secret_reference().key_version(), 1217 ) 1218 .expect("different reference"), 1219 ), 1220 ] { 1221 let invalid = PrivateArtifactMetadata::new( 1222 valid.artifact_id(), 1223 valid.kind().clone(), 1224 valid.schema_id().clone(), 1225 commitment, 1226 protected_size, 1227 secret_reference, 1228 valid.retention(), 1229 valid.created_at_unix_ms(), 1230 ) 1231 .expect("structurally valid metadata"); 1232 assert_eq!( 1233 validate_new_envelope(&invalid, &envelope), 1234 Err(Error::InvalidPrivateArtifactMetadata) 1235 ); 1236 } 1237 } 1238 1239 #[tokio::test] 1240 async fn expiry_and_tombstone_delete_envelope_but_preserve_commitment() { 1241 let store = store(EventStoreMode::ReadWrite).await; 1242 let envelope = sealed_envelope(b"private trade artifact", 3, 2, "trade.artifact").await; 1243 let metadata = metadata( 1244 2, 1245 "trade.artifact", 1246 &envelope, 1247 RetentionPolicy::new(Some(400), Some(300)).expect("retention"), 1248 ); 1249 store 1250 .put_encrypted_private_artifact(metadata.clone(), &envelope) 1251 .await 1252 .expect("store artifact"); 1253 assert!( 1254 store 1255 .expired(299, 10) 1256 .await 1257 .expect("not expired") 1258 .is_empty() 1259 ); 1260 assert_eq!( 1261 store.expired(300, 10).await.expect("expired query"), 1262 vec![metadata.clone()] 1263 ); 1264 let expired = store 1265 .mark_expired(metadata.artifact_id(), metadata.revision(), 300) 1266 .await 1267 .expect("mark expired"); 1268 assert_eq!(expired.stage(), PrivateArtifactStage::Expired); 1269 assert_eq!( 1270 store 1271 .tombstone( 1272 expired.artifact_id(), 1273 expired.revision(), 1274 399, 1275 DeletionReason::RetentionExpired, 1276 ) 1277 .await, 1278 Err(Error::PrivateArtifactRetentionActive) 1279 ); 1280 let tombstoned = store 1281 .tombstone( 1282 expired.artifact_id(), 1283 expired.revision(), 1284 400, 1285 DeletionReason::RetentionExpired, 1286 ) 1287 .await 1288 .expect("tombstone"); 1289 assert_eq!(tombstoned.stage(), PrivateArtifactStage::Tombstoned); 1290 assert_eq!(tombstoned.commitment(), metadata.commitment()); 1291 assert!( 1292 store 1293 .encrypted_private_artifact(metadata.artifact_id()) 1294 .await 1295 .expect("deleted envelope") 1296 .is_none() 1297 ); 1298 assert_eq!( 1299 store.status().await.expect("status"), 1300 PrivateArtifactStatus { 1301 active: 0, 1302 expired: 0, 1303 tombstoned: 1, 1304 } 1305 ); 1306 assert!( 1307 sqlx::query("DELETE FROM radroots_private_artifacts WHERE artifact_id = ?") 1308 .bind(metadata.artifact_id().as_bytes().as_slice()) 1309 .execute(store.private_pool()) 1310 .await 1311 .is_err() 1312 ); 1313 } 1314 1315 #[tokio::test] 1316 async fn all_private_authorities_are_metadata_only_without_an_envelope() { 1317 let store = store(EventStoreMode::ReadWrite).await; 1318 for (id, kind) in [ 1319 (10, "signing.reference"), 1320 (11, "farm.location"), 1321 (12, "trade.artifact"), 1322 (13, "nip46.session"), 1323 ] { 1324 let envelope = sealed_envelope(kind.as_bytes(), 1, id, kind).await; 1325 let metadata = metadata(id, kind, &envelope, RetentionPolicy::indefinite()); 1326 store 1327 .put_metadata(metadata.clone()) 1328 .await 1329 .expect("put metadata"); 1330 assert_eq!( 1331 store 1332 .metadata(metadata.artifact_id()) 1333 .await 1334 .expect("metadata lookup"), 1335 Some(metadata.clone()) 1336 ); 1337 assert!( 1338 store 1339 .encrypted_private_artifact(metadata.artifact_id()) 1340 .await 1341 .expect("envelope lookup") 1342 .is_none() 1343 ); 1344 store 1345 .put_encrypted_private_artifact(metadata.clone(), &envelope) 1346 .await 1347 .expect("attach envelope"); 1348 } 1349 assert_eq!(store.status().await.expect("status").active, 4); 1350 let forbidden = sqlx::query_scalar::<_, i64>( 1351 "SELECT COUNT(*) FROM sqlite_schema 1352 WHERE lower(name) LIKE '%studio%' OR lower(name) LIKE '%ui_state%'", 1353 ) 1354 .fetch_one(store.private_pool()) 1355 .await 1356 .expect("forbidden schema count"); 1357 assert_eq!(forbidden, 0); 1358 } 1359 1360 #[tokio::test] 1361 async fn conflicts_corruption_and_read_only_mode_fail_closed() { 1362 let writable_store = store(EventStoreMode::ReadWrite).await; 1363 let envelope = sealed_envelope(b"signing reference", 5, 20, "signing.reference").await; 1364 let stored_metadata = metadata( 1365 20, 1366 "signing.reference", 1367 &envelope, 1368 RetentionPolicy::indefinite(), 1369 ); 1370 writable_store 1371 .put_encrypted_private_artifact(stored_metadata.clone(), &envelope) 1372 .await 1373 .expect("store artifact"); 1374 let other_envelope = 1375 sealed_envelope(b"another signing reference", 5, 20, "signing.reference").await; 1376 let conflicting = metadata( 1377 20, 1378 "signing.reference", 1379 &other_envelope, 1380 RetentionPolicy::indefinite(), 1381 ); 1382 assert_eq!( 1383 writable_store.put_metadata(conflicting).await, 1384 Err(Error::PrivateArtifactConflict) 1385 ); 1386 sqlx::query("PRAGMA ignore_check_constraints = ON") 1387 .execute(writable_store.private_pool()) 1388 .await 1389 .expect("disable checks"); 1390 sqlx::query("UPDATE radroots_private_artifacts SET stage = 'invalid'") 1391 .execute(writable_store.private_pool()) 1392 .await 1393 .expect("corrupt stage"); 1394 assert_eq!( 1395 writable_store.metadata(stored_metadata.artifact_id()).await, 1396 Err(Error::CorruptPrivateArtifactMetadata) 1397 ); 1398 1399 let read_only = store(EventStoreMode::ReadOnly).await; 1400 let envelope = sealed_envelope(b"read only", 1, 21, "nip46.session").await; 1401 let metadata = metadata( 1402 21, 1403 "nip46.session", 1404 &envelope, 1405 RetentionPolicy::indefinite(), 1406 ); 1407 assert_eq!( 1408 read_only 1409 .put_encrypted_private_artifact(metadata, &envelope) 1410 .await, 1411 Err(Error::BackendUnavailable) 1412 ); 1413 } 1414 1415 #[tokio::test] 1416 async fn legacy_reseal_is_atomic_idempotent_and_context_bound() { 1417 let (store, metadata, legacy) = migrated_legacy_store().await; 1418 assert_eq!(legacy.version(), 1); 1419 assert_eq!( 1420 store 1421 .private_artifact_envelope_migration_status() 1422 .await 1423 .expect("migration status"), 1424 PrivateArtifactEnvelopeMigrationStatus { 1425 v1_pending: 1, 1426 v2_current: 0, 1427 corrupt: 0, 1428 blocked_provider: 0, 1429 conflicted: 0, 1430 } 1431 ); 1432 1433 let context = test_context(1, "trade.private_terms"); 1434 let authority = LegacyV1ResealAuthority::new(); 1435 assert!(matches!( 1436 legacy 1437 .reseal_legacy_v1( 1438 &VectorWrapping, 1439 &authority, 1440 legacy.reference(), 1441 reference(8), 1442 context.clone(), 1443 &|_| false, 1444 SealMaterial::new( 1445 SecretMaterial::from_slice(&[0x33; 32]).expect("fresh key"), 1446 Nonce::new([0x44; 24]), 1447 ), 1448 ) 1449 .await, 1450 Err(SecretError::LegacyPayloadValidationFailed) 1451 )); 1452 assert_eq!( 1453 store 1454 .encrypted_private_artifact(metadata.artifact_id()) 1455 .await 1456 .expect("unchanged legacy") 1457 .expect("legacy envelope") 1458 .version(), 1459 1 1460 ); 1461 1462 let resealed = legacy 1463 .reseal_legacy_v1( 1464 &VectorWrapping, 1465 &authority, 1466 legacy.reference(), 1467 reference(8), 1468 context.clone(), 1469 &|plaintext| plaintext == b"radroots envelope vector", 1470 SealMaterial::new( 1471 SecretMaterial::from_slice(&[0x33; 32]).expect("fresh key"), 1472 Nonce::new([0x44; 24]), 1473 ), 1474 ) 1475 .await 1476 .expect("authorized reseal"); 1477 let encoded = resealed.envelope().encode().expect("v2 bytes"); 1478 let request = PrivateArtifactResealRequest::new( 1479 PrivateArtifactResealId::new([0x55; 16]).expect("reseal id"), 1480 metadata.artifact_id(), 1481 metadata.revision(), 1482 metadata.commitment(), 1483 ArtifactCommitment::new(Sha256::digest(encoded.as_slice()).into()), 1484 u64::try_from(encoded.len()).expect("v2 length"), 1485 DurableSecretReference::new("memory", "private-artifact-key", 8) 1486 .expect("next reference"), 1487 200, 1488 ) 1489 .expect("reseal request"); 1490 let contender = store.clone(); 1491 let (left, right) = tokio::join!( 1492 store.commit_private_artifact_reseal(request.clone(), resealed.envelope()), 1493 contender.commit_private_artifact_reseal(request.clone(), resealed.envelope()), 1494 ); 1495 let dispositions = [ 1496 left.expect("first concurrent outcome").disposition(), 1497 right.expect("second concurrent outcome").disposition(), 1498 ]; 1499 assert!(dispositions.contains(&PrivateArtifactResealDisposition::Committed)); 1500 assert!(dispositions.contains(&PrivateArtifactResealDisposition::Replayed)); 1501 let replayed = store 1502 .commit_private_artifact_reseal(request.clone(), resealed.envelope()) 1503 .await 1504 .expect("lost-response replay"); 1505 assert_eq!( 1506 replayed.disposition(), 1507 PrivateArtifactResealDisposition::Replayed 1508 ); 1509 let current = store 1510 .encrypted_private_artifact(metadata.artifact_id()) 1511 .await 1512 .expect("current envelope") 1513 .expect("v2 envelope"); 1514 assert_eq!(current.version(), 2); 1515 current 1516 .open(&VectorWrapping, &context) 1517 .await 1518 .expect("context-bound open") 1519 .expose_secret(|plaintext| assert_eq!(plaintext, b"radroots envelope vector")); 1520 assert_eq!( 1521 store 1522 .private_artifact_envelope_migration_status() 1523 .await 1524 .expect("migration status"), 1525 PrivateArtifactEnvelopeMigrationStatus { 1526 v1_pending: 0, 1527 v2_current: 1, 1528 corrupt: 0, 1529 blocked_provider: 0, 1530 conflicted: 0, 1531 } 1532 ); 1533 1534 let conflict = PrivateArtifactResealRequest::new( 1535 request.reseal_id(), 1536 request.artifact_id(), 1537 request.expected_revision(), 1538 request.expected_commitment(), 1539 ArtifactCommitment::new([0x66; 32]), 1540 request.next_protected_size_bytes(), 1541 request.next_secret_reference().clone(), 1542 request.committed_at_unix_ms(), 1543 ) 1544 .expect("conflicting request"); 1545 assert_eq!( 1546 store 1547 .commit_private_artifact_reseal(conflict, resealed.envelope()) 1548 .await, 1549 Err(Error::PrivateArtifactResealConflict) 1550 ); 1551 assert!( 1552 sqlx::query( 1553 "UPDATE radroots_private_artifacts SET encrypted_envelope = encrypted_envelope 1554 WHERE artifact_id = ?", 1555 ) 1556 .bind(metadata.artifact_id().as_bytes().as_slice()) 1557 .execute(store.private_pool()) 1558 .await 1559 .is_err() 1560 ); 1561 assert!( 1562 sqlx::query("DELETE FROM radroots_private_envelope_reseals") 1563 .execute(store.private_pool()) 1564 .await 1565 .is_err() 1566 ); 1567 } 1568 }