Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 3 additions & 3 deletions crates/originweave-policy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down
66 changes: 56 additions & 10 deletions crates/originweave-policy/src/sensitive_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 {
Expand Down Expand Up @@ -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<HandleRevocationReason>,
}

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,
}
}

Expand All @@ -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<HandleRevocationReason> {
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 {
Expand Down
53 changes: 51 additions & 2 deletions crates/originweave-policy/tests/sensitive_handle_reservation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
}
}
Loading