mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
fix(auth): seal authoritative adapter boundary
Signed-off-by: Cea Stapleton Cordasco <261786559+cea-block@users.noreply.github.com>
This commit is contained in:
@@ -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(())
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user