From 5a96d2931225e133768878c68d09e1a36b5ca0f6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 11:23:09 +0900 Subject: [PATCH 1/5] test(sensitive): require immediate tracked-reservation recheck --- .../tests/sensitive_handle_recheck.rs | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 crates/originweave-policy/tests/sensitive_handle_recheck.rs diff --git a/crates/originweave-policy/tests/sensitive_handle_recheck.rs b/crates/originweave-policy/tests/sensitive_handle_recheck.rs new file mode 100644 index 00000000..5a3af8f9 --- /dev/null +++ b/crates/originweave-policy/tests/sensitive_handle_recheck.rs @@ -0,0 +1,125 @@ +#![allow(clippy::expect_used)] + +use originweave_core::Origin; +use originweave_policy::{ + DataClassification, HandleRevocationReason, HandleUseDecision, SensitiveDataAuthority, + SensitiveHandleUseState, SensitiveValueHandleScope, +}; + +const TENANT: &str = "tenant_alpha"; +const TASK: &str = "task_ship_order"; +const FIELD: &str = "shipping_address"; +const PURPOSE: &str = "fulfill_order"; +const DESTINATION: &str = "https://shipping.example"; +const AUDIENCE: &str = "trusted_browser_adapter"; + +fn authority(destination: &str) -> SensitiveDataAuthority { + SensitiveDataAuthority::new( + TENANT, + TASK, + FIELD, + PURPOSE, + Origin::parse(destination).expect("test origin must be valid"), + DataClassification::PersonalData, + ) +} + +fn scope(max_uses: u32) -> SensitiveValueHandleScope { + SensitiveValueHandleScope::new(authority(DESTINATION), AUDIENCE, 2_000, max_uses) +} + +#[test] +fn outstanding_reservation_can_be_rechecked_without_consuming_another_use() { + let mut state = SensitiveHandleUseState::new(scope(1)); + let reservation = state + .reserve_tracked_use(authority(DESTINATION), AUDIENCE, 1_900) + .expect("reservation must be authorized"); + + assert_eq!(state.reserved_uses(), 1); + assert_eq!( + state.recheck_reservation(&reservation, authority(DESTINATION), AUDIENCE, 1_999), + HandleUseDecision::Authorized + ); + assert_eq!(state.reserved_uses(), 1); + assert_eq!(state.outstanding_reservations(), 1); +} + +#[test] +fn foreign_or_settled_reservation_cannot_be_rechecked() { + let mut first_state = SensitiveHandleUseState::new(scope(2)); + let mut second_state = SensitiveHandleUseState::new(scope(2)); + let first = first_state + .reserve_tracked_use(authority(DESTINATION), AUDIENCE, 1_900) + .expect("first reservation must be authorized"); + let second = second_state + .reserve_tracked_use(authority(DESTINATION), AUDIENCE, 1_900) + .expect("second reservation must be authorized"); + + assert_eq!( + second_state.recheck_reservation(&first, authority(DESTINATION), AUDIENCE, 1_999), + HandleUseDecision::ReservationNotOutstanding + ); + assert!(first_state.compensate_reservation(&first)); + assert_eq!( + first_state.recheck_reservation(&first, authority(DESTINATION), AUDIENCE, 1_999), + HandleUseDecision::ReservationNotOutstanding + ); + assert!(second_state.commit_reservation(&second)); + assert_eq!( + second_state.recheck_reservation(&second, authority(DESTINATION), AUDIENCE, 1_999), + HandleUseDecision::ReservationNotOutstanding + ); +} + +#[test] +fn recheck_revalidates_scope_audience_and_expiry() { + let mut state = SensitiveHandleUseState::new(scope(1)); + let reservation = state + .reserve_tracked_use(authority(DESTINATION), AUDIENCE, 1_900) + .expect("reservation must be authorized"); + + assert_eq!( + state.recheck_reservation( + &reservation, + authority("https://other.example"), + AUDIENCE, + 1_999, + ), + HandleUseDecision::ScopeMismatch + ); + assert_eq!( + state.recheck_reservation( + &reservation, + authority(DESTINATION), + "other_browser_adapter", + 1_999, + ), + HandleUseDecision::AudienceMismatch + ); + assert_eq!( + state.recheck_reservation(&reservation, authority(DESTINATION), AUDIENCE, 2_000), + HandleUseDecision::Expired + ); + assert_eq!(state.reserved_uses(), 1); + assert_eq!(state.outstanding_reservations(), 1); +} + +#[test] +fn revocation_precedes_reservation_and_request_details_on_recheck() { + let mut active_state = SensitiveHandleUseState::new(scope(1)); + let foreign = active_state + .reserve_tracked_use(authority(DESTINATION), AUDIENCE, 1_900) + .expect("foreign reservation must be authorized"); + let mut revoked_state = SensitiveHandleUseState::new(scope(1)); + assert!(revoked_state.revoke(HandleRevocationReason::PolicyChanged)); + + assert_eq!( + revoked_state.recheck_reservation( + &foreign, + authority("https://other.example"), + "other_browser_adapter", + 2_001, + ), + HandleUseDecision::Revoked + ); +} From 02678764a1860c6847d1e9d882e0fb40e89ff88a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 11:26:41 +0900 Subject: [PATCH 2/5] feat(sensitive): recheck tracked reservation before disclosure --- .../originweave-policy/src/sensitive_data.rs | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/crates/originweave-policy/src/sensitive_data.rs b/crates/originweave-policy/src/sensitive_data.rs index b62791e2..aa371f97 100644 --- a/crates/originweave-policy/src/sensitive_data.rs +++ b/crates/originweave-policy/src/sensitive_data.rs @@ -163,6 +163,8 @@ pub enum HandleUseDecision { Authorized, /// The authoritative in-process handle state was revoked before this use. Revoked, + /// The supplied tracked-use identity is not outstanding in this exact state. + ReservationNotOutstanding, /// Tenant, task, field, purpose, destination, or classification did not match the handle scope. ScopeMismatch, /// The caller audience was invalid or did not match the handle's non-transferable audience. @@ -405,6 +407,51 @@ impl SensitiveHandleUseState { Ok(SensitiveHandleUseReservation { identity }) } + /// Recheck the exact outstanding tracked reservation immediately before disclosure. + /// + /// This does not reserve another use and does not mutate settlement state. The + /// trusted broker must supply authenticated audience, trusted time, and the exact + /// authority that applies at disclosure time, and must call this inside the same + /// transaction or locking boundary that guards value disclosure. Revocation is + /// checked before reservation membership and request detail so a revoked state + /// does not disclose whether a foreign or stale reservation would otherwise match. + /// A use-limit check is intentionally omitted: the outstanding reservation has + /// already consumed its bounded use capacity. + #[must_use] + pub fn recheck_reservation( + &self, + reservation: &SensitiveHandleUseReservation, + authority: SensitiveDataAuthority, + audience_id: &str, + now_epoch_seconds: u64, + ) -> HandleUseDecision { + if self.revocation_reason.is_some() { + return HandleUseDecision::Revoked; + } + if !self + .outstanding_reservations + .iter() + .any(|candidate| candidate == reservation) + { + return HandleUseDecision::ReservationNotOutstanding; + } + if !authority.is_complete() + || !self.scope.authority.is_complete() + || authority != self.scope.authority + { + HandleUseDecision::ScopeMismatch + } else if !authority_identifier_is_valid(audience_id) + || !authority_identifier_is_valid(&self.scope.audience_id) + || audience_id != self.scope.audience_id + { + HandleUseDecision::AudienceMismatch + } else if now_epoch_seconds >= self.scope.expires_at_epoch_seconds { + HandleUseDecision::Expired + } else { + HandleUseDecision::Authorized + } + } + /// Mark one exact tracked reservation as a completed, permanently consumed use. /// /// This method records settlement only; it does not authorize disclosure. A From d6e5f180e87f7caa39a6a16d35a2de455a39a282 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 11:33:50 +0900 Subject: [PATCH 3/5] test(sensitive): cover malformed recheck inputs --- .../tests/sensitive_handle_recheck.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/crates/originweave-policy/tests/sensitive_handle_recheck.rs b/crates/originweave-policy/tests/sensitive_handle_recheck.rs index 5a3af8f9..65aa6560 100644 --- a/crates/originweave-policy/tests/sensitive_handle_recheck.rs +++ b/crates/originweave-policy/tests/sensitive_handle_recheck.rs @@ -24,6 +24,17 @@ fn authority(destination: &str) -> SensitiveDataAuthority { ) } +fn authority_with_tenant(tenant_id: &str) -> SensitiveDataAuthority { + SensitiveDataAuthority::new( + tenant_id, + TASK, + FIELD, + PURPOSE, + Origin::parse(DESTINATION).expect("test origin must be valid"), + DataClassification::PersonalData, + ) +} + fn scope(max_uses: u32) -> SensitiveValueHandleScope { SensitiveValueHandleScope::new(authority(DESTINATION), AUDIENCE, 2_000, max_uses) } @@ -87,6 +98,10 @@ fn recheck_revalidates_scope_audience_and_expiry() { ), HandleUseDecision::ScopeMismatch ); + assert_eq!( + state.recheck_reservation(&reservation, authority_with_tenant(""), AUDIENCE, 1_999), + HandleUseDecision::ScopeMismatch + ); assert_eq!( state.recheck_reservation( &reservation, @@ -96,6 +111,10 @@ fn recheck_revalidates_scope_audience_and_expiry() { ), HandleUseDecision::AudienceMismatch ); + assert_eq!( + state.recheck_reservation(&reservation, authority(DESTINATION), "", 1_999), + HandleUseDecision::AudienceMismatch + ); assert_eq!( state.recheck_reservation(&reservation, authority(DESTINATION), AUDIENCE, 2_000), HandleUseDecision::Expired From c6845af11f71f96afce69169140aa9a7b78c75a4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 11:36:03 +0900 Subject: [PATCH 4/5] fix(sensitive): remove unreachable recheck branches --- crates/originweave-policy/src/sensitive_data.rs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/crates/originweave-policy/src/sensitive_data.rs b/crates/originweave-policy/src/sensitive_data.rs index aa371f97..80a4b188 100644 --- a/crates/originweave-policy/src/sensitive_data.rs +++ b/crates/originweave-policy/src/sensitive_data.rs @@ -415,8 +415,12 @@ impl SensitiveHandleUseState { /// transaction or locking boundary that guards value disclosure. Revocation is /// checked before reservation membership and request detail so a revoked state /// does not disclose whether a foreign or stale reservation would otherwise match. - /// A use-limit check is intentionally omitted: the outstanding reservation has - /// already consumed its bounded use capacity. + /// An outstanding reservation also proves that this immutable state scope passed + /// authority and audience validation when it was admitted; recheck therefore + /// validates the caller-supplied authority and audience without duplicating + /// unreachable scope-validation branches. A use-limit check is intentionally + /// omitted because the outstanding reservation already consumed its bounded use + /// capacity. #[must_use] pub fn recheck_reservation( &self, @@ -435,14 +439,9 @@ impl SensitiveHandleUseState { { return HandleUseDecision::ReservationNotOutstanding; } - if !authority.is_complete() - || !self.scope.authority.is_complete() - || authority != self.scope.authority - { + if !authority.is_complete() || authority != self.scope.authority { HandleUseDecision::ScopeMismatch - } else if !authority_identifier_is_valid(audience_id) - || !authority_identifier_is_valid(&self.scope.audience_id) - || audience_id != self.scope.audience_id + } else if !authority_identifier_is_valid(audience_id) || audience_id != self.scope.audience_id { HandleUseDecision::AudienceMismatch } else if now_epoch_seconds >= self.scope.expires_at_epoch_seconds { From de79d85e6be5131036db119efab767f0eb76a816 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 11:41:51 +0900 Subject: [PATCH 5/5] style(sensitive): apply canonical rustfmt --- crates/originweave-policy/src/sensitive_data.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/originweave-policy/src/sensitive_data.rs b/crates/originweave-policy/src/sensitive_data.rs index 80a4b188..5d09b595 100644 --- a/crates/originweave-policy/src/sensitive_data.rs +++ b/crates/originweave-policy/src/sensitive_data.rs @@ -441,7 +441,8 @@ impl SensitiveHandleUseState { } if !authority.is_complete() || authority != self.scope.authority { HandleUseDecision::ScopeMismatch - } else if !authority_identifier_is_valid(audience_id) || audience_id != self.scope.audience_id + } else if !authority_identifier_is_valid(audience_id) + || audience_id != self.scope.audience_id { HandleUseDecision::AudienceMismatch } else if now_epoch_seconds >= self.scope.expires_at_epoch_seconds {