Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ All notable changes to OriginWeave are documented in this file. The format follo
- Active PR #170 adds conservative MCP `2026-07-28` `tools/list` discovery metadata derived from that protected-main catalog, with `resultType = complete`, zero freshness, private cache scope, no continuation cursor, per-request protocol/client-capability admission, and bounded protocol-version and method metadata validated before cross-field comparison. This remains active-PR evidence only and grants no browser, network, secret, approval, or Agent authority.
- Deterministic fail-closed policy evaluation for untrusted instructions, origin grants, crawler restrictions, execution-mode and purpose consistency, approvals, and brokered secrets.
- Fail-closed resolved-destination policy with IPv4/IPv6 special-purpose and reviewed cloud-platform endpoint classification, IPv4-mapped canonicalization, explicit class grants, non-empty origin-bound DNS snapshots capped at 256 resolver addresses, concrete connection pinning, DNS-set expansion detection, and per-hop redirect reauthorization.
- Bounded resolution-freshness authority with trusted monotonic approval time, capped non-zero validity, half-open use windows, non-expanding revalidation, and credential-free authorization timestamps.
- Direct-only `originweave-network` TCP boundary with explicit canonical `SocketAddr` authority, zero IPv6 flow and scope metadata unless separately modeled, a non-cloneable single-use plan, a 30-second per-attempt timeout ceiling, at most four attempts, exact `peer_addr` verification before stream exposure, and no hostname re-resolution or ambient proxy inheritance.
- Authenticated `originweave-tls` service-identity boundary that consumes an existing verified TCP stream, requires exact TLS-origin and transport-origin equality, derives RFC 9525 DNS or literal-IP reference identity only from the canonical HTTPS origin, validates WebPKI with explicit roots and fixed time, permits only TLS 1.2 and TLS 1.3, and never reconnects or resolves.
- Bounded TLS policy for total handshake time, ALPN identifiers, trust-root count and bytes, and server-presented certificate count and bytes, with explicit optional-versus-required ALPN behavior and `NotConfigured` revocation evidence.
Expand Down
3 changes: 2 additions & 1 deletion crates/originweave-destination/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ pub use redirect::{
RedirectTargetDigestError,
};
pub use resolution::{
ConnectionEvidence, DestinationError, DestinationPolicy, MAX_RESOLUTION_ADDRESS_COUNT,
ConnectionEvidence, DestinationError, DestinationPolicy, FreshConnectionEvidence,
FreshResolutionSnapshot, MAX_RESOLUTION_ADDRESS_COUNT, MAX_RESOLUTION_VALIDITY,
Comment thread
seonghobae marked this conversation as resolved.
ResolutionSnapshot,
};
236 changes: 236 additions & 0 deletions crates/originweave-destination/src/resolution.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::collections::BTreeSet;
use std::fmt;
use std::net::IpAddr;
use std::time::Duration;

use originweave_core::Origin;

Expand All @@ -9,6 +10,13 @@ use crate::{AddressClass, ClassifiedAddress, classify_address};
/// The largest resolver answer accepted by one resolution snapshot.
pub const MAX_RESOLUTION_ADDRESS_COUNT: usize = 256;

/// The largest freshness interval accepted for one resolution approval.
///
/// This is an OriginWeave product safety budget, not a DNS protocol validity
/// rule. Callers may choose any smaller non-zero interval appropriate to their
/// resolver and network adapter.
pub const MAX_RESOLUTION_VALIDITY: Duration = Duration::from_secs(30);

/// A fail-closed allow-list of destination address classes.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DestinationPolicy {
Expand Down Expand Up @@ -103,6 +111,34 @@ pub enum DestinationError {
/// The newly introduced canonical address.
address: IpAddr,
},
/// A freshness interval was zero or exceeded [`MAX_RESOLUTION_VALIDITY`].
InvalidResolutionValidity {
/// The rejected freshness interval.
validity: Duration,
/// The largest accepted freshness interval.
maximum_validity: Duration,
},
/// Adding the freshness interval to the approval time overflowed.
ResolutionValidityOverflow {
/// The trusted monotonic time at which the answer was approved.
approved_at: Duration,
/// The requested freshness interval.
validity: Duration,
},
/// A caller supplied a monotonic time earlier than the recorded approval.
ResolutionUseBeforeApproval {
/// The recorded approval time.
approved_at: Duration,
/// The caller-supplied current time.
current_time: Duration,
},
/// A bounded resolution approval reached its exclusive validity deadline.
ResolutionApprovalExpired {
/// The exclusive upper bound of the approval interval.
valid_until: Duration,
/// The caller-supplied current time.
current_time: Duration,
},
}

impl fmt::Display for DestinationError {
Expand Down Expand Up @@ -142,6 +178,34 @@ impl fmt::Display for DestinationError {
formatter,
"refreshed DNS answer introduced unapproved address {address}",
),
Self::InvalidResolutionValidity {
validity,
maximum_validity,
} => write!(
formatter,
"resolution validity {validity:?} is outside 1ns..={maximum_validity:?}",
),
Self::ResolutionValidityOverflow {
approved_at,
validity,
} => write!(
formatter,
"resolution validity {validity:?} overflows approval time {approved_at:?}",
),
Self::ResolutionUseBeforeApproval {
approved_at,
current_time,
} => write!(
formatter,
"resolution use time {current_time:?} precedes approval time {approved_at:?}",
),
Self::ResolutionApprovalExpired {
valid_until,
current_time,
} => write!(
formatter,
"resolution approval expired at {valid_until:?}; current time is {current_time:?}",
),
}
}
}
Expand Down Expand Up @@ -254,6 +318,143 @@ impl ResolutionSnapshot {
}
}

/// A resolution snapshot bound to one explicit trusted monotonic validity window.
///
/// The time values are opaque durations from one caller-owned monotonic clock
/// domain. This type never reads a wall clock itself. Constructing a new fresh
/// snapshot always reruns the same destination validation used by
/// [`ResolutionSnapshot`], so callers cannot renew authority without presenting
/// another policy-valid answer.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FreshResolutionSnapshot {
snapshot: ResolutionSnapshot,
approved_at: Duration,
validity: Duration,
valid_until: Duration,
}

impl FreshResolutionSnapshot {
/// Validate addresses and bind the resulting snapshot to a bounded lifetime.
pub fn approve(
origin: Origin,
addresses: impl IntoIterator<Item = IpAddr>,
policy: &DestinationPolicy,
approved_at: Duration,
validity: Duration,
) -> Result<Self, DestinationError> {
let snapshot = ResolutionSnapshot::approve(origin, addresses, policy)?;
Self::from_snapshot(snapshot, approved_at, validity)
}

fn from_snapshot(
snapshot: ResolutionSnapshot,
approved_at: Duration,
validity: Duration,
) -> Result<Self, DestinationError> {
if validity.is_zero() || validity > MAX_RESOLUTION_VALIDITY {
return Err(DestinationError::InvalidResolutionValidity {
validity,
maximum_validity: MAX_RESOLUTION_VALIDITY,
});
}
let Some(valid_until) = approved_at.checked_add(validity) else {
return Err(DestinationError::ResolutionValidityOverflow {
approved_at,
validity,
});
};
Ok(Self {
snapshot,
approved_at,
validity,
valid_until,
})
}

/// Return the logical origin whose DNS answer was approved.
#[must_use]
pub const fn origin(&self) -> &Origin {
self.snapshot.origin()
}

/// Return the canonical addresses pinned for this fresh snapshot.
#[must_use]
pub const fn addresses(&self) -> &BTreeSet<IpAddr> {
self.snapshot.addresses()
}

/// Return the trusted monotonic approval time.
#[must_use]
pub const fn approved_at(&self) -> Duration {
self.approved_at
}

/// Return the configured non-zero validity budget.
#[must_use]
pub const fn validity(&self) -> Duration {
self.validity
}

/// Return the exclusive upper bound of the approval interval.
#[must_use]
pub const fn valid_until(&self) -> Duration {
self.valid_until
}

/// Authorize one pinned address only while the freshness window is valid.
pub fn authorize_connection(
&self,
address: IpAddr,
current_time: Duration,
) -> Result<FreshConnectionEvidence, DestinationError> {
self.validate_current_time(current_time)?;
let connection = self.snapshot.authorize_connection(address)?;
Ok(FreshConnectionEvidence {
connection,
resolution_approved_at: self.approved_at,
resolution_valid_until: self.valid_until,
authorized_at: current_time,
})
}

/// Revalidate a fresh answer and renew the same bounded validity budget.
///
/// `revalidated_at` must come from the same monotonic clock domain and may
/// not precede this snapshot's approval time. Expansion of the pinned set
/// remains fail-closed under [`ResolutionSnapshot::revalidate`].
pub fn revalidate(
&self,
addresses: impl IntoIterator<Item = IpAddr>,
policy: &DestinationPolicy,
revalidated_at: Duration,
) -> Result<Self, DestinationError> {
if revalidated_at < self.approved_at {
return Err(DestinationError::ResolutionUseBeforeApproval {
approved_at: self.approved_at,
current_time: revalidated_at,
});
}
let snapshot = self.snapshot.revalidate(addresses, policy)?;
Self::from_snapshot(snapshot, revalidated_at, self.validity)
}
Comment thread
seonghobae marked this conversation as resolved.

fn validate_current_time(&self, current_time: Duration) -> Result<(), DestinationError> {
if current_time < self.approved_at {
return Err(DestinationError::ResolutionUseBeforeApproval {
approved_at: self.approved_at,
current_time,
});
}
if current_time >= self.valid_until {
return Err(DestinationError::ResolutionApprovalExpired {
valid_until: self.valid_until,
current_time,
});
}
Ok(())
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum OriginHostConstraint {
Domain,
Expand Down Expand Up @@ -344,3 +545,38 @@ impl ConnectionEvidence {
self.address_class
}
}

/// Credential-free evidence that a pinned connection address was used while fresh.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FreshConnectionEvidence {
connection: ConnectionEvidence,
resolution_approved_at: Duration,
resolution_valid_until: Duration,
authorized_at: Duration,
}

impl FreshConnectionEvidence {
/// Return the underlying canonical destination/connection evidence.
#[must_use]
pub const fn connection_evidence(&self) -> &ConnectionEvidence {
&self.connection
}

/// Return the trusted monotonic time at which the answer was approved.
#[must_use]
pub const fn resolution_approved_at(&self) -> Duration {
self.resolution_approved_at
}

/// Return the exclusive upper bound of the resolution approval interval.
#[must_use]
pub const fn resolution_valid_until(&self) -> Duration {
self.resolution_valid_until
}

/// Return the trusted monotonic time used for this authorization decision.
#[must_use]
pub const fn authorized_at(&self) -> Duration {
self.authorized_at
}
}
Loading
Loading