lib

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

private_artifact.rs (23527B)


      1 use radroots_storage::{
      2     Error,
      3     private_artifact::{
      4         ArtifactCommitment, ArtifactKind, ArtifactSchemaId, DeletionReason, DurableSecretReference,
      5         PrivateArtifactEnvelopeMigrationStatus, PrivateArtifactId, PrivateArtifactMetadata,
      6         PrivateArtifactResealDisposition, PrivateArtifactResealId, PrivateArtifactResealRequest,
      7         PrivateArtifactRevision, PrivateArtifactStage, PrivateArtifactStore, RetentionPolicy,
      8     },
      9 };
     10 
     11 #[cfg(feature = "memory")]
     12 use futures_executor::block_on;
     13 #[cfg(feature = "memory")]
     14 use radroots_storage::memory::MemoryStorage;
     15 
     16 fn metadata(retention: RetentionPolicy) -> PrivateArtifactMetadata {
     17     PrivateArtifactMetadata::new(
     18         PrivateArtifactId::new([1; 16]).expect("artifact id"),
     19         ArtifactKind::parse("trade.private_terms").expect("artifact kind"),
     20         ArtifactSchemaId::parse("trade.private_terms.v1").expect("schema id"),
     21         ArtifactCommitment::new([2; 32]),
     22         512,
     23         DurableSecretReference::new("keyring", "opaque-key-token", 3).expect("secret reference"),
     24         retention,
     25         100,
     26     )
     27     .expect("metadata")
     28 }
     29 
     30 #[test]
     31 fn retention_distinguishes_expiry_from_earliest_deletion() {
     32     let policy = RetentionPolicy::new(Some(400), Some(300)).expect("retention policy");
     33     assert!(!policy.is_expired_at(299));
     34     assert!(policy.is_expired_at(300));
     35     assert!(!policy.permits_deletion_at(399));
     36     assert!(policy.permits_deletion_at(400));
     37     assert_eq!(
     38         RetentionPolicy::new(Some(0), None),
     39         Err(Error::InvalidPrivateArtifactRetention)
     40     );
     41 }
     42 
     43 #[test]
     44 fn expiry_is_revision_bound_monotonic_and_policy_driven() {
     45     let active = metadata(RetentionPolicy::new(None, Some(300)).expect("retention"));
     46     assert_eq!(
     47         active.mark_expired(PrivateArtifactRevision::INITIAL, 299),
     48         Err(Error::PrivateArtifactNotExpired)
     49     );
     50     let expired = active
     51         .mark_expired(PrivateArtifactRevision::INITIAL, 300)
     52         .expect("expired metadata");
     53     assert_eq!(expired.stage(), PrivateArtifactStage::Expired);
     54     assert_eq!(expired.revision().get(), 2);
     55     assert_eq!(
     56         expired.mark_expired(PrivateArtifactRevision::INITIAL, 301),
     57         Err(Error::PrivateArtifactRevisionConflict)
     58     );
     59 }
     60 
     61 #[test]
     62 fn tombstones_preserve_commitment_and_enforce_retention() {
     63     let active = metadata(RetentionPolicy::new(Some(400), Some(300)).expect("retention"));
     64     let expired = active
     65         .mark_expired(PrivateArtifactRevision::INITIAL, 300)
     66         .expect("expired metadata");
     67     assert_eq!(
     68         expired.tombstone(expired.revision(), 399, DeletionReason::RetentionExpired,),
     69         Err(Error::PrivateArtifactRetentionActive)
     70     );
     71     let deleted = expired
     72         .tombstone(expired.revision(), 400, DeletionReason::RetentionExpired)
     73         .expect("tombstone");
     74     assert_eq!(deleted.stage(), PrivateArtifactStage::Tombstoned);
     75     let tombstone = deleted.tombstone_record().expect("tombstone record");
     76     assert_eq!(tombstone.commitment(), active.commitment());
     77     assert_eq!(tombstone.deleted_at_unix_ms(), 400);
     78     assert_eq!(tombstone.reason(), DeletionReason::RetentionExpired);
     79     assert_eq!(
     80         deleted.tombstone(deleted.revision(), 401, DeletionReason::OperatorRequested,),
     81         Err(Error::PrivateArtifactTombstoned)
     82     );
     83 }
     84 
     85 #[test]
     86 #[cfg(feature = "serde")]
     87 fn secret_references_are_bounded_redacted_and_round_trip() {
     88     let reference =
     89         DurableSecretReference::new("keyring", "opaque-key-token", 3).expect("secret reference");
     90     let diagnostic = format!("{reference:?}");
     91     assert!(diagnostic.contains("[REDACTED]"));
     92     assert!(!diagnostic.contains("opaque-key-token"));
     93 
     94     let encoded = serde_json::to_string(&reference).expect("reference JSON");
     95     let decoded: DurableSecretReference =
     96         serde_json::from_str(encoded.as_str()).expect("reference round trip");
     97     assert_eq!(decoded, reference);
     98     assert!(
     99         serde_json::from_str::<DurableSecretReference>(
    100             r#"{"provider":"keyring","opaque_reference":"token","key_version":0}"#,
    101         )
    102         .is_err()
    103     );
    104     assert_eq!(
    105         DurableSecretReference::new("keyring", "token", 0),
    106         Err(Error::InvalidPrivateArtifactSecretReference)
    107     );
    108 }
    109 
    110 #[test]
    111 fn metadata_contains_only_protected_size_commitment_and_reference() {
    112     fn accepts_dyn(_: Option<&dyn PrivateArtifactStore>) {}
    113     accepts_dyn(None);
    114 
    115     let value = metadata(RetentionPolicy::indefinite());
    116     assert_eq!(value.protected_size_bytes(), 512);
    117     assert_eq!(value.secret_reference().provider(), "keyring");
    118     assert_eq!(value.secret_reference().key_version(), 3);
    119     assert_eq!(
    120         PrivateArtifactId::new([0; 16]),
    121         Err(Error::InvalidPrivateArtifactId)
    122     );
    123     assert_eq!(
    124         ArtifactKind::parse("Not Canonical"),
    125         Err(Error::InvalidPrivateArtifactKind)
    126     );
    127 }
    128 
    129 #[test]
    130 fn private_artifact_value_matrix_covers_every_bound_and_accessor() {
    131     let id = PrivateArtifactId::new([1; 16]).expect("id");
    132     assert_eq!(id.as_bytes(), &[1; 16]);
    133     for value in ["", "Uppercase", " leading", "trailing ", "bad/slash"] {
    134         assert_eq!(
    135             ArtifactKind::parse(value),
    136             Err(Error::InvalidPrivateArtifactKind)
    137         );
    138         assert_eq!(
    139             ArtifactSchemaId::parse(value),
    140             Err(Error::InvalidPrivateArtifactSchema)
    141         );
    142     }
    143     assert_eq!(
    144         ArtifactKind::parse(
    145             "x".repeat(radroots_storage::private_artifact::ARTIFACT_KIND_MAX_BYTES + 1)
    146         ),
    147         Err(Error::InvalidPrivateArtifactKind)
    148     );
    149     assert_eq!(
    150         ArtifactSchemaId::parse(
    151             "x".repeat(radroots_storage::private_artifact::ARTIFACT_SCHEMA_MAX_BYTES + 1)
    152         ),
    153         Err(Error::InvalidPrivateArtifactSchema)
    154     );
    155     let kind = ArtifactKind::parse("trade.private_terms").unwrap();
    156     let schema = ArtifactSchemaId::parse("trade.private_terms.v1").unwrap();
    157     assert_eq!(kind.as_str(), "trade.private_terms");
    158     assert_eq!(schema.as_str(), "trade.private_terms.v1");
    159     let commitment = ArtifactCommitment::new([2; 32]);
    160     assert_eq!(commitment.as_bytes(), &[2; 32]);
    161 
    162     for (provider, reference, version) in [
    163         ("", "token", 1),
    164         ("Bad", "token", 1),
    165         ("keyring", "", 1),
    166         ("keyring", " token", 1),
    167         ("keyring", "token ", 1),
    168         ("keyring", "bad\ntoken", 1),
    169         ("keyring", "token", 0),
    170     ] {
    171         assert_eq!(
    172             DurableSecretReference::new(provider, reference, version),
    173             Err(Error::InvalidPrivateArtifactSecretReference)
    174         );
    175     }
    176     assert_eq!(
    177         DurableSecretReference::new(
    178             "x".repeat(radroots_storage::private_artifact::SECRET_PROVIDER_MAX_BYTES + 1),
    179             "token",
    180             1,
    181         ),
    182         Err(Error::InvalidPrivateArtifactSecretReference)
    183     );
    184     assert_eq!(
    185         DurableSecretReference::new(
    186             "keyring",
    187             "x".repeat(radroots_storage::private_artifact::SECRET_REFERENCE_MAX_BYTES + 1),
    188             1,
    189         ),
    190         Err(Error::InvalidPrivateArtifactSecretReference)
    191     );
    192     let secret = DurableSecretReference::new("keyring", "opaque", 1).unwrap();
    193     assert_eq!(secret.provider(), "keyring");
    194     assert_eq!(secret.opaque_reference(), "opaque");
    195     assert_eq!(secret.key_version(), 1);
    196 
    197     assert_eq!(
    198         RetentionPolicy::new(None, Some(0)),
    199         Err(Error::InvalidPrivateArtifactRetention)
    200     );
    201     let retention = RetentionPolicy::new(Some(200), Some(150)).unwrap();
    202     assert_eq!(retention.delete_not_before_unix_ms(), Some(200));
    203     assert_eq!(retention.expires_at_unix_ms(), Some(150));
    204     assert!(!retention.is_expired_at(149));
    205     assert!(retention.is_expired_at(150));
    206     assert!(!retention.permits_deletion_at(199));
    207     assert!(retention.permits_deletion_at(200));
    208     let indefinite = RetentionPolicy::indefinite();
    209     assert!(!indefinite.is_expired_at(u64::MAX));
    210     assert!(indefinite.permits_deletion_at(0));
    211     assert_eq!(
    212         PrivateArtifactRevision::new(0),
    213         Err(Error::InvalidPrivateArtifactRevision)
    214     );
    215     assert_eq!(PrivateArtifactRevision::new(2).unwrap().get(), 2);
    216 
    217     let value = metadata(retention);
    218     assert_eq!(value.artifact_id(), id);
    219     assert_eq!(value.kind(), &kind);
    220     assert_eq!(value.schema_id(), &schema);
    221     assert_eq!(value.commitment(), commitment);
    222     assert_eq!(value.protected_size_bytes(), 512);
    223     assert_eq!(
    224         value.secret_reference().opaque_reference(),
    225         "opaque-key-token"
    226     );
    227     assert_eq!(value.retention(), retention);
    228     assert_eq!(value.revision(), PrivateArtifactRevision::INITIAL);
    229     assert_eq!(value.stage(), PrivateArtifactStage::Active);
    230     assert_eq!(value.created_at_unix_ms(), 100);
    231     assert_eq!(value.updated_at_unix_ms(), 100);
    232     assert!(value.tombstone_record().is_none());
    233 }
    234 
    235 #[test]
    236 fn metadata_construction_and_durable_state_fail_closed() {
    237     let id = PrivateArtifactId::new([1; 16]).unwrap();
    238     let kind = ArtifactKind::parse("trade.private_terms").unwrap();
    239     let schema = ArtifactSchemaId::parse("trade.private_terms.v1").unwrap();
    240     let commitment = ArtifactCommitment::new([2; 32]);
    241     let secret = DurableSecretReference::new("keyring", "opaque", 1).unwrap();
    242     for (size, created, retention) in [
    243         (0, 100, RetentionPolicy::indefinite()),
    244         (1, 0, RetentionPolicy::indefinite()),
    245         (1, 100, RetentionPolicy::new(Some(99), None).unwrap()),
    246         (1, 100, RetentionPolicy::new(None, Some(99)).unwrap()),
    247     ] {
    248         assert_eq!(
    249             PrivateArtifactMetadata::new(
    250                 id,
    251                 kind.clone(),
    252                 schema.clone(),
    253                 commitment,
    254                 size,
    255                 secret.clone(),
    256                 retention,
    257                 created,
    258             ),
    259             Err(Error::InvalidPrivateArtifactMetadata)
    260         );
    261     }
    262 
    263     let retention = RetentionPolicy::new(Some(200), Some(150)).unwrap();
    264     let durable = |revision, stage, updated, tombstone| {
    265         PrivateArtifactMetadata::from_durable_parts(
    266             id,
    267             kind.clone(),
    268             schema.clone(),
    269             commitment,
    270             1,
    271             secret.clone(),
    272             retention,
    273             revision,
    274             stage,
    275             100,
    276             updated,
    277             tombstone,
    278         )
    279     };
    280     assert!(
    281         durable(
    282             PrivateArtifactRevision::INITIAL,
    283             PrivateArtifactStage::Active,
    284             100,
    285             None
    286         )
    287         .is_ok()
    288     );
    289     assert!(
    290         durable(
    291             PrivateArtifactRevision::new(2).unwrap(),
    292             PrivateArtifactStage::Expired,
    293             150,
    294             None
    295         )
    296         .is_ok()
    297     );
    298     assert!(
    299         durable(
    300             PrivateArtifactRevision::new(3).unwrap(),
    301             PrivateArtifactStage::Tombstoned,
    302             200,
    303             Some((200, DeletionReason::RetentionExpired, commitment)),
    304         )
    305         .is_ok()
    306     );
    307     for result in [
    308         durable(
    309             PrivateArtifactRevision::INITIAL,
    310             PrivateArtifactStage::Active,
    311             99,
    312             None,
    313         ),
    314         durable(
    315             PrivateArtifactRevision::new(2).unwrap(),
    316             PrivateArtifactStage::Active,
    317             100,
    318             None,
    319         ),
    320         durable(
    321             PrivateArtifactRevision::new(2).unwrap(),
    322             PrivateArtifactStage::Expired,
    323             149,
    324             None,
    325         ),
    326         durable(
    327             PrivateArtifactRevision::new(3).unwrap(),
    328             PrivateArtifactStage::Tombstoned,
    329             200,
    330             None,
    331         ),
    332         durable(
    333             PrivateArtifactRevision::new(3).unwrap(),
    334             PrivateArtifactStage::Tombstoned,
    335             200,
    336             Some((199, DeletionReason::UserRequested, commitment)),
    337         ),
    338         durable(
    339             PrivateArtifactRevision::new(3).unwrap(),
    340             PrivateArtifactStage::Tombstoned,
    341             200,
    342             Some((
    343                 200,
    344                 DeletionReason::UserRequested,
    345                 ArtifactCommitment::new([9; 32]),
    346             )),
    347         ),
    348         durable(
    349             PrivateArtifactRevision::new(3).unwrap(),
    350             PrivateArtifactStage::Tombstoned,
    351             199,
    352             Some((199, DeletionReason::UserRequested, commitment)),
    353         ),
    354         durable(
    355             PrivateArtifactRevision::new(3).unwrap(),
    356             PrivateArtifactStage::Tombstoned,
    357             149,
    358             Some((149, DeletionReason::RetentionExpired, commitment)),
    359         ),
    360     ] {
    361         assert_eq!(result, Err(Error::CorruptPrivateArtifactMetadata));
    362     }
    363 }
    364 
    365 #[test]
    366 fn transition_and_status_edge_matrix_is_complete() {
    367     let active = metadata(RetentionPolicy::new(Some(200), Some(150)).unwrap());
    368     assert_eq!(
    369         active.mark_expired(PrivateArtifactRevision::new(2).unwrap(), 150),
    370         Err(Error::PrivateArtifactRevisionConflict)
    371     );
    372     assert_eq!(
    373         active.mark_expired(PrivateArtifactRevision::INITIAL, 99),
    374         Err(Error::InvalidPrivateArtifactTimestamp)
    375     );
    376     assert_eq!(
    377         active.tombstone(
    378             PrivateArtifactRevision::INITIAL,
    379             150,
    380             DeletionReason::RetentionExpired,
    381         ),
    382         Err(Error::PrivateArtifactRetentionActive)
    383     );
    384     let direct = active
    385         .tombstone(
    386             PrivateArtifactRevision::INITIAL,
    387             200,
    388             DeletionReason::UserRequested,
    389         )
    390         .unwrap();
    391     assert_eq!(direct.revision().get(), 2);
    392     assert_eq!(direct.stage(), PrivateArtifactStage::Tombstoned);
    393     let tombstone = direct.tombstone_record().unwrap();
    394     assert_eq!(tombstone.deleted_at_unix_ms(), 200);
    395     assert_eq!(tombstone.reason(), DeletionReason::UserRequested);
    396     assert_eq!(tombstone.commitment(), active.commitment());
    397 
    398     assert_eq!(
    399         radroots_storage::private_artifact::PrivateArtifactStatus {
    400             active: 1,
    401             expired: 2,
    402             tombstoned: 3,
    403         }
    404         .total(),
    405         Some(6)
    406     );
    407     assert_eq!(
    408         radroots_storage::private_artifact::PrivateArtifactStatus {
    409             active: u64::MAX,
    410             expired: 1,
    411             tombstoned: 0,
    412         }
    413         .total(),
    414         None
    415     );
    416     assert_eq!(
    417         radroots_storage::private_artifact::PrivateArtifactStatus {
    418             active: 0,
    419             expired: u64::MAX,
    420             tombstoned: 1,
    421         }
    422         .total(),
    423         None
    424     );
    425 }
    426 
    427 #[test]
    428 fn envelope_context_is_derived_and_transplant_resistant() {
    429     let original = metadata(RetentionPolicy::indefinite());
    430     let context = original.envelope_context();
    431     assert_eq!(
    432         context.purpose(),
    433         "radroots.private_artifact.trade.private_terms"
    434     );
    435     assert_eq!(context.subject_type(), "private_artifact");
    436     assert_eq!(context.subject(), "01010101010101010101010101010101");
    437     assert_eq!(context.payload_schema(), "trade.private_terms.v1");
    438 
    439     let with_id = |id, kind, schema| {
    440         PrivateArtifactMetadata::new(
    441             PrivateArtifactId::new(id).unwrap(),
    442             ArtifactKind::parse(kind).unwrap(),
    443             ArtifactSchemaId::parse(schema).unwrap(),
    444             ArtifactCommitment::new([2; 32]),
    445             512,
    446             DurableSecretReference::new("keyring", "opaque-key-token", 3).unwrap(),
    447             RetentionPolicy::indefinite(),
    448             100,
    449         )
    450         .unwrap()
    451         .envelope_context()
    452         .fingerprint()
    453     };
    454     let fingerprint = context.fingerprint();
    455     assert_ne!(
    456         fingerprint,
    457         with_id([3; 16], "trade.private_terms", "trade.private_terms.v1")
    458     );
    459     assert_ne!(
    460         fingerprint,
    461         with_id([1; 16], "trade.other_terms", "trade.private_terms.v1")
    462     );
    463     assert_ne!(
    464         fingerprint,
    465         with_id([1; 16], "trade.private_terms", "trade.private_terms.v2")
    466     );
    467 
    468     for invalid in ["trade..terms", ".trade.terms", "trade.1terms", "trade"] {
    469         assert_eq!(
    470             ArtifactKind::parse(invalid),
    471             Err(Error::InvalidPrivateArtifactKind)
    472         );
    473     }
    474     for invalid in [
    475         "trade..terms.v1",
    476         "trade.terms",
    477         "trade.terms.latest",
    478         "trade.terms.v",
    479     ] {
    480         assert_eq!(
    481             ArtifactSchemaId::parse(invalid),
    482             Err(Error::InvalidPrivateArtifactSchema)
    483         );
    484     }
    485     let diagnostic = format!("{context:?} {original:?}");
    486     assert!(!diagnostic.contains("01010101010101010101010101010101"));
    487 }
    488 
    489 #[test]
    490 #[cfg(feature = "memory")]
    491 fn reseal_contract_distinguishes_exact_replay_and_conflict() {
    492     let store = MemoryStorage::default();
    493     let initial = metadata(RetentionPolicy::indefinite());
    494     block_on(store.put_metadata(initial.clone())).unwrap();
    495     let reseal_id = PrivateArtifactResealId::new([9; 16]).unwrap();
    496     let next_reference = DurableSecretReference::new("keyring", "fresh-token", 4).unwrap();
    497     for (next_commitment, next_size, committed_at) in [
    498         (initial.commitment(), 640, 200),
    499         (ArtifactCommitment::new([8; 32]), 0, 200),
    500         (ArtifactCommitment::new([8; 32]), 640, 0),
    501     ] {
    502         assert_eq!(
    503             PrivateArtifactResealRequest::new(
    504                 reseal_id,
    505                 initial.artifact_id(),
    506                 initial.revision(),
    507                 initial.commitment(),
    508                 next_commitment,
    509                 next_size,
    510                 next_reference.clone(),
    511                 committed_at,
    512             ),
    513             Err(Error::InvalidPrivateArtifactResealRequest)
    514         );
    515     }
    516     let request = PrivateArtifactResealRequest::new(
    517         reseal_id,
    518         initial.artifact_id(),
    519         initial.revision(),
    520         initial.commitment(),
    521         ArtifactCommitment::new([8; 32]),
    522         640,
    523         next_reference,
    524         200,
    525     )
    526     .unwrap();
    527     let committed = block_on(store.reseal_metadata(request.clone())).unwrap();
    528     assert_eq!(
    529         committed.disposition(),
    530         PrivateArtifactResealDisposition::Committed
    531     );
    532     assert_eq!(committed.committed_revision().get(), 2);
    533     assert_eq!(committed.request_fingerprint(), request.fingerprint());
    534 
    535     let wrong_reseal_id = PrivateArtifactResealRequest::new(
    536         PrivateArtifactResealId::new([7; 16]).unwrap(),
    537         request.artifact_id(),
    538         request.expected_revision(),
    539         request.expected_commitment(),
    540         request.next_commitment(),
    541         request.next_protected_size_bytes(),
    542         request.next_secret_reference().clone(),
    543         request.committed_at_unix_ms(),
    544     )
    545     .unwrap();
    546     assert_eq!(
    547         committed.replay(&wrong_reseal_id),
    548         Err(Error::PrivateArtifactResealConflict)
    549     );
    550     let wrong_artifact = PrivateArtifactResealRequest::new(
    551         request.reseal_id(),
    552         PrivateArtifactId::new([7; 16]).unwrap(),
    553         request.expected_revision(),
    554         request.expected_commitment(),
    555         request.next_commitment(),
    556         request.next_protected_size_bytes(),
    557         request.next_secret_reference().clone(),
    558         request.committed_at_unix_ms(),
    559     )
    560     .unwrap();
    561     assert_eq!(
    562         committed.replay(&wrong_artifact),
    563         Err(Error::PrivateArtifactResealConflict)
    564     );
    565     assert_eq!(
    566         initial.resealed(&wrong_artifact),
    567         Err(Error::PrivateArtifactResealConflict)
    568     );
    569     let wrong_revision = PrivateArtifactResealRequest::new(
    570         request.reseal_id(),
    571         request.artifact_id(),
    572         PrivateArtifactRevision::new(2).unwrap(),
    573         request.expected_commitment(),
    574         request.next_commitment(),
    575         request.next_protected_size_bytes(),
    576         request.next_secret_reference().clone(),
    577         request.committed_at_unix_ms(),
    578     )
    579     .unwrap();
    580     assert_eq!(
    581         initial.resealed(&wrong_revision),
    582         Err(Error::PrivateArtifactResealConflict)
    583     );
    584     let wrong_commitment = PrivateArtifactResealRequest::new(
    585         request.reseal_id(),
    586         request.artifact_id(),
    587         request.expected_revision(),
    588         ArtifactCommitment::new([6; 32]),
    589         request.next_commitment(),
    590         request.next_protected_size_bytes(),
    591         request.next_secret_reference().clone(),
    592         request.committed_at_unix_ms(),
    593     )
    594     .unwrap();
    595     assert_eq!(
    596         initial.resealed(&wrong_commitment),
    597         Err(Error::PrivateArtifactResealConflict)
    598     );
    599     let stale_timestamp = PrivateArtifactResealRequest::new(
    600         request.reseal_id(),
    601         request.artifact_id(),
    602         request.expected_revision(),
    603         request.expected_commitment(),
    604         request.next_commitment(),
    605         request.next_protected_size_bytes(),
    606         request.next_secret_reference().clone(),
    607         initial.updated_at_unix_ms(),
    608     )
    609     .unwrap();
    610     assert_eq!(
    611         initial.resealed(&stale_timestamp),
    612         Err(Error::InvalidPrivateArtifactTimestamp)
    613     );
    614 
    615     let replayed = block_on(store.reseal_metadata(request.clone())).unwrap();
    616     assert_eq!(
    617         replayed.disposition(),
    618         PrivateArtifactResealDisposition::Replayed
    619     );
    620     assert_eq!(
    621         replayed.committed_revision(),
    622         committed.committed_revision()
    623     );
    624 
    625     let conflicting = PrivateArtifactResealRequest::new(
    626         request.reseal_id(),
    627         request.artifact_id(),
    628         request.expected_revision(),
    629         request.expected_commitment(),
    630         ArtifactCommitment::new([7; 32]),
    631         request.next_protected_size_bytes(),
    632         request.next_secret_reference().clone(),
    633         request.committed_at_unix_ms(),
    634     )
    635     .unwrap();
    636     assert_eq!(
    637         block_on(store.reseal_metadata(conflicting)),
    638         Err(Error::PrivateArtifactResealConflict)
    639     );
    640     assert_eq!(
    641         PrivateArtifactResealId::new([0; 16]),
    642         Err(Error::InvalidPrivateArtifactResealId)
    643     );
    644 }
    645 
    646 #[test]
    647 fn migration_status_is_bounded_and_overflow_safe() {
    648     assert_eq!(
    649         PrivateArtifactEnvelopeMigrationStatus {
    650             v1_pending: 1,
    651             v2_current: 2,
    652             corrupt: 3,
    653             blocked_provider: 4,
    654             conflicted: 5,
    655         }
    656         .total(),
    657         Some(15)
    658     );
    659     assert_eq!(
    660         PrivateArtifactEnvelopeMigrationStatus {
    661             v1_pending: u64::MAX,
    662             v2_current: 1,
    663             ..PrivateArtifactEnvelopeMigrationStatus::default()
    664         }
    665         .total(),
    666         None
    667     );
    668 }
    669 
    670 #[test]
    671 #[cfg(feature = "serde")]
    672 fn serde_cannot_bypass_private_artifact_invariants() {
    673     let value = metadata(RetentionPolicy::indefinite());
    674     let encoded = serde_json::to_string(&value).expect("metadata JSON");
    675     let decoded: PrivateArtifactMetadata =
    676         serde_json::from_str(&encoded).expect("validated metadata round trip");
    677     assert_eq!(decoded, value);
    678 
    679     assert!(serde_json::from_str::<PrivateArtifactId>(&format!("{:?}", [0_u8; 16])).is_err());
    680     assert!(serde_json::from_str::<PrivateArtifactResealId>(&format!("{:?}", [0_u8; 16])).is_err());
    681     assert!(serde_json::from_str::<ArtifactKind>(r#""not_namespaced""#).is_err());
    682     assert!(serde_json::from_str::<ArtifactSchemaId>(r#""trade.private_terms.latest""#).is_err());
    683     assert!(serde_json::from_str::<PrivateArtifactRevision>("0").is_err());
    684     assert!(
    685         serde_json::from_str::<RetentionPolicy>(
    686             r#"{"delete_not_before_unix_ms":0,"expires_at_unix_ms":null}"#,
    687         )
    688         .is_err()
    689     );
    690 
    691     let mut forged = serde_json::to_value(&value).expect("metadata value");
    692     forged["revision"] = serde_json::json!(2);
    693     assert!(serde_json::from_value::<PrivateArtifactMetadata>(forged).is_err());
    694 }