fix(auth): seal authoritative adapter boundary

Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com>
This commit is contained in:
Cea Stapleton Cordasco
2026-08-03 16:23:30 -05:00
parent 8ea116a5d0
commit 5a014bda25
6 changed files with 1075 additions and 39 deletions
+621
View File
@@ -0,0 +1,621 @@
use std::{fmt, future::Future, pin::Pin};
use buzz_core::CommunityId;
use nostr::PublicKey;
use uuid::Uuid;
use super::{
AuthContextError, AuthoritativeBindingEvidence, AuthoritativeBindingResolution, BindingExpiry,
BindingSource, BindingVersion, EnrollmentMode, FederatedIdentityRequirement,
FederatedPolicyStamp, FederatedPrincipal, ResolvedFederatedPolicy,
};
/// Boxed asynchronous result returned by a federated authority adapter.
pub type AuthorityAdapterFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
/// Failure while invoking or validating a federated authority adapter.
#[derive(Debug, PartialEq, Eq)]
pub enum AuthorityAdapterError<E> {
/// The storage adapter failed before producing authoritative state.
Adapter(E),
/// Adapter output violated the authorization contract.
Contract(AuthContextError),
/// Current policy no longer matches the atomic binding precondition.
PolicyChanged,
}
impl<E> AuthorityAdapterError<E> {
/// Wrap a storage-adapter failure.
pub const fn adapter(error: E) -> Self {
Self::Adapter(error)
}
/// Report that the policy identifier or epoch changed before binding resolution.
pub const fn policy_changed() -> Self {
Self::PolicyChanged
}
}
impl<E> From<AuthContextError> for AuthorityAdapterError<E> {
fn from(error: AuthContextError) -> Self {
Self::Contract(error)
}
}
/// Read-only request for the current enrollment policy of one authorization domain.
#[derive(Clone, PartialEq, Eq)]
pub struct CurrentPolicyRequest {
authorization_domain: CommunityId,
correlation_id: Uuid,
observed_at: u64,
}
impl CurrentPolicyRequest {
/// Server-resolved authorization domain to read.
pub const fn authorization_domain(&self) -> CommunityId {
self.authorization_domain
}
/// Correlation identifier for the decision being assembled.
pub const fn correlation_id(&self) -> Uuid {
self.correlation_id
}
/// Trusted server time for the policy read.
pub const fn observed_at(&self) -> u64 {
self.observed_at
}
}
impl fmt::Debug for CurrentPolicyRequest {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("CurrentPolicyRequest")
.field("authorization_domain", &"[redacted]")
.field("correlation_id", &"[redacted]")
.field("observed_at", &"[redacted]")
.finish()
}
}
/// Crate-owned capability for sealing one current policy read.
///
/// The adapter receives this value from [`resolve_current_federated_policy`];
/// downstream callers cannot construct it. Calling [`Self::resolved`] validates
/// the raw storage fields and returns an opaque policy value.
pub struct CurrentPolicyResolutionSink {
request: CurrentPolicyRequest,
}
impl CurrentPolicyResolutionSink {
/// Validate and seal current policy fields read by the adapter.
#[allow(clippy::too_many_arguments)]
pub fn resolved(
self,
authorization_domain: CommunityId,
policy_id: Uuid,
epoch: u64,
requirement: FederatedIdentityRequirement,
effective_from: u64,
effective_until: u64,
) -> Result<ResolvedFederatedPolicy, AuthContextError> {
if authorization_domain != self.request.authorization_domain {
return Err(AuthContextError::PolicyDomainMismatch);
}
let stamp = FederatedPolicyStamp::from_authoritative_state(
authorization_domain,
policy_id,
epoch,
self.request.correlation_id,
requirement,
effective_from,
effective_until,
)?;
if stamp.is_not_yet_effective_at(self.request.observed_at) {
return Err(AuthContextError::FederatedPolicyNotYetEffective);
}
if stamp.is_expired_at(self.request.observed_at) {
return Err(AuthContextError::FederatedPolicyExpired);
}
Ok(ResolvedFederatedPolicy::from_authoritative_resolution(
stamp,
))
}
}
impl fmt::Debug for CurrentPolicyResolutionSink {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_tuple("CurrentPolicyResolutionSink")
.field(&"[redacted]")
.finish()
}
}
/// Atomic binding request tied to an exact current enrollment-policy epoch.
#[derive(Clone, PartialEq, Eq)]
pub struct BindingResolutionRequest {
authorization_domain: CommunityId,
principal: FederatedPrincipal,
bound_pubkey: PublicKey,
policy_id: Uuid,
policy_epoch: u64,
policy_requirement: FederatedIdentityRequirement,
correlation_id: Uuid,
key_attested: bool,
effective_from: u64,
effective_until: u64,
observed_at: u64,
}
impl BindingResolutionRequest {
/// Server-resolved authorization domain.
pub const fn authorization_domain(&self) -> CommunityId {
self.authorization_domain
}
/// Exact issuer-qualified principal being resolved.
pub const fn principal(&self) -> &FederatedPrincipal {
&self.principal
}
/// Authenticated Nostr key being resolved.
pub const fn bound_pubkey(&self) -> PublicKey {
self.bound_pubkey
}
/// Stable current enrollment-policy identifier.
pub const fn policy_id(&self) -> Uuid {
self.policy_id
}
/// Exact policy epoch that must still be current inside the binding transaction.
pub const fn policy_epoch(&self) -> u64 {
self.policy_epoch
}
/// Enrollment requirement at the expected policy epoch.
pub const fn policy_requirement(&self) -> FederatedIdentityRequirement {
self.policy_requirement
}
/// Correlation identifier for this decision.
pub const fn correlation_id(&self) -> Uuid {
self.correlation_id
}
/// Whether verifier-owned assertion evidence attested the exact bound key.
pub const fn key_attested(&self) -> bool {
self.key_attested
}
/// Inclusive joined assertion, capability, and policy validity bound.
pub const fn effective_from(&self) -> u64 {
self.effective_from
}
/// Exclusive joined assertion, capability, and policy validity bound.
pub const fn effective_until(&self) -> u64 {
self.effective_until
}
/// Trusted server time for binding eligibility.
pub const fn observed_at(&self) -> u64 {
self.observed_at
}
}
impl fmt::Debug for BindingResolutionRequest {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("BindingResolutionRequest")
.field("authorization_domain", &"[redacted]")
.field("principal", &"[redacted]")
.field("bound_pubkey", &"[redacted]")
.field("policy_id", &"[redacted]")
.field("policy_epoch", &"[redacted]")
.field("policy_requirement", &"[redacted]")
.field("correlation_id", &"[redacted]")
.field("key_attested", &"[redacted]")
.field("effective_from", &"[redacted]")
.field("effective_until", &"[redacted]")
.field("observed_at", &"[redacted]")
.finish()
}
}
#[derive(Clone)]
struct BindingExpectation {
request: BindingResolutionRequest,
}
impl BindingExpectation {
#[allow(clippy::too_many_arguments)]
fn evidence(
self,
authorization_domain: CommunityId,
binding_id: Uuid,
principal: FederatedPrincipal,
bound_pubkey: PublicKey,
binding_version: BindingVersion,
expires_at: Option<BindingExpiry>,
source: BindingSource,
) -> Result<AuthoritativeBindingEvidence, AuthContextError> {
if authorization_domain != self.request.authorization_domain {
return Err(AuthContextError::BindingDomainMismatch);
}
if principal != self.request.principal {
return Err(AuthContextError::AssertionPrincipalMismatch);
}
if bound_pubkey != self.request.bound_pubkey {
return Err(AuthContextError::DirectBindingKeyMismatch);
}
if expires_at.is_some_and(|bound| bound.is_expired_at(self.request.observed_at)) {
return Err(AuthContextError::BindingExpired);
}
AuthoritativeBindingEvidence::new(
authorization_domain,
binding_id,
principal,
bound_pubkey,
binding_version,
expires_at,
source,
)
}
}
/// Crate-owned capability for sealing direct binding state.
///
/// Implementations may call [`Self::existing_active`] after a current active
/// read, or [`Self::atomically_enrolled`] only after enrollment commits in the
/// same transaction that compared the request's policy identifier and epoch.
pub struct DirectBindingResolutionSink {
expected: BindingExpectation,
}
impl DirectBindingResolutionSink {
/// Seal an already-active binding returned by authoritative storage.
#[allow(clippy::too_many_arguments)]
pub fn existing_active(
self,
authorization_domain: CommunityId,
binding_id: Uuid,
principal: FederatedPrincipal,
bound_pubkey: PublicKey,
binding_version: BindingVersion,
expires_at: Option<BindingExpiry>,
source: BindingSource,
) -> Result<AuthoritativeBindingResolution, AuthContextError> {
self.expected
.evidence(
authorization_domain,
binding_id,
principal,
bound_pubkey,
binding_version,
expires_at,
source,
)
.map(AuthoritativeBindingResolution::existing_active)
}
/// Seal a binding created under the request's atomic policy precondition.
#[allow(clippy::too_many_arguments)]
pub fn atomically_enrolled(
self,
authorization_domain: CommunityId,
binding_id: Uuid,
principal: FederatedPrincipal,
bound_pubkey: PublicKey,
binding_version: BindingVersion,
expires_at: Option<BindingExpiry>,
source: BindingSource,
) -> Result<AuthoritativeBindingResolution, AuthContextError> {
match self.expected.request.policy_requirement {
FederatedIdentityRequirement::Required(EnrollmentMode::AttestedKey)
if !self.expected.request.key_attested =>
{
return Err(AuthContextError::KeyAttestationRequired);
}
FederatedIdentityRequirement::Required(
EnrollmentMode::AttestedKey | EnrollmentMode::Tofu,
) => {}
FederatedIdentityRequirement::NotRequired
| FederatedIdentityRequirement::Required(EnrollmentMode::Provisioned) => {
return Err(AuthContextError::InvalidAuthorizationReason);
}
}
self.expected
.evidence(
authorization_domain,
binding_id,
principal,
bound_pubkey,
binding_version,
expires_at,
source,
)
.map(AuthoritativeBindingResolution::atomically_enrolled)
}
}
impl fmt::Debug for DirectBindingResolutionSink {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_tuple("DirectBindingResolutionSink")
.field(&"[redacted]")
.finish()
}
}
/// Crate-owned capability for sealing a read-only existing owner binding.
///
/// This sink intentionally has no enrollment method, so delegated-owner
/// resolution cannot create or relabel a binding.
pub struct ExistingBindingResolutionSink {
expected: BindingExpectation,
}
impl ExistingBindingResolutionSink {
/// Seal an already-active owner binding returned by authoritative storage.
#[allow(clippy::too_many_arguments)]
pub fn existing_active(
self,
authorization_domain: CommunityId,
binding_id: Uuid,
principal: FederatedPrincipal,
bound_pubkey: PublicKey,
binding_version: BindingVersion,
expires_at: Option<BindingExpiry>,
source: BindingSource,
) -> Result<AuthoritativeBindingResolution, AuthContextError> {
self.expected
.evidence(
authorization_domain,
binding_id,
principal,
bound_pubkey,
binding_version,
expires_at,
source,
)
.map(AuthoritativeBindingResolution::existing_active)
}
}
impl fmt::Debug for ExistingBindingResolutionSink {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_tuple("ExistingBindingResolutionSink")
.field(&"[redacted]")
.finish()
}
}
/// Trusted cross-crate adapter for current policy and binding state.
///
/// The server must compose exactly one implementation backed by authoritative
/// storage; request and transport code must never select an implementation.
/// Binding methods must compare `policy_id` and `policy_epoch` and check
/// database time against `[effective_from, effective_until)` after lock
/// acquisition and immediately before commit, inside the same transaction as
/// the active read or enrollment. A mismatch or elapsed interval fails closed
/// without binding mutation; policy mismatch is
/// [`AuthorityAdapterError::PolicyChanged`].
pub trait FederatedAuthorityAdapter: Send + Sync {
/// Storage-specific failure type.
type Error;
/// Read the domain's current enrollment policy and seal it with `sink`.
fn resolve_current_policy<'a>(
&'a self,
request: CurrentPolicyRequest,
sink: CurrentPolicyResolutionSink,
) -> AuthorityAdapterFuture<
'a,
Result<ResolvedFederatedPolicy, AuthorityAdapterError<Self::Error>>,
>;
/// Resolve or atomically enroll a direct binding under the exact policy precondition.
fn resolve_direct_binding<'a>(
&'a self,
request: BindingResolutionRequest,
sink: DirectBindingResolutionSink,
) -> AuthorityAdapterFuture<
'a,
Result<AuthoritativeBindingResolution, AuthorityAdapterError<Self::Error>>,
>;
/// Resolve an already-active owner binding without enrollment or mutation.
fn resolve_existing_binding<'a>(
&'a self,
request: BindingResolutionRequest,
sink: ExistingBindingResolutionSink,
) -> AuthorityAdapterFuture<
'a,
Result<AuthoritativeBindingResolution, AuthorityAdapterError<Self::Error>>,
>;
}
/// Resolve and seal the current policy for one authorization decision.
pub async fn resolve_current_federated_policy<A: FederatedAuthorityAdapter + ?Sized>(
adapter: &A,
authorization_domain: CommunityId,
correlation_id: Uuid,
now_unix_seconds: u64,
) -> Result<ResolvedFederatedPolicy, AuthorityAdapterError<A::Error>> {
let request = CurrentPolicyRequest {
authorization_domain,
correlation_id,
observed_at: now_unix_seconds,
};
let sink = CurrentPolicyResolutionSink {
request: request.clone(),
};
let policy = adapter.resolve_current_policy(request, sink).await?;
validate_returned_policy(
&policy,
authorization_domain,
correlation_id,
now_unix_seconds,
)?;
Ok(policy)
}
/// Resolve or atomically enroll a direct binding under an exact current policy.
#[allow(dead_code)]
// Keep the verifier-derived attestation bit and joined interval explicit at
// this sealed boundary so storage adapters cannot infer or widen either fact.
#[allow(clippy::too_many_arguments)]
pub(crate) async fn resolve_direct_binding<A: FederatedAuthorityAdapter + ?Sized>(
adapter: &A,
policy: &ResolvedFederatedPolicy,
principal: FederatedPrincipal,
bound_pubkey: PublicKey,
key_attested: bool,
effective_from: u64,
effective_until: u64,
now_unix_seconds: u64,
) -> Result<AuthoritativeBindingResolution, AuthorityAdapterError<A::Error>> {
let request = binding_request(
policy,
principal,
bound_pubkey,
key_attested,
effective_from,
effective_until,
now_unix_seconds,
)?;
let sink = DirectBindingResolutionSink {
expected: BindingExpectation {
request: request.clone(),
},
};
let resolution = adapter
.resolve_direct_binding(request.clone(), sink)
.await?;
validate_returned_binding(&resolution, &request, false)?;
Ok(resolution)
}
/// Resolve an already-active owner binding under an exact current policy.
#[allow(dead_code)]
pub(crate) async fn resolve_existing_binding<A: FederatedAuthorityAdapter + ?Sized>(
adapter: &A,
policy: &ResolvedFederatedPolicy,
principal: FederatedPrincipal,
bound_pubkey: PublicKey,
effective_from: u64,
effective_until: u64,
now_unix_seconds: u64,
) -> Result<AuthoritativeBindingResolution, AuthorityAdapterError<A::Error>> {
let request = binding_request(
policy,
principal,
bound_pubkey,
false,
effective_from,
effective_until,
now_unix_seconds,
)?;
let sink = ExistingBindingResolutionSink {
expected: BindingExpectation {
request: request.clone(),
},
};
let resolution = adapter
.resolve_existing_binding(request.clone(), sink)
.await?;
validate_returned_binding(&resolution, &request, true)?;
Ok(resolution)
}
fn binding_request<E>(
policy: &ResolvedFederatedPolicy,
principal: FederatedPrincipal,
bound_pubkey: PublicKey,
key_attested: bool,
effective_from: u64,
effective_until: u64,
now_unix_seconds: u64,
) -> Result<BindingResolutionRequest, AuthorityAdapterError<E>> {
if policy.stamp().is_not_yet_effective_at(now_unix_seconds) {
return Err(AuthContextError::FederatedPolicyNotYetEffective.into());
}
if policy.stamp().is_expired_at(now_unix_seconds) {
return Err(AuthContextError::FederatedPolicyExpired.into());
}
if effective_from < policy.stamp().effective_from()
|| effective_until > policy.stamp().effective_until()
|| effective_from >= effective_until
{
return Err(AuthContextError::InvalidFederatedPolicyInterval.into());
}
if now_unix_seconds < effective_from {
return Err(AuthContextError::FederatedPolicyNotYetEffective.into());
}
if now_unix_seconds >= effective_until {
return Err(AuthContextError::FederatedPolicyExpired.into());
}
Ok(BindingResolutionRequest {
authorization_domain: policy.authorization_domain(),
principal,
bound_pubkey,
policy_id: policy.stamp().policy_id(),
policy_epoch: policy.stamp().epoch(),
policy_requirement: policy.requirement(),
correlation_id: policy.stamp().correlation_id(),
key_attested,
effective_from,
effective_until,
observed_at: now_unix_seconds,
})
}
fn validate_returned_policy<E>(
policy: &ResolvedFederatedPolicy,
authorization_domain: CommunityId,
correlation_id: Uuid,
now_unix_seconds: u64,
) -> Result<(), AuthorityAdapterError<E>> {
if policy.authorization_domain() != authorization_domain {
return Err(AuthContextError::PolicyDomainMismatch.into());
}
if policy.stamp().correlation_id() != correlation_id {
return Err(AuthContextError::FederatedPolicyCorrelationMismatch.into());
}
if policy.stamp().is_not_yet_effective_at(now_unix_seconds) {
return Err(AuthContextError::FederatedPolicyNotYetEffective.into());
}
if policy.stamp().is_expired_at(now_unix_seconds) {
return Err(AuthContextError::FederatedPolicyExpired.into());
}
Ok(())
}
fn validate_returned_binding<E>(
resolution: &AuthoritativeBindingResolution,
request: &BindingResolutionRequest,
require_existing: bool,
) -> Result<(), AuthorityAdapterError<E>> {
if require_existing && !resolution.is_existing_active() {
return Err(AuthContextError::DelegatedBindingNotExistingActive.into());
}
if resolution.authorization_domain() != request.authorization_domain {
return Err(AuthContextError::BindingDomainMismatch.into());
}
if resolution.principal() != &request.principal {
return Err(AuthContextError::AssertionPrincipalMismatch.into());
}
if resolution.bound_pubkey() != request.bound_pubkey {
return Err(AuthContextError::DirectBindingKeyMismatch.into());
}
if resolution
.expires_at()
.is_some_and(|bound| bound.is_expired_at(request.observed_at))
{
return Err(AuthContextError::BindingExpired.into());
}
Ok(())
}
+72 -21
View File
@@ -48,8 +48,8 @@ impl fmt::Debug for FederatedIdentityRequirement {
///
/// This stamp is not provider capability-policy evidence. It names the
/// server-owned federated enrollment policy that supplied the requirement and
/// its half-open effective interval. The constructor validates shape, while the
/// O3 authority adapter remains responsible for sourcing current policy state.
/// its half-open effective interval. The constructor validates shape, while a
/// crate-owned authority adapter remains responsible for sourcing current policy state.
#[derive(Clone, PartialEq, Eq)]
pub struct FederatedPolicyStamp {
authorization_domain: CommunityId,
@@ -62,12 +62,12 @@ pub struct FederatedPolicyStamp {
}
impl FederatedPolicyStamp {
/// Validate lineage read from current authoritative O3 policy state.
/// Validate lineage read from current authoritative policy state.
///
/// This constructor enforces structural invariants only. Callers must not
/// source any field from transport input, and O3 must compare the epoch as
/// an atomic precondition before enrollment.
pub fn from_authoritative_state(
/// source any field from transport input, and the authority adapter must
/// compare the epoch as an atomic precondition before enrollment.
pub(crate) fn from_authoritative_state(
authorization_domain: CommunityId,
policy_id: Uuid,
epoch: u64,
@@ -181,8 +181,8 @@ impl fmt::Debug for ResolvedFederatedPolicy {
}
impl ResolvedFederatedPolicy {
/// Seal structurally validated current O3 policy lineage for finalization.
pub const fn from_authoritative_resolution(stamp: FederatedPolicyStamp) -> Self {
/// Seal structurally validated current policy lineage for finalization.
pub(crate) const fn from_authoritative_resolution(stamp: FederatedPolicyStamp) -> Self {
Self { stamp }
}
@@ -235,6 +235,11 @@ impl ResolvedFederatedPolicy {
pub const fn stamp(&self) -> &FederatedPolicyStamp {
&self.stamp
}
#[allow(dead_code)]
pub(crate) fn into_stamp(self) -> FederatedPolicyStamp {
self.stamp
}
}
/// Provenance recorded when a binding is created.
@@ -347,14 +352,14 @@ pub struct VersionedBindingRef {
resolution_reason: AuthorizationReason,
}
/// Structurally validated binding fields returned by authoritative O3 state.
/// Structurally validated binding fields returned by authoritative state.
///
/// This is not authorization by itself. The crate-owned finalizer additionally
/// requires a typed lifecycle outcome proving that the binding was already
/// active or was atomically enrolled during this decision. It has no default or
/// deserialization path.
#[derive(PartialEq, Eq)]
pub struct AuthoritativeBindingEvidence {
pub(crate) struct AuthoritativeBindingEvidence {
authorization_domain: CommunityId,
binding_id: Uuid,
principal: FederatedPrincipal,
@@ -367,7 +372,7 @@ pub struct AuthoritativeBindingEvidence {
impl AuthoritativeBindingEvidence {
/// Validate typed fields read from authoritative binding state.
#[allow(clippy::too_many_arguments)]
pub fn new(
pub(crate) fn new(
authorization_domain: CommunityId,
binding_id: Uuid,
principal: FederatedPrincipal,
@@ -447,9 +452,9 @@ enum BindingResolutionOutcome {
AtomicallyEnrolled,
}
/// Typed authoritative lifecycle result consumed by the O1 finalizer.
/// Typed authoritative lifecycle result consumed by the crate-owned finalizer.
///
/// It carries no caller-selected authorization reason; O1 derives that reason
/// It carries no caller-selected authorization reason; the finalizer derives that reason
/// from the lifecycle outcome, persisted provenance, and current enrollment
/// policy.
#[derive(PartialEq, Eq)]
@@ -459,21 +464,61 @@ pub struct AuthoritativeBindingResolution {
}
impl AuthoritativeBindingResolution {
/// Record O3's authoritative result that the binding already existed.
pub fn existing_active(evidence: AuthoritativeBindingEvidence) -> Self {
/// Record the authoritative result that the binding already existed.
pub(crate) fn existing_active(evidence: AuthoritativeBindingEvidence) -> Self {
Self {
evidence,
outcome: BindingResolutionOutcome::ExistingActive,
}
}
/// Record O3's authoritative result that enrollment committed atomically.
pub fn atomically_enrolled(evidence: AuthoritativeBindingEvidence) -> Self {
/// Record the authoritative result that enrollment committed atomically.
pub(crate) fn atomically_enrolled(evidence: AuthoritativeBindingEvidence) -> Self {
Self {
evidence,
outcome: BindingResolutionOutcome::AtomicallyEnrolled,
}
}
/// Whether authoritative storage resolved an already-active binding.
pub const fn is_existing_active(&self) -> bool {
matches!(self.outcome, BindingResolutionOutcome::ExistingActive)
}
/// Server-resolved authorization domain that owns the binding.
pub const fn authorization_domain(&self) -> CommunityId {
self.evidence.authorization_domain()
}
/// Stable binding identifier.
pub const fn binding_id(&self) -> Uuid {
self.evidence.binding_id()
}
/// Issuer-qualified principal represented by the binding.
pub const fn principal(&self) -> &FederatedPrincipal {
self.evidence.principal()
}
/// Nostr key owned by the binding.
pub const fn bound_pubkey(&self) -> PublicKey {
self.evidence.bound_pubkey()
}
/// Current local binding version.
pub const fn binding_version(&self) -> BindingVersion {
self.evidence.binding_version()
}
/// Optional authoritative temporal bound for authorization eligibility.
pub const fn expires_at(&self) -> Option<BindingExpiry> {
self.evidence.expires_at()
}
/// Persisted provenance of the active binding.
pub const fn source(&self) -> BindingSource {
self.evidence.source()
}
}
impl fmt::Debug for AuthoritativeBindingResolution {
@@ -528,10 +573,16 @@ impl VersionedBindingRef {
))
}
pub(super) fn from_existing_authoritative_evidence(
evidence: AuthoritativeBindingEvidence,
) -> Self {
Self::from_authoritative_evidence(evidence, AuthorizationReason::ExistingBinding)
pub(super) fn from_existing_authoritative_resolution(
resolution: AuthoritativeBindingResolution,
) -> Result<Self, AuthContextError> {
if !resolution.is_existing_active() {
return Err(AuthContextError::DelegatedBindingNotExistingActive);
}
Ok(Self::from_authoritative_evidence(
resolution.evidence,
AuthorizationReason::ExistingBinding,
))
}
fn from_authoritative_evidence(
+82 -14
View File
@@ -13,14 +13,21 @@ use uuid::Uuid;
use crate::Scope;
pub(crate) mod authority;
mod binding;
mod evidence;
mod reason;
pub use authority::{
resolve_current_federated_policy, AuthorityAdapterError, AuthorityAdapterFuture,
BindingResolutionRequest, CurrentPolicyRequest, CurrentPolicyResolutionSink,
DirectBindingResolutionSink, ExistingBindingResolutionSink, FederatedAuthorityAdapter,
};
pub(crate) use binding::AuthoritativeBindingEvidence;
pub use binding::{
AuthoritativeBindingEvidence, AuthoritativeBindingResolution, BindingExpiry, BindingSource,
BindingVersion, EnrollmentMode, FederatedIdentityRequirement, FederatedPolicyStamp,
ResolvedFederatedPolicy, VersionedBindingRef,
AuthoritativeBindingResolution, BindingExpiry, BindingSource, BindingVersion, EnrollmentMode,
FederatedIdentityRequirement, FederatedPolicyStamp, ResolvedFederatedPolicy,
VersionedBindingRef,
};
pub use evidence::{
AdmissionExpiry, AssertionExpiry, AssertionNotBefore, AssertionTransport, AuthMethod,
@@ -73,7 +80,7 @@ impl fmt::Debug for FederatedAuthorization {
}
}
/// Authoritative O3 result consumed by the public O1 production finalizer.
/// Authoritative result consumed by the production finalizer.
///
/// Unlike [`FederatedAuthorization`], this input cannot contain a raw
/// [`VersionedBindingRef`] or a caller-selected authorization reason.
@@ -83,15 +90,15 @@ pub enum AuthoritativeFederatedResolution {
NotRequired,
/// Direct authority backed by an existing or atomically enrolled binding.
Direct {
/// Typed authoritative O3 lifecycle result.
/// Typed authoritative lifecycle result.
binding: AuthoritativeBindingResolution,
/// Current verified assertion for the authenticated actor.
assertion: VerifiedFederatedAssertion,
},
/// Delegated authority backed by an already-active owner binding.
Delegated {
/// Typed authoritative evidence for the existing owner binding.
owner: AuthoritativeBindingEvidence,
/// Typed authoritative result for the existing owner binding.
owner: AuthoritativeBindingResolution,
/// Current admission resolved for the owner.
admission: VerifiedOwnerAdmission,
},
@@ -106,6 +113,17 @@ impl fmt::Debug for AuthoritativeFederatedResolution {
}
}
impl AuthoritativeFederatedResolution {
#[allow(dead_code)]
pub(crate) const fn principal(&self) -> Option<&FederatedPrincipal> {
match self {
Self::NotRequired => None,
Self::Direct { assertion, .. } => Some(assertion.principal()),
Self::Delegated { admission, .. } => Some(admission.principal()),
}
}
}
/// Initial shared authorization-context contract.
#[derive(PartialEq, Eq)]
pub struct AuthContextV1 {
@@ -128,6 +146,31 @@ pub struct AuthContextInput {
community_access: AuthorizedCommunityAccess,
}
/// Opaque proof that a validated capability snapshot was consumed.
///
/// Only the crate-owned provider finalizer can construct this value. It keeps
/// the low-level context finalizer public for a stacked contract while making
/// it impossible for downstream code to bypass capability authorization.
pub struct CapabilityFinalizationSeal {
_private: (),
}
impl CapabilityFinalizationSeal {
#[allow(dead_code)]
pub(crate) const fn new() -> Self {
Self { _private: () }
}
}
impl fmt::Debug for CapabilityFinalizationSeal {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_tuple("CapabilityFinalizationSeal")
.field(&"[redacted]")
.finish()
}
}
impl AuthContextInput {
/// Collect evidence after cryptographic authentication and community
/// admission have both succeeded.
@@ -144,6 +187,26 @@ impl AuthContextInput {
community_access,
}
}
#[allow(dead_code)]
pub(crate) const fn authorization_domain(&self) -> CommunityId {
self.tenant.community()
}
#[allow(dead_code)]
pub(crate) const fn correlation_id(&self) -> Uuid {
self.correlation_id
}
#[allow(dead_code)]
pub(crate) const fn transport(&self) -> AuthTransport {
self.nostr_proof.authorized_transport()
}
#[allow(dead_code)]
pub(crate) const fn actor_pubkey(&self) -> PublicKey {
self.nostr_proof.actor_pubkey()
}
}
impl fmt::Debug for AuthContextV1 {
@@ -182,14 +245,19 @@ impl fmt::Debug for AuthContext {
}
impl AuthContext {
/// Finalize an immutable V1 context from authoritative O3 binding evidence.
/// Finalize an immutable V1 context from authoritative binding evidence.
///
/// This is the production O1↔O3 seam. O3 must resolve the enrollment-policy
/// stamp and binding lifecycle state from current authoritative storage,
/// use the policy epoch as a conditional precondition for any atomic
/// enrollment, and pass the resulting typed lifecycle outcome here. O1
/// derives the authorization reason; transport code cannot select it.
/// A crate-owned authority adapter must resolve the enrollment-policy stamp
/// and binding lifecycle state from current authoritative storage, use the
/// policy epoch as a conditional precondition for any atomic enrollment,
/// and pass the resulting opaque lifecycle outcome here. The finalizer
/// derives the authorization reason; transport code cannot select it or
/// construct authoritative policy and binding outcomes.
///
/// The opaque seal ensures a production caller first consumed the validated
/// capability decision supplied by the provider contract.
pub fn finalize_authoritative_v1(
_capability: CapabilityFinalizationSeal,
input: AuthContextInput,
federated_policy: ResolvedFederatedPolicy,
resolution: AuthoritativeFederatedResolution,
@@ -209,7 +277,7 @@ impl AuthContext {
}
AuthoritativeFederatedResolution::Delegated { owner, admission } => {
FederatedAuthorization::Delegated {
owner: VersionedBindingRef::from_existing_authoritative_evidence(owner),
owner: VersionedBindingRef::from_existing_authoritative_resolution(owner)?,
admission,
}
}
+6
View File
@@ -169,6 +169,9 @@ pub enum AuthContextError {
/// Delegated authorization did not match the verified Nostr owner.
#[error("delegated federated authorization does not match the verified Nostr owner")]
DelegatedOwnerMismatch,
/// Delegated owner evidence did not resolve an already-active binding.
#[error("delegated federated authorization requires an existing active binding")]
DelegatedBindingNotExistingActive,
}
impl AuthContextError {
@@ -216,6 +219,9 @@ impl AuthContextError {
Self::DelegateKeyMismatch => "federated_delegate_key_mismatch",
Self::DelegationRequired => "federated_delegation_required",
Self::DelegatedOwnerMismatch => "federated_delegated_owner_mismatch",
Self::DelegatedBindingNotExistingActive => {
"federated_delegated_binding_not_existing_active"
}
}
}
}
+287
View File
@@ -132,6 +132,92 @@ fn authoritative_binding_evidence(
.expect("synthetic authoritative binding evidence is valid")
}
struct TestAuthorityAdapter;
impl FederatedAuthorityAdapter for TestAuthorityAdapter {
type Error = &'static str;
fn resolve_current_policy<'a>(
&'a self,
request: CurrentPolicyRequest,
sink: CurrentPolicyResolutionSink,
) -> AuthorityAdapterFuture<
'a,
Result<ResolvedFederatedPolicy, AuthorityAdapterError<Self::Error>>,
> {
Box::pin(async move {
assert_eq!(request.authorization_domain(), authorization_domain(1));
assert_eq!(request.correlation_id(), Uuid::from_u128(2));
assert_eq!(request.observed_at(), 100);
assert!(!format!("{request:?}").contains("100"));
sink.resolved(
request.authorization_domain(),
Uuid::from_u128(40),
7,
FederatedIdentityRequirement::Required(EnrollmentMode::AttestedKey),
90,
200,
)
.map_err(AuthorityAdapterError::from)
})
}
fn resolve_direct_binding<'a>(
&'a self,
request: BindingResolutionRequest,
sink: DirectBindingResolutionSink,
) -> AuthorityAdapterFuture<
'a,
Result<AuthoritativeBindingResolution, AuthorityAdapterError<Self::Error>>,
> {
Box::pin(async move {
assert_eq!(request.policy_id(), Uuid::from_u128(40));
assert_eq!(request.policy_epoch(), 7);
assert_eq!(
request.policy_requirement(),
FederatedIdentityRequirement::Required(EnrollmentMode::AttestedKey)
);
assert!(request.key_attested());
assert_eq!(request.effective_from(), 90);
assert_eq!(request.effective_until(), 180);
assert_eq!(request.observed_at(), 100);
assert!(!format!("{request:?}").contains("subject-123"));
sink.atomically_enrolled(
request.authorization_domain(),
Uuid::from_u128(10),
request.principal().clone(),
request.bound_pubkey(),
BindingVersion::INITIAL,
Some(BindingExpiry::new(180).expect("synthetic binding expiry is valid")),
BindingSource::AttestedKey,
)
.map_err(AuthorityAdapterError::from)
})
}
fn resolve_existing_binding<'a>(
&'a self,
request: BindingResolutionRequest,
sink: ExistingBindingResolutionSink,
) -> AuthorityAdapterFuture<
'a,
Result<AuthoritativeBindingResolution, AuthorityAdapterError<Self::Error>>,
> {
Box::pin(async move {
sink.existing_active(
request.authorization_domain(),
Uuid::from_u128(10),
request.principal().clone(),
request.bound_pubkey(),
BindingVersion::INITIAL,
Some(BindingExpiry::new(180).expect("synthetic binding expiry is valid")),
BindingSource::Provisioned,
)
.map_err(AuthorityAdapterError::from)
})
}
}
fn binding_in(domain: u128, pubkey: PublicKey) -> VersionedBindingRef {
binding_with_source_in(domain, pubkey, BindingSource::AttestedKey)
}
@@ -1127,6 +1213,7 @@ fn authorization_error_codes_are_unique_and_provider_neutral() {
AuthContextError::DelegateKeyMismatch,
AuthContextError::DelegationRequired,
AuthContextError::DelegatedOwnerMismatch,
AuthContextError::DelegatedBindingNotExistingActive,
];
let mut codes = errors
.iter()
@@ -1311,6 +1398,7 @@ fn federated_policy_stamp_rejects_invalid_lineage() {
fn authoritative_finalizer_derives_binding_reason() {
let existing_actor = Keys::generate();
let existing = AuthContext::finalize_authoritative_v1(
CapabilityFinalizationSeal::new(),
input(
existing_actor.public_key(),
AuthTransport::RelayWebSocket,
@@ -1336,6 +1424,7 @@ fn authoritative_finalizer_derives_binding_reason() {
let attested_actor = Keys::generate();
let attested = AuthContext::finalize_authoritative_v1(
CapabilityFinalizationSeal::new(),
input(
attested_actor.public_key(),
AuthTransport::RelayWebSocket,
@@ -1366,6 +1455,7 @@ fn authoritative_finalizer_derives_binding_reason() {
let tofu_actor = Keys::generate();
let tofu = AuthContext::finalize_authoritative_v1(
CapabilityFinalizationSeal::new(),
input(tofu_actor.public_key(), AuthTransport::RelayWebSocket, None),
policy_required(EnrollmentMode::Tofu),
AuthoritativeFederatedResolution::Direct {
@@ -1388,10 +1478,149 @@ fn authoritative_finalizer_derives_binding_reason() {
);
}
#[tokio::test]
async fn cross_crate_authority_adapter_seals_policy_and_binding_outcome() {
let actor = Keys::generate();
let adapter = TestAuthorityAdapter;
let policy = resolve_current_federated_policy(
&adapter,
authorization_domain(1),
Uuid::from_u128(2),
100,
)
.await
.expect("current authoritative policy is valid");
let binding = authority::resolve_direct_binding(
&adapter,
&policy,
principal(),
actor.public_key(),
true,
90,
180,
100,
)
.await
.expect("atomic binding resolution is valid");
let context = AuthContext::finalize_authoritative_v1(
CapabilityFinalizationSeal::new(),
input(actor.public_key(), AuthTransport::RelayWebSocket, None),
policy,
AuthoritativeFederatedResolution::Direct {
binding,
assertion: assertion_with_attested_key(
principal(),
AssertionTransport::TrustedProxy,
180,
actor.public_key(),
),
},
100,
)
.expect("sealed adapter output finalizes");
assert_eq!(
context.authorization_reason(),
AuthorizationReason::EnrolledAttestedKey
);
}
#[tokio::test]
async fn binding_sink_rejects_missing_attestation_for_attested_enrollment() {
struct MissingAttestationAdapter;
impl FederatedAuthorityAdapter for MissingAttestationAdapter {
type Error = &'static str;
fn resolve_current_policy<'a>(
&'a self,
request: CurrentPolicyRequest,
sink: CurrentPolicyResolutionSink,
) -> AuthorityAdapterFuture<
'a,
Result<ResolvedFederatedPolicy, AuthorityAdapterError<Self::Error>>,
> {
Box::pin(async move {
sink.resolved(
request.authorization_domain(),
Uuid::from_u128(40),
7,
FederatedIdentityRequirement::Required(EnrollmentMode::AttestedKey),
90,
200,
)
.map_err(AuthorityAdapterError::from)
})
}
fn resolve_direct_binding<'a>(
&'a self,
request: BindingResolutionRequest,
sink: DirectBindingResolutionSink,
) -> AuthorityAdapterFuture<
'a,
Result<AuthoritativeBindingResolution, AuthorityAdapterError<Self::Error>>,
> {
Box::pin(async move {
sink.atomically_enrolled(
request.authorization_domain(),
Uuid::from_u128(10),
request.principal().clone(),
request.bound_pubkey(),
BindingVersion::INITIAL,
None,
BindingSource::AttestedKey,
)
.map_err(AuthorityAdapterError::from)
})
}
fn resolve_existing_binding<'a>(
&'a self,
_request: BindingResolutionRequest,
_sink: ExistingBindingResolutionSink,
) -> AuthorityAdapterFuture<
'a,
Result<AuthoritativeBindingResolution, AuthorityAdapterError<Self::Error>>,
> {
Box::pin(async { Err(AuthorityAdapterError::adapter("not called")) })
}
}
let actor = Keys::generate();
let adapter = MissingAttestationAdapter;
let policy = resolve_current_federated_policy(
&adapter,
authorization_domain(1),
Uuid::from_u128(2),
100,
)
.await
.expect("current authoritative policy is valid");
let error = authority::resolve_direct_binding(
&adapter,
&policy,
principal(),
actor.public_key(),
false,
90,
180,
100,
)
.await
.expect_err("attested-key enrollment requires sealed matching attestation");
assert_eq!(
error,
AuthorityAdapterError::Contract(AuthContextError::KeyAttestationRequired)
);
}
#[test]
fn authoritative_finalizer_rejects_incompatible_enrollment_result() {
let actor = Keys::generate();
let error = AuthContext::finalize_authoritative_v1(
CapabilityFinalizationSeal::new(),
input(actor.public_key(), AuthTransport::RelayWebSocket, None),
policy_required(EnrollmentMode::Provisioned),
AuthoritativeFederatedResolution::Direct {
@@ -1421,6 +1650,7 @@ fn authoritative_finalizer_carries_binding_expiry() {
)
.expect("synthetic authoritative binding evidence is valid");
let error = AuthContext::finalize_authoritative_v1(
CapabilityFinalizationSeal::new(),
input(actor.public_key(), AuthTransport::RelayWebSocket, None),
policy_required(EnrollmentMode::Provisioned),
AuthoritativeFederatedResolution::Direct {
@@ -1434,6 +1664,63 @@ fn authoritative_finalizer_carries_binding_expiry() {
assert_eq!(error, AuthContextError::BindingExpired);
}
#[test]
fn authoritative_finalizer_requires_existing_active_delegated_owner() {
let actor = Keys::generate();
let owner = Keys::generate();
let admission = || {
VerifiedOwnerAdmission::new(
authorization_domain(1),
principal(),
AdmissionExpiry::new(200).expect("synthetic admission expiry is valid"),
)
};
let enrolled_error = AuthContext::finalize_authoritative_v1(
CapabilityFinalizationSeal::new(),
input(
actor.public_key(),
AuthTransport::RelayWebSocket,
Some(owner.public_key()),
),
policy_required(EnrollmentMode::Provisioned),
AuthoritativeFederatedResolution::Delegated {
owner: AuthoritativeBindingResolution::atomically_enrolled(
authoritative_binding_evidence(owner.public_key(), BindingSource::Provisioned),
),
admission: admission(),
},
100,
)
.expect_err("a newly enrolled record cannot be relabeled as an existing delegated owner");
assert_eq!(
enrolled_error,
AuthContextError::DelegatedBindingNotExistingActive
);
let existing = AuthContext::finalize_authoritative_v1(
CapabilityFinalizationSeal::new(),
input(
actor.public_key(),
AuthTransport::RelayWebSocket,
Some(owner.public_key()),
),
policy_required(EnrollmentMode::Provisioned),
AuthoritativeFederatedResolution::Delegated {
owner: AuthoritativeBindingResolution::existing_active(authoritative_binding_evidence(
owner.public_key(),
BindingSource::Provisioned,
)),
admission: admission(),
},
100,
)
.expect("an existing active owner binding is eligible for delegated finalization");
assert_eq!(
existing.authorization_reason(),
AuthorizationReason::DelegatedOwnerBinding
);
}
#[test]
fn tofu_enrollment_cannot_use_attested_key_policy_reason() {
let actor = Keys::generate();
+7 -4
View File
@@ -34,10 +34,13 @@ pub mod scope;
pub use access::{check_read_access, check_write_access, require_scope, ChannelAccessChecker};
pub use context::{
AdmissionExpiry, AssertionExpiry, AssertionNotBefore, AssertionTransport, AuthContext,
AuthContextError, AuthContextInput, AuthContextV1, AuthContextVersion, AuthMethod,
AuthTransport, AuthorizationReason, AuthorizedCommunityAccess, BindingSource, BindingVersion,
DelegationCapability, DelegationExpiry, EnrollmentMode, FederatedAuthorization,
resolve_current_federated_policy, AdmissionExpiry, AssertionExpiry, AssertionNotBefore,
AssertionTransport, AuthContext, AuthContextError, AuthContextInput, AuthContextV1,
AuthContextVersion, AuthMethod, AuthTransport, AuthorityAdapterError, AuthorityAdapterFuture,
AuthorizationReason, AuthorizedCommunityAccess, BindingResolutionRequest, BindingSource,
BindingVersion, CapabilityFinalizationSeal, CurrentPolicyRequest, CurrentPolicyResolutionSink,
DelegationCapability, DelegationExpiry, DirectBindingResolutionSink, EnrollmentMode,
ExistingBindingResolutionSink, FederatedAuthorityAdapter, FederatedAuthorization,
FederatedIdentityRequirement, FederatedPrincipal, NostrAuthority, ResolvedFederatedPolicy,
VerifiedFederatedAssertion, VerifiedKeyAttestation, VerifiedNostrProof, VerifiedOwnerAdmission,
VerifiedTransportDelegation, VersionedBindingRef,