From 219bfdc95facadfe034a371badc14326e169c627 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 03:06:15 +0900 Subject: [PATCH 1/4] test(sensitive): require authoritative handle revocation --- .../tests/sensitive_handle_reservation.rs | 53 ++++++++++++++++++- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/crates/originweave-policy/tests/sensitive_handle_reservation.rs b/crates/originweave-policy/tests/sensitive_handle_reservation.rs index 6e5ce0e1..6dd0439f 100644 --- a/crates/originweave-policy/tests/sensitive_handle_reservation.rs +++ b/crates/originweave-policy/tests/sensitive_handle_reservation.rs @@ -2,8 +2,8 @@ use originweave_core::Origin; use originweave_policy::{ - DataClassification, HandleUseDecision, SensitiveDataAuthority, SensitiveHandleUseState, - SensitiveValueHandleScope, + DataClassification, HandleRevocationReason, HandleUseDecision, SensitiveDataAuthority, + SensitiveHandleUseState, SensitiveValueHandleScope, }; const TENANT: &str = "tenant_alpha"; @@ -75,3 +75,52 @@ fn zero_use_scope_never_reserves_or_wraps_the_counter() { ); assert_eq!(state.reserved_uses(), 0); } + +#[test] +fn revocation_is_authoritative_idempotent_and_blocks_future_use() { + let mut state = SensitiveHandleUseState::new(scope(3)); + + assert_eq!(state.revocation_reason(), None); + assert_eq!( + state.reserve_use(authority(DESTINATION), 1_999), + HandleUseDecision::Authorized + ); + assert_eq!(state.reserved_uses(), 1); + + assert!(state.revoke(HandleRevocationReason::TaskCompleted)); + assert_eq!( + state.revocation_reason(), + Some(HandleRevocationReason::TaskCompleted) + ); + assert_eq!( + state.reserve_use(authority(DESTINATION), 1_999), + HandleUseDecision::Revoked + ); + assert_eq!(state.reserved_uses(), 1); + + assert!(!state.revoke(HandleRevocationReason::PolicyChanged)); + assert_eq!( + state.revocation_reason(), + Some(HandleRevocationReason::TaskCompleted) + ); +} + +#[test] +fn every_required_revocation_cause_can_be_recorded() { + for reason in [ + HandleRevocationReason::TaskCompleted, + HandleRevocationReason::PolicyChanged, + HandleRevocationReason::KeyRotated, + HandleRevocationReason::SessionTerminated, + HandleRevocationReason::SuspiciousUse, + ] { + let mut state = SensitiveHandleUseState::new(scope(1)); + assert!(state.revoke(reason)); + assert_eq!(state.revocation_reason(), Some(reason)); + assert_eq!( + state.reserve_use(authority(DESTINATION), 1_999), + HandleUseDecision::Revoked + ); + assert_eq!(state.reserved_uses(), 0); + } +} From 2e760a661ef662623161f764713be901dbd3600b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 03:10:33 +0900 Subject: [PATCH 2/4] feat(sensitive): enforce authoritative handle revocation --- .../originweave-policy/src/sensitive_data.rs | 66 ++++++++++++++++--- 1 file changed, 56 insertions(+), 10 deletions(-) diff --git a/crates/originweave-policy/src/sensitive_data.rs b/crates/originweave-policy/src/sensitive_data.rs index b49b3c37..d5bcbb44 100644 --- a/crates/originweave-policy/src/sensitive_data.rs +++ b/crates/originweave-policy/src/sensitive_data.rs @@ -159,6 +159,8 @@ pub fn evaluate_disclosure( pub enum HandleUseDecision { /// The supplied exact scope, classification, expiry, and prior-use count permit broker admission. Authorized, + /// The authoritative in-process handle state was revoked before this use. + Revoked, /// Tenant, task, field, purpose, destination, or classification did not match the handle scope. ScopeMismatch, /// The handle is no longer valid at the supplied trusted time. @@ -167,6 +169,25 @@ pub enum HandleUseDecision { UseLimitReached, } +/// Reason that authoritative in-process handle state was revoked. +/// +/// The reason is credential-free policy metadata. The first successful +/// revocation is retained so a later duplicate transition cannot rewrite the +/// original lifecycle cause. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HandleRevocationReason { + /// The delegated task completed and no further disclosure is permitted. + TaskCompleted, + /// A relevant authorization or disclosure policy changed. + PolicyChanged, + /// Key rotation invalidated the handle lifecycle controlled by the broker. + KeyRotated, + /// The task or browser session terminated. + SessionTerminated, + /// Security monitoring identified suspicious handle use. + SuspiciousUse, +} + /// Authority metadata attached to an opaque sensitive-value handle. #[derive(Debug, Clone, PartialEq, Eq)] pub struct SensitiveValueHandleScope { @@ -222,32 +243,34 @@ impl HandleUseRequest { } } -/// In-process authoritative use-count state for one opaque sensitive-value handle scope. +/// In-process authoritative use-count and revocation state for one opaque sensitive-value handle scope. /// /// This value removes the caller-supplied prior-use count from the reservation /// operation. A successful reservation compares the exact authority, trusted -/// time, expiry, and current count and then increments the count while the caller -/// holds an exclusive mutable borrow of this state. Denied reservations never -/// consume a use. +/// time, expiry, revocation state, and current count and then increments the +/// count while the caller holds an exclusive mutable borrow of this state. +/// Denied reservations never consume a use. /// /// This is a policy-state primitive, not the trusted broker itself. It contains /// neither the opaque handle token nor protected data and provides no durable or -/// cross-process transaction, revocation, value resolution, compensation, or -/// persistence. A shared or durable broker must place the state behind its own -/// transactional/locking boundary and recheck lifecycle state before disclosure. +/// cross-process transaction, value resolution, compensation, or persistence. A +/// shared or durable broker must place the state behind its own transactional or +/// locking boundary, persist lifecycle state, and recheck it before disclosure. #[derive(Debug, PartialEq, Eq)] pub struct SensitiveHandleUseState { scope: SensitiveValueHandleScope, reserved_uses: u32, + revocation_reason: Option, } impl SensitiveHandleUseState { - /// Start authoritative in-process reservation state with no uses consumed. + /// Start authoritative in-process reservation state with no uses consumed or revocation recorded. #[must_use] pub const fn new(scope: SensitiveValueHandleScope) -> Self { Self { scope, reserved_uses: 0, + revocation_reason: None, } } @@ -257,15 +280,38 @@ impl SensitiveHandleUseState { self.reserved_uses } + /// Return the first authoritative revocation reason, if this state was revoked. + #[must_use] + pub const fn revocation_reason(&self) -> Option { + self.revocation_reason + } + + /// Revoke future reservations and retain the first lifecycle reason. + /// + /// Returns `true` only for the state transition from active to revoked. A + /// later duplicate call is a no-op and cannot rewrite the original reason. + pub fn revoke(&mut self, reason: HandleRevocationReason) -> bool { + if self.revocation_reason.is_some() { + false + } else { + self.revocation_reason = Some(reason); + true + } + } + /// Reserve one use from the current authoritative count when policy permits it. /// - /// The supplied time must come from the trusted broker boundary. Exact-scope, - /// expiry, and use-limit denial leaves the authoritative count unchanged. + /// The supplied time must come from the trusted broker boundary. Revocation, + /// exact-scope, expiry, and use-limit denial leaves the authoritative count + /// unchanged. pub fn reserve_use( &mut self, authority: SensitiveDataAuthority, now_epoch_seconds: u64, ) -> HandleUseDecision { + if self.revocation_reason.is_some() { + return HandleUseDecision::Revoked; + } let request = HandleUseRequest::new(authority, now_epoch_seconds, self.reserved_uses); let decision = evaluate_handle_use(&request, &self.scope); if decision == HandleUseDecision::Authorized { From 0bea0382e1c89fceb26820e39590754ad37999a8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 03:11:07 +0900 Subject: [PATCH 3/4] feat(sensitive): export handle revocation authority --- crates/originweave-policy/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/originweave-policy/src/lib.rs b/crates/originweave-policy/src/lib.rs index 4e795c50..8cab72bb 100644 --- a/crates/originweave-policy/src/lib.rs +++ b/crates/originweave-policy/src/lib.rs @@ -10,9 +10,9 @@ mod sensitive_data; pub use sensitive_data::{ - DataClassification, DisclosureDecision, DisclosureScope, HandleUseDecision, HandleUseRequest, - SensitiveDataAuthority, SensitiveDataRequest, SensitiveHandleUseState, - SensitiveValueHandleScope, evaluate_disclosure, evaluate_handle_use, + DataClassification, DisclosureDecision, DisclosureScope, HandleRevocationReason, + HandleUseDecision, HandleUseRequest, SensitiveDataAuthority, SensitiveDataRequest, + SensitiveHandleUseState, SensitiveValueHandleScope, evaluate_disclosure, evaluate_handle_use, }; use originweave_core::{ From 86ce4bc1c11c270dc532593d673c42bd6f623d74 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 03:16:19 +0900 Subject: [PATCH 4/4] docs(changelog): record sensitive handle revocation --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cd00929..09c1db80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Added -- In-process authoritative sensitive-handle use reservation state that owns the current bounded use count, increments only after exact scope/classification/expiry/use-limit authorization, and leaves denied reservations unconsumed; this is a policy primitive only and does not claim durable broker storage, protected-value resolution, revocation, or cross-process transactionality. +- In-process authoritative sensitive-handle use reservation and first-revocation-wins lifecycle state that owns the bounded use count, records task-completion/policy-change/key-rotation/session-termination/suspicious-use revocation causes, blocks all future reservations after revocation, increments only after exact scope/classification/expiry/use-limit authorization, and leaves denied reservations unconsumed; this is a policy primitive only and does not claim durable broker storage, protected-value resolution, or cross-process transactionality. - Rust workspace for independently reusable core, policy, destination, network, TLS, resource, and evidence modules. - Canonical HTTPS and loopback-origin boundary with case-normalized schemes and hosts, default-port normalization, IPv4/IPv6 handling, browser-special numeric-host rejection, and explicit malformed-input errors. - Typed browser actions, capabilities, risk classes, execution modes, robots decisions, secret-delivery contracts, immutable canonical action-intent digests, and intent-bound approval scopes.