lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

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 }