#![allow(dead_code)]
use core::marker::PhantomData;
use std::time::{Duration, Instant};
use crate::authority::capability::{
CapabilityKind, Endpoint, ModerationCapability, SubstrateScope, UserCapability,
};
use crate::authority::predicate::{BindError, BindFailureReason, DenialReason, PipelineStage};
use crate::authority::subjects::HasResourceLocation;
use crate::identity::TraceId;
use crate::ingress::{AuthContext, Requester};
use crate::proto::Did;
use crate::sealed;
#[derive(Debug)]
enum BindFlow {
Success,
DeniedAtPipeline {
stage: PipelineStage,
reason: DenialReason,
},
}
fn precheck_target_match<S: PartialEq>(
proof_subject: &S,
target: &S,
) -> Result<(), BindError> {
if proof_subject == target {
Ok(())
} else {
Err(BindError::TargetMismatch)
}
}
fn precheck_context_match(
proof_requester: &Did,
ctx: &AuthContext<'_>,
) -> Result<(), BindError> {
let ctx_did = match ctx.requester() {
Requester::Did(did) => did,
Requester::Service(svc) => svc.service_did(),
Requester::Anonymous => return Err(BindError::ContextMismatch),
};
if proof_requester == ctx_did {
Ok(())
} else {
Err(BindError::ContextMismatch)
}
}
fn precheck_expired(issued_at: Instant, max_age: Duration) -> Result<(), BindError> {
if issued_at.elapsed() > max_age {
Err(BindError::Expired)
} else {
Ok(())
}
}
fn now_unix_seconds() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::SystemTime::UNIX_EPOCH)
.map(|d| i64::try_from(d.as_secs()).unwrap_or(i64::MAX))
.unwrap_or(0)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct AuthorityId([u8; 16]);
impl AuthorityId {
#[must_use]
pub const fn from_bytes(bytes: [u8; 16]) -> Self {
AuthorityId(bytes)
}
}
#[must_use = "an unbound UserProof grants no access; call .bind to use it"]
pub struct UserProof<C: UserCapability> {
requester: Did,
subject: <C as UserCapability>::Subject,
issued_at: Instant,
issuer: AuthorityId,
capability_kind: CapabilityKind,
trace_id: TraceId,
_capability: PhantomData<C>,
_unconstructible_outside_crate: PhantomData<sealed::Token>,
}
impl<C: UserCapability> UserProof<C> {
pub(crate) fn new_internal(
requester: Did,
subject: <C as UserCapability>::Subject,
issued_at: Instant,
issuer: AuthorityId,
trace_id: TraceId,
) -> Self {
UserProof {
requester,
subject,
issued_at,
issuer,
capability_kind: C::KIND,
trace_id,
_capability: PhantomData,
_unconstructible_outside_crate: PhantomData,
}
}
pub async fn bind<'p>(
self,
ctx: &AuthContext<'_>,
target: &<C as UserCapability>::Subject,
) -> Result<BoundUserProof<'p, C>, BindError>
where
Self: 'p,
<C as UserCapability>::Subject:
PartialEq + crate::authority::HasResourceLocation,
<C as UserCapability>::OracleResults: Default,
C: crate::authority::IssuancePolicy,
{
let start = Instant::now();
precheck_target_match(&self.subject, target)?;
precheck_context_match(&self.requester, ctx)?;
precheck_expired(self.issued_at, C::MAX_AGE)?;
let trace_id = self.trace_id;
let proof_requester_did = self.requester.clone();
let attribution = ctx.attribution_chain().clone();
let now = std::time::SystemTime::now();
let target_did = target.resource_did().clone();
let target_nsid = target.resource_nsid().clone();
let subject_repr = crate::target::TargetRepresentation::structural_only(
crate::target::StructuralRepresentation::Resource {
did: target_did.clone(),
nsid: target_nsid.clone(),
},
);
let oracles_block = ctx.oracles().block;
let trace_id_for_predicate = trace_id;
let attribution_ref_for_predicate = ctx.attribution_chain();
let requester_ref_for_predicate = ctx.requester();
let pipeline_result: Result<BindFlow, BindError> =
crate::audit::composite_audit(trace_id, ctx.audit(), async |scope| {
if matches!(
C::SEMANTICS,
crate::authority::CapabilitySemantics::Write
) {
if let Err(reason) = crate::authority::check_stage_0_deprecation(
&target_nsid,
now_unix_seconds(),
) {
let event = crate::audit::UserAuditEvent::CapabilityIssuanceDenied {
trace_id,
requester: Requester::Did(proof_requester_did.clone()),
capability: C::KIND,
target_repr: subject_repr.clone(),
reason: reason.clone(),
attribution: attribution.clone(),
at: now,
};
scope.emit_user(event);
return Ok(BindFlow::DeniedAtPipeline {
stage: PipelineStage::DeprecationGate,
reason,
});
}
}
let block_state =
oracles_block.block_state(&proof_requester_did, &target_did);
if !matches!(block_state, crate::oracle::BlockState::None) {
let reason = DenialReason::Blocked {
query: crate::oracle::BlockOracleQuery::RequesterVsResourceOwner,
state: block_state,
};
let event = crate::audit::UserAuditEvent::CapabilityIssuanceDenied {
trace_id,
requester: Requester::Did(proof_requester_did.clone()),
capability: C::KIND,
target_repr: subject_repr.clone(),
reason: reason.clone(),
attribution: attribution.clone(),
at: now,
};
scope.emit_user(event);
return Ok(BindFlow::DeniedAtPipeline {
stage: PipelineStage::BlockConsultation,
reason,
});
}
let oracle_results =
<<C as UserCapability>::OracleResults as Default>::default();
let predicate_ctx = crate::authority::PredicateContext::new(
requester_ref_for_predicate,
trace_id_for_predicate,
attribution_ref_for_predicate,
);
if let Err(reason) =
<C as crate::authority::IssuancePolicy>::capability_predicate(
&predicate_ctx,
target,
&oracle_results,
)
{
let event = crate::audit::UserAuditEvent::CapabilityIssuanceDenied {
trace_id,
requester: Requester::Did(proof_requester_did.clone()),
capability: C::KIND,
target_repr: subject_repr.clone(),
reason: reason.clone(),
attribution: attribution.clone(),
at: now,
};
scope.emit_user(event);
return Ok(BindFlow::DeniedAtPipeline {
stage: PipelineStage::Predicate,
reason,
});
}
let event = crate::audit::UserAuditEvent::CapabilityBound {
trace_id,
requester: proof_requester_did,
subject_repr,
capability: C::KIND,
outcome: crate::authority::BindOutcomeRepr::Success,
attribution,
at: now,
};
scope.emit_user(event);
Ok(BindFlow::Success)
})
.await;
let timing_target = crate::timing::equalize_timing_target_for::<C>(ctx.oracles());
crate::timing::equalize_timing(start, timing_target).await;
match pipeline_result {
Ok(BindFlow::Success) => Ok(BoundUserProof {
proof: self,
_life: PhantomData,
}),
Ok(BindFlow::DeniedAtPipeline { stage, reason }) => {
Err(BindError::DeniedAtPipeline { stage, reason })
}
Err(bind_err) => Err(bind_err),
}
}
}
#[must_use]
pub struct BoundUserProof<'p, C: UserCapability> {
proof: UserProof<C>,
_life: PhantomData<&'p ()>,
}
impl<'p, C: UserCapability> BoundUserProof<'p, C> {
pub fn subject(&self) -> &<C as UserCapability>::Subject {
&self.proof.subject
}
pub fn requester(&self) -> &Did {
&self.proof.requester
}
pub fn trace_id(&self) -> TraceId {
self.proof.trace_id
}
pub async fn reborrow<'r>(
&'r self,
ctx: &AuthContext<'_>,
) -> Result<UserProofRef<'r, C>, BindFailureReason>
where
<C as UserCapability>::Subject: crate::authority::HasResourceLocation,
{
if self.proof.issued_at.elapsed() <= C::MAX_AGE {
return Ok(UserProofRef { proof: &self.proof });
}
let trace_id = self.proof.trace_id;
let requester = self.proof.requester.clone();
let now = std::time::SystemTime::now();
let target_did = self.proof.subject.resource_did().clone();
let target_nsid = self.proof.subject.resource_nsid().clone();
let subject_repr = crate::target::TargetRepresentation::structural_only(
crate::target::StructuralRepresentation::Resource {
did: target_did,
nsid: target_nsid,
},
);
let audit_result: Result<(), BindError> = crate::audit::composite_audit(
trace_id,
ctx.audit(),
async |scope| {
let event = crate::audit::UserAuditEvent::ReborrowFailed {
trace_id,
requester,
subject_repr,
capability: C::KIND,
reason: BindFailureReason::Expired,
at: now,
};
scope.emit_user(event);
Ok::<(), BindError>(())
},
)
.await;
match audit_result {
Ok(()) => Err(BindFailureReason::Expired),
Err(_) => Err(BindFailureReason::AuditUnavailable),
}
}
}
pub struct UserProofRef<'p, C: UserCapability> {
proof: &'p UserProof<C>,
}
impl<'p, C: UserCapability> UserProofRef<'p, C> {
pub fn subject(&self) -> &<C as UserCapability>::Subject {
&self.proof.subject
}
pub fn requester(&self) -> &Did {
&self.proof.requester
}
pub fn trace_id(&self) -> TraceId {
self.proof.trace_id
}
}
#[must_use = "an unbound ChannelProof grants no access; call .bind to use it"]
pub struct ChannelProof<E: Endpoint> {
requester: Did,
subject: <E as Endpoint>::Subject,
issued_at: Instant,
issuer: AuthorityId,
capability_kind: CapabilityKind,
trace_id: TraceId,
_capability: PhantomData<E>,
_unconstructible_outside_crate: PhantomData<sealed::Token>,
}
impl<E: Endpoint> ChannelProof<E> {
pub(crate) fn new_internal(
requester: Did,
subject: <E as Endpoint>::Subject,
issued_at: Instant,
issuer: AuthorityId,
trace_id: TraceId,
) -> Self {
ChannelProof {
requester,
subject,
issued_at,
issuer,
capability_kind: E::KIND,
trace_id,
_capability: PhantomData,
_unconstructible_outside_crate: PhantomData,
}
}
pub async fn bind<'p>(
self,
ctx: &AuthContext<'_>,
target: &<E as Endpoint>::Subject,
) -> Result<BoundChannelProof<'p, E>, BindError>
where
Self: 'p,
<E as Endpoint>::Subject: PartialEq,
{
precheck_target_match(&self.subject, target)?;
precheck_context_match(&self.requester, ctx)?;
precheck_expired(self.issued_at, E::MAX_AGE)?;
let trace_id = self.trace_id;
let proof_requester_did = self.requester.clone();
let now = std::time::SystemTime::now();
let peer_placeholder = crate::identity::ServiceIdentity::new_internal(
proof_requester_did.clone(),
crate::identity::KeyId::from_bytes([0u8; 32]),
crate::identity::PublicKey {
algorithm: crate::identity::SignatureAlgorithm::Ed25519,
bytes: [0u8; 32],
},
None,
);
let session_id_placeholder =
crate::identity::SessionId::from_bytes([0u8; 32]);
let session_digest = crate::identity::SessionDigest::compute(
&session_id_placeholder,
ctx.audit().correlation_key,
);
let _ = target;
let pipeline_result: Result<BindFlow, BindError> =
crate::audit::composite_audit(trace_id, ctx.audit(), async |scope| {
let event = crate::audit::ChannelAuditEvent::ChannelBound {
trace_id,
peer: peer_placeholder,
session_digest,
endpoint: E::KIND,
outcome: crate::authority::BindOutcomeRepr::Success,
payload_completeness: crate::audit::PayloadCompleteness::PartialV01,
at: now,
};
scope.emit_channel(event);
Ok(BindFlow::Success)
})
.await;
match pipeline_result {
Ok(BindFlow::Success) => Ok(BoundChannelProof {
proof: self,
_life: PhantomData,
}),
Ok(BindFlow::DeniedAtPipeline { stage, reason }) => {
Err(BindError::DeniedAtPipeline { stage, reason })
}
Err(bind_err) => Err(bind_err),
}
}
}
#[must_use]
pub struct BoundChannelProof<'p, E: Endpoint> {
proof: ChannelProof<E>,
_life: PhantomData<&'p ()>,
}
impl<'p, E: Endpoint> BoundChannelProof<'p, E> {
pub fn subject(&self) -> &<E as Endpoint>::Subject {
&self.proof.subject
}
pub fn requester(&self) -> &Did {
&self.proof.requester
}
pub fn trace_id(&self) -> TraceId {
self.proof.trace_id
}
pub async fn reborrow<'r>(
&'r self,
ctx: &AuthContext<'_>,
) -> Result<ChannelProofRef<'r, E>, BindFailureReason> {
if self.proof.issued_at.elapsed() <= E::MAX_AGE {
return Ok(ChannelProofRef { proof: &self.proof });
}
let trace_id = self.proof.trace_id;
let now = std::time::SystemTime::now();
let peer_placeholder = crate::identity::ServiceIdentity::new_internal(
self.proof.requester.clone(),
crate::identity::KeyId::from_bytes([0u8; 32]),
crate::identity::PublicKey {
algorithm: crate::identity::SignatureAlgorithm::Ed25519,
bytes: [0u8; 32],
},
None,
);
let session_id_placeholder =
crate::identity::SessionId::from_bytes([0u8; 32]);
let session_digest = crate::identity::SessionDigest::compute(
&session_id_placeholder,
ctx.audit().correlation_key,
);
let audit_result: Result<(), BindError> = crate::audit::composite_audit(
trace_id,
ctx.audit(),
async |scope| {
let event = crate::audit::ChannelAuditEvent::ChannelReborrowFailed {
trace_id,
peer: peer_placeholder,
session_digest,
endpoint: E::KIND,
reason: BindFailureReason::Expired,
payload_completeness: crate::audit::PayloadCompleteness::PartialV01,
at: now,
};
scope.emit_channel(event);
Ok::<(), BindError>(())
},
)
.await;
match audit_result {
Ok(()) => Err(BindFailureReason::Expired),
Err(_) => Err(BindFailureReason::AuditUnavailable),
}
}
}
pub struct ChannelProofRef<'p, E: Endpoint> {
proof: &'p ChannelProof<E>,
}
impl<'p, E: Endpoint> ChannelProofRef<'p, E> {
pub fn subject(&self) -> &<E as Endpoint>::Subject {
&self.proof.subject
}
pub fn requester(&self) -> &Did {
&self.proof.requester
}
pub fn trace_id(&self) -> TraceId {
self.proof.trace_id
}
}
#[must_use = "an unbound SubstrateProof grants no access; call .bind to use it"]
pub struct SubstrateProof<S: SubstrateScope> {
requester: Did,
subject: <S as SubstrateScope>::Subject,
issued_at: Instant,
issuer: AuthorityId,
capability_kind: CapabilityKind,
trace_id: TraceId,
_capability: PhantomData<S>,
_unconstructible_outside_crate: PhantomData<sealed::Token>,
}
impl<S: SubstrateScope> SubstrateProof<S> {
pub(crate) fn new_internal(
requester: Did,
subject: <S as SubstrateScope>::Subject,
issued_at: Instant,
issuer: AuthorityId,
trace_id: TraceId,
) -> Self {
SubstrateProof {
requester,
subject,
issued_at,
issuer,
capability_kind: S::KIND,
trace_id,
_capability: PhantomData,
_unconstructible_outside_crate: PhantomData,
}
}
pub async fn bind<'p>(
self,
ctx: &AuthContext<'_>,
target: &<S as SubstrateScope>::Subject,
) -> Result<BoundSubstrateProof<'p, S>, BindError>
where
Self: 'p,
<S as SubstrateScope>::Subject: PartialEq,
{
precheck_target_match(&self.subject, target)?;
precheck_context_match(&self.requester, ctx)?;
precheck_expired(self.issued_at, S::MAX_AGE)?;
let service = match ctx.requester() {
Requester::Service(svc) => svc.clone(),
_ => return Err(BindError::ContextMismatch),
};
let trace_id = self.trace_id;
let now = std::time::SystemTime::now();
let scope_repr = crate::target::TargetRepresentation::structural_only(
crate::target::StructuralRepresentation::Scope {
kind: crate::target::ScopeKind::Shard,
},
);
let _ = target;
let pipeline_result: Result<BindFlow, BindError> =
crate::audit::composite_audit(trace_id, ctx.audit(), async |scope| {
let event = crate::audit::SubstrateAuditEvent::ScopeBound {
trace_id,
service,
scope_repr,
capability: S::KIND,
outcome: crate::authority::BindOutcomeRepr::Success,
payload_completeness: crate::audit::PayloadCompleteness::PartialV01,
at: now,
};
scope.emit_substrate(event);
Ok(BindFlow::Success)
})
.await;
match pipeline_result {
Ok(BindFlow::Success) => Ok(BoundSubstrateProof {
proof: self,
_life: PhantomData,
}),
Ok(BindFlow::DeniedAtPipeline { stage, reason }) => {
Err(BindError::DeniedAtPipeline { stage, reason })
}
Err(bind_err) => Err(bind_err),
}
}
}
#[must_use]
pub struct BoundSubstrateProof<'p, S: SubstrateScope> {
proof: SubstrateProof<S>,
_life: PhantomData<&'p ()>,
}
impl<'p, S: SubstrateScope> BoundSubstrateProof<'p, S> {
pub fn subject(&self) -> &<S as SubstrateScope>::Subject {
&self.proof.subject
}
pub fn requester(&self) -> &Did {
&self.proof.requester
}
pub fn trace_id(&self) -> TraceId {
self.proof.trace_id
}
pub async fn reborrow<'r>(
&'r self,
ctx: &AuthContext<'_>,
) -> Result<SubstrateProofRef<'r, S>, BindFailureReason> {
if self.proof.issued_at.elapsed() <= S::MAX_AGE {
return Ok(SubstrateProofRef { proof: &self.proof });
}
let trace_id = self.proof.trace_id;
let now = std::time::SystemTime::now();
let service = match ctx.requester() {
Requester::Service(svc) => svc.clone(),
_ => return Err(BindFailureReason::Expired),
};
let scope_repr = crate::target::TargetRepresentation::structural_only(
crate::target::StructuralRepresentation::Scope {
kind: crate::target::ScopeKind::Shard,
},
);
let audit_result: Result<(), BindError> = crate::audit::composite_audit(
trace_id,
ctx.audit(),
async |scope| {
let event = crate::audit::SubstrateAuditEvent::ScopeBound {
trace_id,
service,
scope_repr,
capability: S::KIND,
outcome: crate::authority::BindOutcomeRepr::Expired {
issued_at: self.proof.issued_at,
max_age: S::MAX_AGE,
},
payload_completeness: crate::audit::PayloadCompleteness::PartialV01,
at: now,
};
scope.emit_substrate(event);
Ok::<(), BindError>(())
},
)
.await;
match audit_result {
Ok(()) => Err(BindFailureReason::Expired),
Err(_) => Err(BindFailureReason::AuditUnavailable),
}
}
}
pub struct SubstrateProofRef<'p, S: SubstrateScope> {
proof: &'p SubstrateProof<S>,
}
impl<'p, S: SubstrateScope> SubstrateProofRef<'p, S> {
pub fn subject(&self) -> &<S as SubstrateScope>::Subject {
&self.proof.subject
}
pub fn requester(&self) -> &Did {
&self.proof.requester
}
pub fn trace_id(&self) -> TraceId {
self.proof.trace_id
}
}
#[must_use = "an unbound ModerationProof grants no access; call .bind to use it"]
pub struct ModerationProof<C: ModerationCapability> {
requester: Did,
subject: <C as ModerationCapability>::Subject,
issued_at: Instant,
issuer: AuthorityId,
capability_kind: CapabilityKind,
trace_id: TraceId,
_capability: PhantomData<C>,
_unconstructible_outside_crate: PhantomData<sealed::Token>,
}
impl<C: ModerationCapability> ModerationProof<C> {
pub(crate) fn new_internal(
requester: Did,
subject: <C as ModerationCapability>::Subject,
issued_at: Instant,
issuer: AuthorityId,
trace_id: TraceId,
) -> Self {
ModerationProof {
requester,
subject,
issued_at,
issuer,
capability_kind: C::KIND,
trace_id,
_capability: PhantomData,
_unconstructible_outside_crate: PhantomData,
}
}
pub async fn bind<'p>(
self,
ctx: &AuthContext<'_>,
target: &<C as ModerationCapability>::Subject,
rationale: crate::audit::ModeratorRationale,
) -> Result<BoundModerationProof<'p, C>, BindError>
where
Self: 'p,
<C as ModerationCapability>::Subject:
PartialEq + crate::authority::HasResourceLocation,
{
precheck_target_match(&self.subject, target)?;
precheck_context_match(&self.requester, ctx)?;
precheck_expired(self.issued_at, C::MAX_AGE)?;
let trace_id = self.trace_id;
let moderator_did = self.requester.clone();
let now = std::time::SystemTime::now();
let target_did = target.resource_did().clone();
let target_nsid = target.resource_nsid().clone();
let target_repr = crate::target::TargetRepresentation::structural_only(
crate::target::StructuralRepresentation::Resource {
did: target_did.clone(),
nsid: target_nsid.clone(),
},
);
let case = crate::authority::ModerationCaseId::from_bytes([0u8; 16]);
let kind = C::KIND;
let rationale_for_audit = rationale.clone();
let rationale_for_notification = rationale;
let target_repr_for_notification = target_repr.clone();
let moderator_for_audit = moderator_did.clone();
let pipeline_result: Result<BindFlow, BindError> =
crate::audit::composite_audit(trace_id, ctx.audit(), async |scope| {
if let Err(reason) = crate::authority::check_stage_0_deprecation(
&target_nsid,
now_unix_seconds(),
) {
let event = crate::audit::ModerationAuditEvent::ModerationIssuanceDenied {
trace_id,
moderator: moderator_for_audit.clone(),
capability: kind,
reason: reason.clone(),
at: now,
};
scope.emit_moderation(event);
return Ok(BindFlow::DeniedAtPipeline {
stage: PipelineStage::DeprecationGate,
reason,
});
}
let event = match kind {
crate::authority::CapabilityKind::ModeratorRead => {
crate::audit::ModerationAuditEvent::ModeratorInspected {
trace_id,
moderator: moderator_for_audit,
case,
target_repr: target_repr.clone(),
rationale: rationale_for_audit,
payload_completeness:
crate::audit::PayloadCompleteness::PartialV01,
at: now,
}
}
crate::authority::CapabilityKind::ModeratorTakedown => {
crate::audit::ModerationAuditEvent::ModeratorTookDown {
trace_id,
moderator: moderator_for_audit,
case,
target_repr: target_repr.clone(),
outcome: crate::authority::BindOutcomeRepr::Success,
rationale: rationale_for_audit,
payload_completeness:
crate::audit::PayloadCompleteness::PartialV01,
at: now,
}
}
crate::authority::CapabilityKind::ModeratorRestore => {
crate::audit::ModerationAuditEvent::ModeratorRestored {
trace_id,
moderator: moderator_for_audit,
case,
target_repr: target_repr.clone(),
outcome: crate::authority::BindOutcomeRepr::Success,
rationale: rationale_for_audit,
payload_completeness:
crate::audit::PayloadCompleteness::PartialV01,
at: now,
}
}
other => unreachable!(
"non-moderation capability kind {other:?} reached ModerationProof::bind"
),
};
scope.emit_moderation(event);
Ok(BindFlow::Success)
})
.await;
if matches!(pipeline_result, Ok(BindFlow::Success)) {
let inspection_kind = match kind {
crate::authority::CapabilityKind::ModeratorRead => {
crate::authority::InspectionKind::ModeratorRead {
case,
rationale: rationale_for_notification,
}
}
crate::authority::CapabilityKind::ModeratorTakedown => {
crate::authority::InspectionKind::Takedown {
case,
rationale: rationale_for_notification,
}
}
crate::authority::CapabilityKind::ModeratorRestore => {
crate::authority::InspectionKind::Restore {
case,
rationale: rationale_for_notification,
}
}
other => unreachable!(
"non-moderation capability kind {other:?} reached inspection-kind dispatch"
),
};
let mut notification_id_bytes = [0u8; 16];
getrandom::getrandom(&mut notification_id_bytes)
.expect("§6.7 notification-id init: OS CSPRNG unavailable");
let notification = crate::authority::InspectionNotification {
notification_id: crate::authority::NotificationId::from_bytes(
notification_id_bytes,
),
trace_id,
kind: inspection_kind,
target_repr: target_repr_for_notification,
at: now,
};
ctx.audit()
.inspection_queue
.enqueue(&target_did, notification);
}
match pipeline_result {
Ok(BindFlow::Success) => Ok(BoundModerationProof {
proof: self,
_life: PhantomData,
}),
Ok(BindFlow::DeniedAtPipeline { stage, reason }) => {
Err(BindError::DeniedAtPipeline { stage, reason })
}
Err(bind_err) => Err(bind_err),
}
}
}
#[must_use]
pub struct BoundModerationProof<'p, C: ModerationCapability> {
proof: ModerationProof<C>,
_life: PhantomData<&'p ()>,
}
impl<'p, C: ModerationCapability> BoundModerationProof<'p, C> {
pub fn subject(&self) -> &<C as ModerationCapability>::Subject {
&self.proof.subject
}
pub fn requester(&self) -> &Did {
&self.proof.requester
}
pub fn trace_id(&self) -> TraceId {
self.proof.trace_id
}
pub async fn reborrow<'r>(
&'r self,
_ctx: &AuthContext<'_>,
) -> Result<ModerationProofRef<'r, C>, BindFailureReason> {
if self.proof.issued_at.elapsed() <= C::MAX_AGE {
return Ok(ModerationProofRef { proof: &self.proof });
}
Err(BindFailureReason::Expired)
}
}
pub struct ModerationProofRef<'p, C: ModerationCapability> {
proof: &'p ModerationProof<C>,
}
impl<'p, C: ModerationCapability> ModerationProofRef<'p, C> {
pub fn subject(&self) -> &<C as ModerationCapability>::Subject {
&self.proof.subject
}
pub fn requester(&self) -> &Did {
&self.proof.requester
}
pub fn trace_id(&self) -> TraceId {
self.proof.trace_id
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn authority_id_round_trips() {
let a = AuthorityId::from_bytes([1; 16]);
assert_eq!(a, a);
}
#[test]
fn precheck_target_match_pins_equality() {
let a = "subject-a";
let b = "subject-b";
assert!(matches!(precheck_target_match(&a, &a), Ok(())));
assert!(matches!(
precheck_target_match(&a, &b),
Err(BindError::TargetMismatch)
));
}
#[test]
fn precheck_context_match_pins_did_comparison() {
use crate::ingress::{AttributionChain, AuditSinks, OracleSet};
use std::sync::Arc;
struct NoSink;
impl crate::audit::UserAuditSink for NoSink {
fn record(
&self,
_: crate::audit::UserAuditEvent,
) -> Result<(), crate::audit::AuditError> {
Ok(())
}
}
impl crate::audit::ChannelAuditSink for NoSink {
fn record(
&self,
_: crate::audit::ChannelAuditEvent,
) -> Result<(), crate::audit::AuditError> {
Ok(())
}
}
impl crate::audit::SubstrateAuditSink for NoSink {
fn record(
&self,
_: crate::audit::SubstrateAuditEvent,
) -> Result<(), crate::audit::AuditError> {
Ok(())
}
}
impl crate::audit::ModerationAuditSink for NoSink {
fn record(
&self,
_: crate::audit::ModerationAuditEvent,
) -> Result<(), crate::audit::AuditError> {
Ok(())
}
}
impl crate::audit::FallbackAuditSink for NoSink {
fn record_panic(
&self,
_: crate::audit::SinkKind,
_: TraceId,
_: CapabilityKind,
_: std::time::SystemTime,
) {
}
fn record_composite_failure(
&self,
_: TraceId,
_: crate::audit::CompositeOpId,
_: &[crate::audit::SinkKind],
_: &[crate::audit::SinkKind],
_: std::time::SystemTime,
) {
}
fn record_event(&self, _: crate::audit::FallbackAuditEvent) {}
}
struct NoOracle;
impl crate::oracle::BlockOracle for NoOracle {
fn block_state(&self, _: &Did, _: &Did) -> crate::oracle::BlockState {
crate::oracle::BlockState::None
}
fn last_synced_at(&self) -> std::time::SystemTime {
std::time::SystemTime::UNIX_EPOCH
}
fn data_freshness_bound(&self) -> Duration {
Duration::from_secs(60)
}
fn worst_case_latency_for(&self, _: crate::oracle::BlockOracleQuery) -> Duration {
Duration::ZERO
}
}
impl crate::oracle::AudienceOracle for NoOracle {
fn audience_state(
&self,
_: &Did,
_: &crate::authority::ResourceId,
) -> crate::oracle::AudienceState {
crate::oracle::AudienceState::NoAudienceConfigured
}
fn last_synced_at(&self) -> std::time::SystemTime {
std::time::SystemTime::UNIX_EPOCH
}
fn data_freshness_bound(&self) -> Duration {
Duration::from_secs(60)
}
fn worst_case_latency_for(
&self,
_: crate::oracle::AudienceOracleQuery,
) -> Duration {
Duration::ZERO
}
}
impl crate::oracle::MuteOracle for NoOracle {
fn mute_state(&self, _: &Did, _: &Did) -> crate::oracle::MuteState {
crate::oracle::MuteState::None
}
fn last_synced_at(&self) -> std::time::SystemTime {
std::time::SystemTime::UNIX_EPOCH
}
fn data_freshness_bound(&self) -> Duration {
Duration::from_secs(60)
}
fn worst_case_latency_for(&self, _: crate::oracle::MuteOracleQuery) -> Duration {
Duration::ZERO
}
}
let sink: Arc<NoSink> = Arc::new(NoSink);
let oracle: Arc<NoOracle> = Arc::new(NoOracle);
let inspection: Arc<crate::authority::NoInspectionNotifications> =
Arc::new(crate::authority::NoInspectionNotifications);
let correlation_key =
crate::identity::CorrelationKey::from_bytes([0u8; 32]);
let did_a = Did::new("did:plc:contextmatch-a").unwrap();
let did_b = Did::new("did:plc:contextmatch-b").unwrap();
let ctx_with_a = AuthContext::new_internal(
Requester::Did(did_a.clone()),
TraceId::from_bytes([0u8; 16]),
AuditSinks {
user: &*sink,
channel: &*sink,
substrate: &*sink,
moderation: &*sink,
fallback: &*sink,
inspection_queue: &*inspection,
correlation_key: &correlation_key,
},
OracleSet {
block: &*oracle,
audience: &*oracle,
mute: &*oracle,
},
AttributionChain::empty(),
crate::authority::capability::CapabilitySet::empty(),
);
assert!(matches!(precheck_context_match(&did_a, &ctx_with_a), Ok(())));
assert!(matches!(
precheck_context_match(&did_b, &ctx_with_a),
Err(BindError::ContextMismatch)
));
let ctx_anon = AuthContext::new_internal(
Requester::Anonymous,
TraceId::from_bytes([0u8; 16]),
AuditSinks {
user: &*sink,
channel: &*sink,
substrate: &*sink,
moderation: &*sink,
fallback: &*sink,
inspection_queue: &*inspection,
correlation_key: &correlation_key,
},
OracleSet {
block: &*oracle,
audience: &*oracle,
mute: &*oracle,
},
AttributionChain::empty(),
crate::authority::capability::CapabilitySet::empty(),
);
assert!(matches!(
precheck_context_match(&did_a, &ctx_anon),
Err(BindError::ContextMismatch)
));
}
#[test]
fn precheck_expired_pins_max_age_window() {
let now = Instant::now();
assert!(matches!(
precheck_expired(now, Duration::from_secs(60)),
Ok(())
));
let past = Instant::now() - Duration::from_millis(200);
assert!(matches!(
precheck_expired(past, Duration::from_millis(100)),
Err(BindError::Expired)
));
}
#[test]
fn bind_error_from_composite_audit_error_pins_mapping() {
use crate::audit::{AuditError, CompositeAuditError, SinkKind};
let cae_commit = CompositeAuditError::SinkCommitFailed {
class: SinkKind::User,
source: AuditError::Unavailable,
};
assert!(matches!(BindError::from(cae_commit), BindError::AuditUnavailable));
let cae_rollback = CompositeAuditError::RollbackDispatchFailed {
class: SinkKind::User,
source: AuditError::Unavailable,
};
assert!(matches!(
BindError::from(cae_rollback),
BindError::AuditUnavailable
));
let cae_tracker = CompositeAuditError::TrackerFull;
assert!(matches!(BindError::from(cae_tracker), BindError::AuditUnavailable));
let cae_unrec = CompositeAuditError::InconsistencyUnrecoverable;
assert!(matches!(BindError::from(cae_unrec), BindError::AuditPanicked));
}
}
#[cfg(test)]
mod bind_test_fixtures {
use std::sync::{Arc, Mutex};
use std::time::{Duration, SystemTime};
use super::*;
use crate::audit::{
AuditError, ChannelAuditEvent, ChannelAuditSink, CompositeOpId, FallbackAuditEvent,
FallbackAuditSink, ModerationAuditEvent, ModerationAuditSink, SinkKind,
SubstrateAuditEvent, SubstrateAuditSink, UserAuditEvent, UserAuditSink,
};
use crate::authority::moderation::{
InspectionNotification, InspectionNotificationQueueImpl,
};
use crate::ingress::{AttributionChain, AuditSinks, OracleSet};
use crate::oracle::{
AudienceOracle, AudienceOracleQuery, AudienceState, BlockOracle, BlockOracleQuery,
BlockState, MuteOracle, MuteOracleQuery, MuteState,
};
pub struct CapturingUserSink {
captured: Mutex<Vec<UserAuditEvent>>,
}
impl CapturingUserSink {
pub fn new() -> Self {
CapturingUserSink {
captured: Mutex::new(Vec::new()),
}
}
pub fn captured(&self) -> Vec<UserAuditEvent> {
self.captured.lock().unwrap().clone()
}
}
impl UserAuditSink for CapturingUserSink {
fn record(&self, event: UserAuditEvent) -> Result<(), AuditError> {
self.captured.lock().unwrap().push(event);
Ok(())
}
}
pub struct CapturingChannelSink {
captured: Mutex<Vec<ChannelAuditEvent>>,
}
impl CapturingChannelSink {
pub fn new() -> Self {
CapturingChannelSink {
captured: Mutex::new(Vec::new()),
}
}
pub fn captured(&self) -> Vec<ChannelAuditEvent> {
self.captured.lock().unwrap().clone()
}
}
impl ChannelAuditSink for CapturingChannelSink {
fn record(&self, event: ChannelAuditEvent) -> Result<(), AuditError> {
self.captured.lock().unwrap().push(event);
Ok(())
}
}
pub struct CapturingSubstrateSink {
captured: Mutex<Vec<SubstrateAuditEvent>>,
}
impl CapturingSubstrateSink {
pub fn new() -> Self {
CapturingSubstrateSink {
captured: Mutex::new(Vec::new()),
}
}
pub fn captured(&self) -> Vec<SubstrateAuditEvent> {
self.captured.lock().unwrap().clone()
}
}
impl SubstrateAuditSink for CapturingSubstrateSink {
fn record(&self, event: SubstrateAuditEvent) -> Result<(), AuditError> {
self.captured.lock().unwrap().push(event);
Ok(())
}
}
pub struct CapturingModerationSink {
captured: Mutex<Vec<ModerationAuditEvent>>,
}
impl CapturingModerationSink {
pub fn new() -> Self {
CapturingModerationSink {
captured: Mutex::new(Vec::new()),
}
}
pub fn captured(&self) -> Vec<ModerationAuditEvent> {
self.captured.lock().unwrap().clone()
}
}
impl ModerationAuditSink for CapturingModerationSink {
fn record(&self, event: ModerationAuditEvent) -> Result<(), AuditError> {
self.captured.lock().unwrap().push(event);
Ok(())
}
}
pub struct NoopFallback;
impl FallbackAuditSink for NoopFallback {
fn record_panic(
&self,
_: SinkKind,
_: TraceId,
_: CapabilityKind,
_: SystemTime,
) {
}
fn record_composite_failure(
&self,
_: TraceId,
_: CompositeOpId,
_: &[SinkKind],
_: &[SinkKind],
_: SystemTime,
) {
}
fn record_event(&self, _: FallbackAuditEvent) {}
}
pub struct CapturingInspection {
captured: Mutex<Vec<(Did, InspectionNotification)>>,
}
impl CapturingInspection {
pub fn new() -> Self {
CapturingInspection {
captured: Mutex::new(Vec::new()),
}
}
pub fn captured(&self) -> Vec<(Did, InspectionNotification)> {
self.captured.lock().unwrap().clone()
}
}
impl InspectionNotificationQueueImpl for CapturingInspection {
fn enqueue(&self, owner: &Did, event: InspectionNotification) {
self.captured
.lock()
.unwrap()
.push((owner.clone(), event));
}
}
pub struct ConfigurableBlockOracle {
pub state: BlockState,
}
impl BlockOracle for ConfigurableBlockOracle {
fn block_state(&self, _: &Did, _: &Did) -> BlockState {
self.state.clone()
}
fn last_synced_at(&self) -> SystemTime {
SystemTime::UNIX_EPOCH
}
fn data_freshness_bound(&self) -> Duration {
Duration::from_secs(60)
}
fn worst_case_latency_for(&self, _: BlockOracleQuery) -> Duration {
Duration::ZERO
}
}
pub struct NoopAudienceOracle;
impl AudienceOracle for NoopAudienceOracle {
fn audience_state(
&self,
_: &Did,
_: &crate::authority::ResourceId,
) -> AudienceState {
AudienceState::NoAudienceConfigured
}
fn last_synced_at(&self) -> SystemTime {
SystemTime::UNIX_EPOCH
}
fn data_freshness_bound(&self) -> Duration {
Duration::from_secs(60)
}
fn worst_case_latency_for(&self, _: AudienceOracleQuery) -> Duration {
Duration::ZERO
}
}
pub struct NoopMuteOracle;
impl MuteOracle for NoopMuteOracle {
fn mute_state(&self, _: &Did, _: &Did) -> MuteState {
MuteState::None
}
fn last_synced_at(&self) -> SystemTime {
SystemTime::UNIX_EPOCH
}
fn data_freshness_bound(&self) -> Duration {
Duration::from_secs(60)
}
fn worst_case_latency_for(&self, _: MuteOracleQuery) -> Duration {
Duration::ZERO
}
}
pub struct BindFixture {
pub user: Arc<CapturingUserSink>,
pub channel: Arc<CapturingChannelSink>,
pub substrate: Arc<CapturingSubstrateSink>,
pub moderation: Arc<CapturingModerationSink>,
pub fallback: Arc<NoopFallback>,
pub inspection: Arc<CapturingInspection>,
pub correlation_key: crate::identity::CorrelationKey,
pub block: Arc<ConfigurableBlockOracle>,
pub audience: Arc<NoopAudienceOracle>,
pub mute: Arc<NoopMuteOracle>,
}
impl BindFixture {
pub fn new() -> Self {
BindFixture {
user: Arc::new(CapturingUserSink::new()),
channel: Arc::new(CapturingChannelSink::new()),
substrate: Arc::new(CapturingSubstrateSink::new()),
moderation: Arc::new(CapturingModerationSink::new()),
fallback: Arc::new(NoopFallback),
inspection: Arc::new(CapturingInspection::new()),
correlation_key: crate::identity::CorrelationKey::from_bytes([0u8; 32]),
block: Arc::new(ConfigurableBlockOracle {
state: BlockState::None,
}),
audience: Arc::new(NoopAudienceOracle),
mute: Arc::new(NoopMuteOracle),
}
}
pub fn with_block_state(state: BlockState) -> Self {
let mut f = Self::new();
f.block = Arc::new(ConfigurableBlockOracle { state });
f
}
pub fn build_ctx(&self, requester: Requester) -> AuthContext<'_> {
AuthContext::new_internal(
requester,
TraceId::from_bytes([0xCD; 16]),
AuditSinks {
user: &*self.user,
channel: &*self.channel,
substrate: &*self.substrate,
moderation: &*self.moderation,
fallback: &*self.fallback,
inspection_queue: &*self.inspection,
correlation_key: &self.correlation_key,
},
OracleSet {
block: &*self.block,
audience: &*self.audience,
mute: &*self.mute,
},
AttributionChain::empty(),
crate::authority::capability::CapabilitySet::empty(),
)
}
}
pub fn sample_did() -> Did {
Did::new("did:plc:phase7dbind").unwrap()
}
pub fn sample_did_other() -> Did {
Did::new("did:plc:phase7dother").unwrap()
}
pub fn sample_resource_id() -> crate::authority::ResourceId {
crate::authority::ResourceId::new(
sample_did(),
crate::Nsid::new("tools.kryphocron.feed.postPrivate").unwrap(),
crate::proto::Rkey::new("3jzfcijpj2z2a").unwrap(),
)
}
}
#[cfg(test)]
mod user_bind_tests {
use super::bind_test_fixtures::*;
use super::*;
use crate::audit::UserAuditEvent;
use crate::authority::v1::{ParticipatePrivate, ViewPrivate};
use crate::authority::{issue_user, BindOutcomeRepr, CapabilityClass, CapabilityKind};
use crate::oracle::{BlockOracleQuery, BlockState};
use std::time::Duration;
fn issue_view_private_for(
ctx: &AuthContext<'_>,
subject: crate::authority::ResourceId,
) -> UserProof<ViewPrivate> {
match issue_user::<ViewPrivate>(ctx, subject) {
Ok(p) => p,
Err(_) => panic!("issuance prerequisite failed"),
}
}
#[tokio::test]
async fn bind_succeeds_with_did_requester_and_clean_state() {
let fixture = BindFixture::new();
let ctx = fixture.build_ctx(Requester::Did(sample_did()));
let proof = issue_view_private_for(&ctx, sample_resource_id());
let r = proof.bind(&ctx, &sample_resource_id()).await;
assert!(r.is_ok(), "happy path bind should succeed");
let captured = fixture.user.captured();
assert_eq!(captured.len(), 1, "exactly one terminal audit event");
match &captured[0] {
UserAuditEvent::CapabilityBound {
capability,
outcome,
..
} => {
assert_eq!(*capability, CapabilityKind::ViewPrivate);
assert!(matches!(outcome, BindOutcomeRepr::Success));
}
other => panic!("expected CapabilityBound, got {other:?}"),
}
}
#[tokio::test]
async fn bind_rejects_target_mismatch_at_precondition() {
let fixture = BindFixture::new();
let ctx = fixture.build_ctx(Requester::Did(sample_did()));
let proof = issue_view_private_for(&ctx, sample_resource_id());
let different_target = crate::authority::ResourceId::new(
sample_did_other(),
crate::Nsid::new("tools.kryphocron.feed.postPrivate").unwrap(),
crate::proto::Rkey::new("3jzfcijpj2z2a").unwrap(),
);
let r = proof.bind(&ctx, &different_target).await;
match r {
Err(BindError::TargetMismatch) => {}
Err(other) => panic!("expected TargetMismatch, got {other:?}"),
Ok(_) => panic!("expected Err, got Ok"),
}
assert_eq!(
fixture.user.captured().len(),
0,
"precondition failure does not emit audit"
);
}
#[tokio::test]
async fn bind_rejects_context_mismatch_at_precondition() {
let fixture = BindFixture::new();
let ctx_a = fixture.build_ctx(Requester::Did(sample_did()));
let proof = issue_view_private_for(&ctx_a, sample_resource_id());
let ctx_b = fixture.build_ctx(Requester::Did(sample_did_other()));
let r = proof.bind(&ctx_b, &sample_resource_id()).await;
assert!(matches!(r, Err(BindError::ContextMismatch)));
assert_eq!(fixture.user.captured().len(), 0);
}
#[tokio::test]
async fn bind_rejects_expired_at_precondition() {
let fixture = BindFixture::new();
let ctx = fixture.build_ctx(Requester::Did(sample_did()));
let backdated_proof = UserProof::<ParticipatePrivate>::new_internal(
sample_did(),
sample_resource_id(),
Instant::now() - Duration::from_secs(200),
AuthorityId::from_bytes([0u8; 16]),
TraceId::from_bytes([0xCD; 16]),
);
let r = backdated_proof.bind(&ctx, &sample_resource_id()).await;
assert!(matches!(r, Err(BindError::Expired)));
assert_eq!(fixture.user.captured().len(), 0);
}
#[tokio::test]
async fn bind_denied_at_block_consultation_when_oracle_returns_blocked() {
let fixture = BindFixture::with_block_state(BlockState::Mutual);
let ctx = fixture.build_ctx(Requester::Did(sample_did()));
let proof = issue_view_private_for(&ctx, sample_resource_id());
let r = proof.bind(&ctx, &sample_resource_id()).await;
let Err(err) = r else {
panic!("expected Err, got Ok");
};
match err {
BindError::DeniedAtPipeline { stage, reason } => {
assert_eq!(stage, PipelineStage::BlockConsultation);
match reason {
DenialReason::Blocked { query, state } => {
assert_eq!(query, BlockOracleQuery::RequesterVsResourceOwner);
assert!(matches!(state, BlockState::Mutual));
}
other => panic!("expected Blocked, got {other:?}"),
}
}
other => panic!("expected DeniedAtPipeline(BlockConsultation), got {other:?}"),
}
let captured = fixture.user.captured();
assert_eq!(captured.len(), 1);
match &captured[0] {
UserAuditEvent::CapabilityIssuanceDenied { reason, .. } => {
assert!(matches!(reason, DenialReason::Blocked { .. }));
}
other => panic!("expected CapabilityIssuanceDenied, got {other:?}"),
}
}
#[tokio::test]
async fn reborrow_succeeds_within_max_age() {
let fixture = BindFixture::new();
let ctx = fixture.build_ctx(Requester::Did(sample_did()));
let proof = issue_view_private_for(&ctx, sample_resource_id());
let bound = match proof.bind(&ctx, &sample_resource_id()).await {
Ok(b) => b,
Err(_) => panic!("bind prerequisite failed"),
};
let captured_after_bind = fixture.user.captured().len();
let r = bound.reborrow(&ctx).await;
assert!(r.is_ok(), "reborrow within MAX_AGE should succeed");
assert_eq!(
fixture.user.captured().len(),
captured_after_bind,
"successful reborrow is silent (no audit emit)"
);
}
#[tokio::test]
async fn reborrow_returns_expired_past_max_age_and_emits_event() {
let fixture = BindFixture::new();
let ctx = fixture.build_ctx(Requester::Did(sample_did()));
let backdated = UserProof::<ParticipatePrivate>::new_internal(
sample_did(),
sample_resource_id(),
Instant::now() - Duration::from_secs(100),
AuthorityId::from_bytes([0u8; 16]),
TraceId::from_bytes([0xCD; 16]),
);
let bound = BoundUserProof {
proof: backdated,
_life: PhantomData,
};
let r = bound.reborrow(&ctx).await;
assert!(matches!(r, Err(BindFailureReason::Expired)));
let captured = fixture.user.captured();
assert_eq!(captured.len(), 1);
match &captured[0] {
UserAuditEvent::ReborrowFailed { reason, .. } => {
assert!(matches!(reason, BindFailureReason::Expired));
}
other => panic!("expected ReborrowFailed, got {other:?}"),
}
}
#[tokio::test]
async fn reborrow_does_not_re_consult_oracles() {
let fixture = BindFixture::new();
let ctx = fixture.build_ctx(Requester::Did(sample_did()));
let proof = issue_view_private_for(&ctx, sample_resource_id());
let bound = match proof.bind(&ctx, &sample_resource_id()).await {
Ok(b) => b,
Err(_) => panic!("bind prerequisite failed"),
};
let blocked_fixture = BindFixture::with_block_state(BlockState::Mutual);
let blocked_ctx = blocked_fixture.build_ctx(Requester::Did(sample_did()));
let r = bound.reborrow(&blocked_ctx).await;
assert!(
r.is_ok(),
"reborrow within MAX_AGE succeeds even with blocked oracle (oracles not re-consulted)"
);
}
#[test]
fn user_class_discriminator_pinned() {
assert_eq!(
CapabilityKind::ViewPrivate.class(),
CapabilityClass::User
);
}
}
#[cfg(test)]
mod channel_bind_tests {
use super::bind_test_fixtures::*;
use super::*;
use crate::audit::ChannelAuditEvent;
use crate::authority::v1::EmitToSyncChannel;
use crate::authority::{issue_channel, BindOutcomeRepr, CapabilityClass, CapabilityKind};
use std::time::Duration;
fn sample_channel_subject() -> crate::authority::ChannelBinding {
crate::authority::ChannelBinding {
peer: crate::identity::ServiceIdentity::new_internal(
sample_did(),
crate::identity::KeyId::from_bytes([0u8; 32]),
crate::identity::PublicKey {
algorithm: crate::identity::SignatureAlgorithm::Ed25519,
bytes: [0u8; 32],
},
None,
),
session_id: crate::identity::SessionId::from_bytes([0u8; 32]),
}
}
fn issue_emit_for(
ctx: &AuthContext<'_>,
subject: crate::authority::ChannelBinding,
) -> ChannelProof<EmitToSyncChannel> {
match issue_channel::<EmitToSyncChannel>(ctx, subject) {
Ok(p) => p,
Err(_) => panic!("issuance prerequisite failed"),
}
}
#[tokio::test]
async fn bind_succeeds_with_did_requester() {
let fixture = BindFixture::new();
let ctx = fixture.build_ctx(Requester::Did(sample_did()));
let proof = issue_emit_for(&ctx, sample_channel_subject());
let r = proof.bind(&ctx, &sample_channel_subject()).await;
assert!(r.is_ok());
let captured = fixture.channel.captured();
assert_eq!(captured.len(), 1);
match &captured[0] {
ChannelAuditEvent::ChannelBound {
endpoint, outcome, ..
} => {
assert_eq!(*endpoint, CapabilityKind::EmitToSyncChannel);
assert!(matches!(outcome, BindOutcomeRepr::Success));
}
other => panic!("expected ChannelBound, got {other:?}"),
}
}
#[tokio::test]
async fn bind_rejects_target_mismatch_at_precondition() {
let fixture = BindFixture::new();
let ctx = fixture.build_ctx(Requester::Did(sample_did()));
let proof = issue_emit_for(&ctx, sample_channel_subject());
let different_target = crate::authority::ChannelBinding {
peer: crate::identity::ServiceIdentity::new_internal(
sample_did(),
crate::identity::KeyId::from_bytes([0u8; 32]),
crate::identity::PublicKey {
algorithm: crate::identity::SignatureAlgorithm::Ed25519,
bytes: [0u8; 32],
},
None,
),
session_id: crate::identity::SessionId::from_bytes([0xFF; 32]),
};
let r = proof.bind(&ctx, &different_target).await;
assert!(matches!(r, Err(BindError::TargetMismatch)));
assert_eq!(fixture.channel.captured().len(), 0);
}
#[tokio::test]
async fn bind_rejects_context_mismatch_at_precondition() {
let fixture = BindFixture::new();
let ctx_a = fixture.build_ctx(Requester::Did(sample_did()));
let proof = issue_emit_for(&ctx_a, sample_channel_subject());
let ctx_b = fixture.build_ctx(Requester::Did(sample_did_other()));
let r = proof.bind(&ctx_b, &sample_channel_subject()).await;
assert!(matches!(r, Err(BindError::ContextMismatch)));
assert_eq!(fixture.channel.captured().len(), 0);
}
#[tokio::test]
async fn reborrow_succeeds_within_max_age() {
let fixture = BindFixture::new();
let ctx = fixture.build_ctx(Requester::Did(sample_did()));
let proof = issue_emit_for(&ctx, sample_channel_subject());
let bound = match proof.bind(&ctx, &sample_channel_subject()).await {
Ok(b) => b,
Err(_) => panic!("bind prerequisite failed"),
};
let captured_after_bind = fixture.channel.captured().len();
let r = bound.reborrow(&ctx).await;
assert!(r.is_ok());
assert_eq!(
fixture.channel.captured().len(),
captured_after_bind,
"successful reborrow is silent"
);
}
#[tokio::test]
async fn reborrow_returns_expired_past_max_age_and_emits_event() {
let fixture = BindFixture::new();
let ctx = fixture.build_ctx(Requester::Did(sample_did()));
let backdated = ChannelProof::<EmitToSyncChannel>::new_internal(
sample_did(),
sample_channel_subject(),
Instant::now() - Duration::from_secs(100),
AuthorityId::from_bytes([0u8; 16]),
TraceId::from_bytes([0xCD; 16]),
);
let bound = BoundChannelProof {
proof: backdated,
_life: PhantomData,
};
let r = bound.reborrow(&ctx).await;
assert!(matches!(r, Err(BindFailureReason::Expired)));
let captured = fixture.channel.captured();
assert_eq!(captured.len(), 1);
match &captured[0] {
ChannelAuditEvent::ChannelReborrowFailed { reason, .. } => {
assert!(matches!(reason, BindFailureReason::Expired));
}
other => panic!("expected ChannelReborrowFailed, got {other:?}"),
}
}
#[test]
fn channel_class_discriminator_pinned() {
assert_eq!(
CapabilityKind::EmitToSyncChannel.class(),
CapabilityClass::Channel
);
}
}
#[cfg(test)]
mod substrate_bind_tests {
use super::bind_test_fixtures::*;
use super::*;
use crate::audit::SubstrateAuditEvent;
use crate::authority::v1::ScanShard;
use crate::authority::{
issue_substrate, BindOutcomeRepr, CapabilityClass, CapabilityKind,
};
use std::time::Duration;
fn sample_substrate_subject() -> crate::authority::ScopeSelector {
crate::authority::ScopeSelector::Shard(
crate::authority::ShardRange::new(
crate::authority::ShardId::from_bytes([0; 8]),
crate::authority::ShardId::from_bytes([0xFF; 8]),
)
.unwrap(),
)
}
fn sample_service_identity() -> crate::identity::ServiceIdentity {
crate::identity::ServiceIdentity::new_internal(
sample_did(),
crate::identity::KeyId::from_bytes([0u8; 32]),
crate::identity::PublicKey {
algorithm: crate::identity::SignatureAlgorithm::Ed25519,
bytes: [0u8; 32],
},
None,
)
}
fn issue_scan_for(
ctx: &AuthContext<'_>,
subject: crate::authority::ScopeSelector,
) -> SubstrateProof<ScanShard> {
match issue_substrate::<ScanShard>(ctx, subject) {
Ok(p) => p,
Err(_) => panic!("issuance prerequisite failed"),
}
}
#[tokio::test]
async fn bind_succeeds_with_service_requester() {
let fixture = BindFixture::new();
let ctx = fixture.build_ctx(Requester::Service(sample_service_identity()));
let proof = issue_scan_for(&ctx, sample_substrate_subject());
let r = proof.bind(&ctx, &sample_substrate_subject()).await;
assert!(r.is_ok());
let captured = fixture.substrate.captured();
assert_eq!(captured.len(), 1);
match &captured[0] {
SubstrateAuditEvent::ScopeBound {
capability, outcome, ..
} => {
assert_eq!(*capability, CapabilityKind::ScanShard);
assert!(matches!(outcome, BindOutcomeRepr::Success));
}
other => panic!("expected ScopeBound, got {other:?}"),
}
}
#[tokio::test]
async fn bind_rejects_target_mismatch_at_precondition() {
let fixture = BindFixture::new();
let ctx = fixture.build_ctx(Requester::Service(sample_service_identity()));
let proof = issue_scan_for(&ctx, sample_substrate_subject());
let different_target = crate::authority::ScopeSelector::Shard(
crate::authority::ShardRange::new(
crate::authority::ShardId::from_bytes([0x10; 8]),
crate::authority::ShardId::from_bytes([0x20; 8]),
)
.unwrap(),
);
let r = proof.bind(&ctx, &different_target).await;
assert!(matches!(r, Err(BindError::TargetMismatch)));
assert_eq!(fixture.substrate.captured().len(), 0);
}
#[tokio::test]
async fn bind_rejects_context_mismatch_at_precondition() {
let fixture = BindFixture::new();
let svc_a = sample_service_identity();
let ctx_a = fixture.build_ctx(Requester::Service(svc_a));
let proof = issue_scan_for(&ctx_a, sample_substrate_subject());
let svc_b = crate::identity::ServiceIdentity::new_internal(
sample_did_other(),
crate::identity::KeyId::from_bytes([0u8; 32]),
crate::identity::PublicKey {
algorithm: crate::identity::SignatureAlgorithm::Ed25519,
bytes: [0u8; 32],
},
None,
);
let ctx_b = fixture.build_ctx(Requester::Service(svc_b));
let r = proof.bind(&ctx_b, &sample_substrate_subject()).await;
assert!(matches!(r, Err(BindError::ContextMismatch)));
assert_eq!(fixture.substrate.captured().len(), 0);
}
#[tokio::test]
async fn reborrow_succeeds_within_max_age() {
let fixture = BindFixture::new();
let ctx = fixture.build_ctx(Requester::Service(sample_service_identity()));
let proof = issue_scan_for(&ctx, sample_substrate_subject());
let bound = match proof.bind(&ctx, &sample_substrate_subject()).await {
Ok(b) => b,
Err(_) => panic!("bind prerequisite failed"),
};
let captured_after_bind = fixture.substrate.captured().len();
let r = bound.reborrow(&ctx).await;
assert!(r.is_ok());
assert_eq!(
fixture.substrate.captured().len(),
captured_after_bind,
"successful reborrow is silent"
);
}
#[tokio::test]
async fn reborrow_returns_expired_past_max_age_and_emits_event() {
let fixture = BindFixture::new();
let ctx = fixture.build_ctx(Requester::Service(sample_service_identity()));
let backdated = SubstrateProof::<ScanShard>::new_internal(
sample_did(),
sample_substrate_subject(),
Instant::now() - Duration::from_secs(200),
AuthorityId::from_bytes([0u8; 16]),
TraceId::from_bytes([0xCD; 16]),
);
let bound = BoundSubstrateProof {
proof: backdated,
_life: PhantomData,
};
let r = bound.reborrow(&ctx).await;
assert!(matches!(r, Err(BindFailureReason::Expired)));
let captured = fixture.substrate.captured();
assert_eq!(captured.len(), 1);
match &captured[0] {
SubstrateAuditEvent::ScopeBound { outcome, .. } => {
assert!(
matches!(outcome, BindOutcomeRepr::Expired { .. }),
"expected outcome=Expired, got {outcome:?}"
);
}
other => panic!("expected ScopeBound{{outcome: Expired}}, got {other:?}"),
}
}
#[test]
fn substrate_class_discriminator_pinned() {
assert_eq!(
CapabilityKind::ScanShard.class(),
CapabilityClass::Substrate
);
}
}
#[cfg(test)]
mod moderation_bind_tests {
use super::bind_test_fixtures::*;
use super::*;
use crate::audit::{ModerationAuditEvent, ModeratorRationale};
use crate::authority::v1::{ModeratorRead, ModeratorTakedown};
use crate::authority::{
issue_moderation, BindOutcomeRepr, CapabilityClass, CapabilityKind, InspectionKind,
};
use std::time::Duration;
fn sample_moderation_subject() -> crate::authority::ModerationSubject {
crate::authority::ModerationSubject {
resource: sample_resource_id(),
case: crate::authority::ModerationCaseId::from_bytes([0u8; 16]),
}
}
fn sample_service_identity() -> crate::identity::ServiceIdentity {
crate::identity::ServiceIdentity::new_internal(
sample_did(),
crate::identity::KeyId::from_bytes([0u8; 32]),
crate::identity::PublicKey {
algorithm: crate::identity::SignatureAlgorithm::Ed25519,
bytes: [0u8; 32],
},
None,
)
}
fn sample_rationale() -> ModeratorRationale {
ModeratorRationale::Declared(
crate::audit::BoundedString::<{ crate::audit::MAX_RATIONALE_LEN }>::new(
"v0.1 test rationale",
)
.unwrap(),
)
}
fn issue_modread_for(
ctx: &AuthContext<'_>,
subject: crate::authority::ModerationSubject,
) -> ModerationProof<ModeratorRead> {
match issue_moderation::<ModeratorRead>(ctx, subject) {
Ok(p) => p,
Err(_) => panic!("issuance prerequisite failed"),
}
}
#[tokio::test]
async fn bind_moderator_read_succeeds_and_dual_emits() {
let fixture = BindFixture::new();
let ctx = fixture.build_ctx(Requester::Service(sample_service_identity()));
let proof = issue_modread_for(&ctx, sample_moderation_subject());
let r = proof
.bind(&ctx, &sample_moderation_subject(), sample_rationale())
.await;
assert!(r.is_ok());
let captured_audit = fixture.moderation.captured();
assert_eq!(captured_audit.len(), 1);
match &captured_audit[0] {
ModerationAuditEvent::ModeratorInspected { rationale, .. } => {
assert!(matches!(rationale, ModeratorRationale::Declared(_)));
}
other => panic!("expected ModeratorInspected, got {other:?}"),
}
let captured_notifications = fixture.inspection.captured();
assert_eq!(
captured_notifications.len(),
1,
"ModeratorRead bind enqueues one InspectionNotification"
);
let (_owner, notification) = &captured_notifications[0];
match ¬ification.kind {
InspectionKind::ModeratorRead { rationale, .. } => {
assert!(matches!(rationale, ModeratorRationale::Declared(_)));
}
other => panic!("expected InspectionKind::ModeratorRead, got {other:?}"),
}
assert_eq!(
notification.trace_id,
match &captured_audit[0] {
ModerationAuditEvent::ModeratorInspected { trace_id, .. } => *trace_id,
_ => panic!("unreachable"),
},
"audit event and inspection notification share trace_id"
);
}
#[tokio::test]
async fn bind_moderator_takedown_emits_took_down_variant() {
let fixture = BindFixture::new();
let ctx = fixture.build_ctx(Requester::Service(sample_service_identity()));
let proof = match issue_moderation::<ModeratorTakedown>(&ctx, sample_moderation_subject()) {
Ok(p) => p,
Err(_) => panic!("issuance prerequisite failed"),
};
let r = proof
.bind(&ctx, &sample_moderation_subject(), sample_rationale())
.await;
assert!(r.is_ok());
let captured = fixture.moderation.captured();
assert_eq!(captured.len(), 1);
match &captured[0] {
ModerationAuditEvent::ModeratorTookDown { outcome, .. } => {
assert!(matches!(outcome, BindOutcomeRepr::Success));
}
other => panic!("expected ModeratorTookDown, got {other:?}"),
}
let captured_notifications = fixture.inspection.captured();
assert_eq!(captured_notifications.len(), 1);
match &captured_notifications[0].1.kind {
InspectionKind::Takedown { .. } => {}
other => panic!("expected InspectionKind::Takedown, got {other:?}"),
}
}
#[tokio::test]
async fn bind_rejects_target_mismatch_at_precondition() {
let fixture = BindFixture::new();
let ctx = fixture.build_ctx(Requester::Service(sample_service_identity()));
let proof = issue_modread_for(&ctx, sample_moderation_subject());
let different_target = crate::authority::ModerationSubject {
resource: sample_resource_id(),
case: crate::authority::ModerationCaseId::from_bytes([0xFF; 16]),
};
let r = proof
.bind(&ctx, &different_target, sample_rationale())
.await;
assert!(matches!(r, Err(BindError::TargetMismatch)));
assert_eq!(fixture.moderation.captured().len(), 0);
assert_eq!(
fixture.inspection.captured().len(),
0,
"precondition failure emits NEITHER audit NOR inspection"
);
}
#[tokio::test]
async fn denial_path_skips_inspection_emit() {
let fixture = BindFixture::new();
let ctx = fixture.build_ctx(Requester::Service(sample_service_identity()));
let proof = issue_modread_for(&ctx, sample_moderation_subject());
let svc_b = crate::identity::ServiceIdentity::new_internal(
sample_did_other(),
crate::identity::KeyId::from_bytes([0u8; 32]),
crate::identity::PublicKey {
algorithm: crate::identity::SignatureAlgorithm::Ed25519,
bytes: [0u8; 32],
},
None,
);
let ctx_b = fixture.build_ctx(Requester::Service(svc_b));
let r = proof
.bind(&ctx_b, &sample_moderation_subject(), sample_rationale())
.await;
assert!(matches!(r, Err(BindError::ContextMismatch)));
assert_eq!(fixture.moderation.captured().len(), 0);
assert_eq!(fixture.inspection.captured().len(), 0);
}
#[tokio::test]
async fn reborrow_succeeds_within_max_age() {
let fixture = BindFixture::new();
let ctx = fixture.build_ctx(Requester::Service(sample_service_identity()));
let proof = issue_modread_for(&ctx, sample_moderation_subject());
let bound = match proof
.bind(&ctx, &sample_moderation_subject(), sample_rationale())
.await
{
Ok(b) => b,
Err(_) => panic!("bind prerequisite failed"),
};
let captured_after_bind = fixture.moderation.captured().len();
let r = bound.reborrow(&ctx).await;
assert!(r.is_ok());
assert_eq!(
fixture.moderation.captured().len(),
captured_after_bind,
"successful reborrow is silent"
);
}
#[tokio::test]
async fn reborrow_returns_expired_past_max_age_silently() {
let fixture = BindFixture::new();
let ctx = fixture.build_ctx(Requester::Service(sample_service_identity()));
let backdated = ModerationProof::<ModeratorRead>::new_internal(
sample_did(),
sample_moderation_subject(),
Instant::now() - Duration::from_secs(60),
AuthorityId::from_bytes([0u8; 16]),
TraceId::from_bytes([0xCD; 16]),
);
let bound = BoundModerationProof {
proof: backdated,
_life: PhantomData,
};
let r = bound.reborrow(&ctx).await;
assert!(matches!(r, Err(BindFailureReason::Expired)));
assert_eq!(
fixture.moderation.captured().len(),
0,
"moderation reborrow miss is silent at the audit layer (v0.2 enrichment for emit)"
);
}
#[test]
fn moderation_class_discriminator_pinned() {
assert_eq!(
CapabilityKind::ModeratorRead.class(),
CapabilityClass::Moderation
);
}
}