pub(crate) mod capability;
pub(crate) mod moderation;
pub(crate) mod predicate;
pub(crate) mod proof;
pub(crate) mod subjects;
pub(crate) mod v1;
use std::sync::OnceLock;
use std::time::Instant;
use crate::ingress::{AuthContext, Requester};
use crate::proto::Did;
pub use self::capability::{
CapabilityClass, CapabilityKind, CapabilitySemantics, CapabilitySet, Endpoint,
ModerationCapability, OracleConsultations, OracleResultsForCapability, SubstrateScope,
UserCapability,
};
pub use self::moderation::{
InspectionKind, InspectionNotification, InspectionNotificationQueueImpl,
InspectionNotificationQueueReader, NoInspectionNotifications, NotificationId,
};
pub use self::predicate::{
AuthDenial, BindError, BindFailureReason, BindOutcomeRepr, DenialReason, IssuancePolicy,
PipelineStage, PredicateContext, SemVer,
};
pub use self::proof::{
AuthorityId, BoundChannelProof, BoundModerationProof, BoundSubstrateProof,
BoundUserProof, ChannelProof, ChannelProofRef, ModerationProof, ModerationProofRef,
SubstrateProof, SubstrateProofRef, UserProof, UserProofRef,
};
pub use self::subjects::{
AudienceListId, ChannelBinding, HasResourceLocation, ManageAudienceSubject,
ModerationCaseId, ModerationSubject, RecordStateFilter, ResourceId, ScopeError,
ScopeSelector, ShardId, ShardRange, TimeWindow,
};
pub use self::v1::{
AppViewSync, DeletePrivatePost, DeletePrivatePostOracleResults, EditPrivatePost,
EditPrivatePostOracleResults, EmitToSyncChannel, GarbageCollect, GraphSync,
ManageAudience, ManageAudienceOracleResults, ModeratorRead, ModeratorRestore,
ModeratorTakedown, ParticipatePrivate, ParticipatePrivateOracleResults,
ReplicatePrivate, ScanShard, ViewPrivate, ViewPrivateOracleResults,
};
pub fn issue_user<C>(
ctx: &AuthContext<'_>,
subject: <C as UserCapability>::Subject,
) -> Result<UserProof<C>, AuthDenial>
where
C: UserCapability + IssuancePolicy,
{
let requester = stage1_extract_requester_did(ctx, CapabilityClass::User, true)?;
Ok(UserProof::new_internal(
requester,
subject,
Instant::now(),
process_authority_id(),
ctx.trace_id(),
))
}
pub fn issue_channel<E>(
ctx: &AuthContext<'_>,
subject: <E as Endpoint>::Subject,
) -> Result<ChannelProof<E>, AuthDenial>
where
E: Endpoint,
{
let requester = stage1_extract_requester_did(ctx, CapabilityClass::Channel, true)?;
Ok(ChannelProof::new_internal(
requester,
subject,
Instant::now(),
process_authority_id(),
ctx.trace_id(),
))
}
pub fn issue_substrate<S>(
ctx: &AuthContext<'_>,
subject: <S as SubstrateScope>::Subject,
) -> Result<SubstrateProof<S>, AuthDenial>
where
S: SubstrateScope,
{
let requester = stage1_extract_requester_did(ctx, CapabilityClass::Substrate, false)?;
Ok(SubstrateProof::new_internal(
requester,
subject,
Instant::now(),
process_authority_id(),
ctx.trace_id(),
))
}
pub fn issue_moderation<C>(
ctx: &AuthContext<'_>,
subject: <C as ModerationCapability>::Subject,
) -> Result<ModerationProof<C>, AuthDenial>
where
C: ModerationCapability,
{
let requester = stage1_extract_requester_did(ctx, CapabilityClass::Moderation, false)?;
Ok(ModerationProof::new_internal(
requester,
subject,
Instant::now(),
process_authority_id(),
ctx.trace_id(),
))
}
pub fn check_jwt_scope_for<C: IssuancePolicy>(
jwt_scope: &crate::verification::JwtScope,
) -> Result<(), AuthDenial> {
check_jwt_scope_required(<C as IssuancePolicy>::required_jwt_scope(), jwt_scope)
}
pub(crate) fn check_jwt_scope_required(
required: Option<&'static str>,
jwt_scope: &crate::verification::JwtScope,
) -> Result<(), AuthDenial> {
let Some(required) = required else {
return Ok(());
};
if jwt_scope.scopes.iter().any(|s| s == required) {
return Ok(());
}
Err(AuthDenial::ScopeMismatch {
required: required.to_string(),
granted: jwt_scope.scopes.clone(),
})
}
fn stage1_extract_requester_did(
ctx: &AuthContext<'_>,
class: CapabilityClass,
accept_did: bool,
) -> Result<Did, AuthDenial> {
match ctx.requester() {
Requester::Did(did) if accept_did => Ok(did.clone()),
Requester::Service(service) => Ok(service.service_did().clone()),
other => Err(AuthDenial::RequesterLacksAuthority {
class,
found: other.kind(),
}),
}
}
pub(crate) fn check_stage_0_deprecation(
nsid: &crate::proto::Nsid,
now_unix_seconds: i64,
) -> Result<(), DenialReason> {
use kryphocron_lexicons::DeprecationState;
for entry in crate::KRYPHOCRON_LEXICON_REGISTRY {
if entry.nsid == nsid.as_str() {
return match entry.deprecation {
DeprecationState::Active => Ok(()),
DeprecationState::Deprecated {
since_version,
successor,
} => Err(DenialReason::CapabilityDeprecated {
nsid: entry.nsid,
since_version,
successor,
}),
DeprecationState::DeprecatedWithGrace {
since_version,
grace_until_unix_seconds,
successor,
} => {
if now_unix_seconds > grace_until_unix_seconds {
Err(DenialReason::CapabilityDeprecated {
nsid: entry.nsid,
since_version,
successor,
})
} else {
Ok(())
}
}
_ => Err(DenialReason::CapabilityDeprecated {
nsid: entry.nsid,
since_version: kryphocron_lexicons::SemVer::new(0, 0, 0),
successor: None,
}),
};
}
}
Ok(())
}
fn process_authority_id() -> AuthorityId {
static AUTHORITY_ID: OnceLock<AuthorityId> = OnceLock::new();
*AUTHORITY_ID.get_or_init(|| {
let mut bytes = [0u8; 16];
getrandom::getrandom(&mut bytes)
.expect("§4.3 authority-id init: OS CSPRNG unavailable");
AuthorityId::from_bytes(bytes)
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::authority::v1::{
EmitToSyncChannel, ModeratorRead, ScanShard, ViewPrivate,
};
use crate::verification::JwtScope;
use smallvec::{smallvec, SmallVec};
#[test]
fn no_scope_required_succeeds_with_empty_jwt_scope() {
let empty = JwtScope { scopes: SmallVec::new() };
check_jwt_scope_for::<ViewPrivate>(&empty).unwrap();
check_jwt_scope_required(None, &empty).unwrap();
}
#[test]
fn no_scope_required_succeeds_with_arbitrary_jwt_scope() {
let some = JwtScope {
scopes: smallvec!["whatever".to_string()],
};
check_jwt_scope_for::<ViewPrivate>(&some).unwrap();
check_jwt_scope_required(None, &some).unwrap();
}
#[test]
fn scope_match_succeeds() {
let granted = JwtScope {
scopes: smallvec![
"tools.kryphocron.test.scope".to_string(),
"tools.kryphocron.other".to_string(),
],
};
check_jwt_scope_required(Some("tools.kryphocron.test.scope"), &granted).unwrap();
}
#[test]
fn scope_mismatch_returns_scope_mismatch() {
let granted = JwtScope {
scopes: smallvec!["other.scope".to_string()],
};
let err = check_jwt_scope_required(Some("tools.kryphocron.test.scope"), &granted)
.unwrap_err();
match err {
AuthDenial::ScopeMismatch { required, granted: g } => {
assert_eq!(required, "tools.kryphocron.test.scope");
assert_eq!(g.as_slice(), &["other.scope"]);
}
other => panic!("expected ScopeMismatch, got {other:?}"),
}
}
#[test]
fn empty_scope_against_required_capability_fails_closed() {
let empty = JwtScope { scopes: SmallVec::new() };
let err = check_jwt_scope_required(Some("tools.kryphocron.test.scope"), &empty)
.unwrap_err();
match err {
AuthDenial::ScopeMismatch { required, granted } => {
assert_eq!(required, "tools.kryphocron.test.scope");
assert!(granted.is_empty());
}
other => panic!("expected ScopeMismatch, got {other:?}"),
}
}
#[test]
fn jwt_scope_insufficient_and_pipeline_stage_jwt_scope_reachable() {
let _r = DenialReason::JwtScopeInsufficient {
required: "scope".to_string(),
granted: SmallVec::new(),
};
let _b = BindOutcomeRepr::DeniedAtPipeline {
stage: PipelineStage::JwtScope,
reason: DenialReason::JwtScopeInsufficient {
required: "scope".to_string(),
granted: SmallVec::new(),
},
};
}
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use crate::audit::{
AuditError, ChannelAuditEvent, ChannelAuditSink, CompositeOpId, FallbackAuditEvent,
FallbackAuditSink, ModerationAuditEvent, ModerationAuditSink, SinkKind,
SubstrateAuditEvent, SubstrateAuditSink, UserAuditEvent, UserAuditSink,
};
use crate::authority::subjects::{
ChannelBinding, ModerationCaseId, ModerationSubject, ResourceId, ScopeSelector,
ShardId, ShardRange,
};
use crate::identity::{
KeyId, PublicKey, ServiceIdentity, SessionId, SignatureAlgorithm, TraceId,
};
use crate::ingress::{AttributionChain, AuditSinks, OracleSet, RequesterKind};
use crate::oracle::{
AudienceOracle, AudienceOracleQuery, AudienceState, BlockOracle, BlockOracleQuery,
BlockState, MuteOracle, MuteOracleQuery, MuteState,
};
use crate::proto::{Did, Nsid, Rkey};
struct NoopUserSink;
impl UserAuditSink for NoopUserSink {
fn record(&self, _: UserAuditEvent) -> Result<(), AuditError> {
Ok(())
}
}
struct NoopChannelSink;
impl ChannelAuditSink for NoopChannelSink {
fn record(&self, _: ChannelAuditEvent) -> Result<(), AuditError> {
Ok(())
}
}
struct NoopSubstrateSink;
impl SubstrateAuditSink for NoopSubstrateSink {
fn record(&self, _: SubstrateAuditEvent) -> Result<(), AuditError> {
Ok(())
}
}
struct NoopModerationSink;
impl ModerationAuditSink for NoopModerationSink {
fn record(&self, _: ModerationAuditEvent) -> Result<(), AuditError> {
Ok(())
}
}
struct NoopFallback;
impl FallbackAuditSink for NoopFallback {
fn record_panic(
&self,
_: SinkKind,
_: TraceId,
_: crate::authority::capability::CapabilityKind,
_: SystemTime,
) {
}
fn record_composite_failure(
&self,
_: TraceId,
_: CompositeOpId,
_: &[SinkKind],
_: &[SinkKind],
_: SystemTime,
) {
}
fn record_event(&self, _: FallbackAuditEvent) {}
}
struct NoopBlockOracle;
impl BlockOracle for NoopBlockOracle {
fn block_state(&self, _: &Did, _: &Did) -> BlockState {
BlockState::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, _: BlockOracleQuery) -> Duration {
Duration::ZERO
}
}
struct NoopAudienceOracle;
impl AudienceOracle for NoopAudienceOracle {
fn audience_state(&self, _: &Did, _: &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
}
}
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
}
}
fn sample_did() -> Did {
Did::new("did:plc:phase7ctest").unwrap()
}
fn sample_service_identity() -> ServiceIdentity {
ServiceIdentity::new_internal(
sample_did(),
KeyId::from_bytes([0u8; 32]),
PublicKey {
algorithm: SignatureAlgorithm::Ed25519,
bytes: [0u8; 32],
},
None,
)
}
fn sample_resource_id() -> ResourceId {
ResourceId::new(
sample_did(),
Nsid::new("tools.kryphocron.feed.postPrivate").unwrap(),
Rkey::new("3jzfcijpj2z2a").unwrap(),
)
}
fn sample_channel_subject() -> ChannelBinding {
ChannelBinding {
peer: sample_service_identity(),
session_id: SessionId::from_bytes([0u8; 32]),
}
}
fn sample_substrate_subject() -> ScopeSelector {
ScopeSelector::Shard(
ShardRange::new(ShardId::from_bytes([0; 8]), ShardId::from_bytes([0xFF; 8]))
.unwrap(),
)
}
fn sample_moderation_subject() -> ModerationSubject {
ModerationSubject {
resource: sample_resource_id(),
case: ModerationCaseId::from_bytes([0u8; 16]),
}
}
struct ContextFixture {
_user: Arc<NoopUserSink>,
_channel: Arc<NoopChannelSink>,
_substrate: Arc<NoopSubstrateSink>,
_moderation: Arc<NoopModerationSink>,
_fallback: Arc<NoopFallback>,
_inspection: Arc<NoInspectionNotifications>,
_correlation_key: crate::identity::CorrelationKey,
_block: Arc<NoopBlockOracle>,
_audience: Arc<NoopAudienceOracle>,
_mute: Arc<NoopMuteOracle>,
}
impl ContextFixture {
fn new() -> Self {
ContextFixture {
_user: Arc::new(NoopUserSink),
_channel: Arc::new(NoopChannelSink),
_substrate: Arc::new(NoopSubstrateSink),
_moderation: Arc::new(NoopModerationSink),
_fallback: Arc::new(NoopFallback),
_inspection: Arc::new(NoInspectionNotifications),
_correlation_key: crate::identity::CorrelationKey::from_bytes([0u8; 32]),
_block: Arc::new(NoopBlockOracle),
_audience: Arc::new(NoopAudienceOracle),
_mute: Arc::new(NoopMuteOracle),
}
}
fn build_ctx(&self, requester: Requester) -> AuthContext<'_> {
AuthContext::new_internal(
requester,
TraceId::from_bytes([0xAB; 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(),
)
}
}
#[test]
fn issue_user_succeeds_with_did_requester() {
let fixture = ContextFixture::new();
let ctx = fixture.build_ctx(Requester::Did(sample_did()));
let r = issue_user::<ViewPrivate>(&ctx, sample_resource_id());
assert!(r.is_ok(), "user-class issuance with Did requester should succeed");
}
#[test]
fn issue_channel_succeeds_with_did_requester() {
let fixture = ContextFixture::new();
let ctx = fixture.build_ctx(Requester::Did(sample_did()));
let r = issue_channel::<EmitToSyncChannel>(&ctx, sample_channel_subject());
assert!(r.is_ok(), "channel-class issuance with Did requester should succeed");
}
#[test]
fn issue_substrate_succeeds_with_service_requester() {
let fixture = ContextFixture::new();
let ctx = fixture.build_ctx(Requester::Service(sample_service_identity()));
let r = issue_substrate::<ScanShard>(&ctx, sample_substrate_subject());
assert!(r.is_ok(), "substrate-class issuance with Service requester should succeed");
}
#[test]
fn issue_moderation_succeeds_with_service_requester() {
let fixture = ContextFixture::new();
let ctx = fixture.build_ctx(Requester::Service(sample_service_identity()));
let r = issue_moderation::<ModeratorRead>(&ctx, sample_moderation_subject());
assert!(r.is_ok(), "moderation-class issuance with Service requester should succeed");
}
#[test]
fn issue_user_rejects_anonymous_at_stage_1() {
let fixture = ContextFixture::new();
let ctx = fixture.build_ctx(Requester::Anonymous);
let Err(err) = issue_user::<ViewPrivate>(&ctx, sample_resource_id()) else {
panic!("expected Err, got Ok");
};
match err {
AuthDenial::RequesterLacksAuthority { class, found } => {
assert_eq!(class, CapabilityClass::User);
assert_eq!(found, RequesterKind::Anonymous);
}
other => panic!("expected RequesterLacksAuthority(User, Anonymous), got {other:?}"),
}
}
#[test]
fn issue_channel_rejects_anonymous_at_stage_1() {
let fixture = ContextFixture::new();
let ctx = fixture.build_ctx(Requester::Anonymous);
let Err(err) = issue_channel::<EmitToSyncChannel>(&ctx, sample_channel_subject())
else {
panic!("expected Err, got Ok");
};
match err {
AuthDenial::RequesterLacksAuthority { class, found } => {
assert_eq!(class, CapabilityClass::Channel);
assert_eq!(found, RequesterKind::Anonymous);
}
other => panic!(
"expected RequesterLacksAuthority(Channel, Anonymous), got {other:?}"
),
}
}
#[test]
fn issue_substrate_rejects_anonymous_at_stage_1() {
let fixture = ContextFixture::new();
let ctx = fixture.build_ctx(Requester::Anonymous);
let Err(err) = issue_substrate::<ScanShard>(&ctx, sample_substrate_subject()) else {
panic!("expected Err, got Ok");
};
match err {
AuthDenial::RequesterLacksAuthority { class, found } => {
assert_eq!(class, CapabilityClass::Substrate);
assert_eq!(found, RequesterKind::Anonymous);
}
other => panic!(
"expected RequesterLacksAuthority(Substrate, Anonymous), got {other:?}"
),
}
}
#[test]
fn issue_moderation_rejects_anonymous_at_stage_1() {
let fixture = ContextFixture::new();
let ctx = fixture.build_ctx(Requester::Anonymous);
let Err(err) =
issue_moderation::<ModeratorRead>(&ctx, sample_moderation_subject())
else {
panic!("expected Err, got Ok");
};
match err {
AuthDenial::RequesterLacksAuthority { class, found } => {
assert_eq!(class, CapabilityClass::Moderation);
assert_eq!(found, RequesterKind::Anonymous);
}
other => panic!(
"expected RequesterLacksAuthority(Moderation, Anonymous), got {other:?}"
),
}
}
#[test]
fn issue_substrate_rejects_did_at_stage_1_per_4_6() {
let fixture = ContextFixture::new();
let ctx = fixture.build_ctx(Requester::Did(sample_did()));
let Err(err) = issue_substrate::<ScanShard>(&ctx, sample_substrate_subject()) else {
panic!("expected Err, got Ok");
};
match err {
AuthDenial::RequesterLacksAuthority { class, found } => {
assert_eq!(class, CapabilityClass::Substrate);
assert_eq!(found, RequesterKind::Did);
}
other => panic!(
"expected RequesterLacksAuthority(Substrate, Did) per §4.6 read-everything-authority, got {other:?}"
),
}
}
#[test]
fn issue_moderation_rejects_did_at_stage_1_per_moderation_as_service() {
let fixture = ContextFixture::new();
let ctx = fixture.build_ctx(Requester::Did(sample_did()));
let Err(err) =
issue_moderation::<ModeratorRead>(&ctx, sample_moderation_subject())
else {
panic!("expected Err, got Ok");
};
match err {
AuthDenial::RequesterLacksAuthority { class, found } => {
assert_eq!(class, CapabilityClass::Moderation);
assert_eq!(found, RequesterKind::Did);
}
other => panic!(
"expected RequesterLacksAuthority(Moderation, Did) per §4.3 moderation-as-service, got {other:?}"
),
}
}
#[test]
fn process_authority_id_is_stable_within_process() {
let a = process_authority_id();
let b = process_authority_id();
assert_eq!(a, b, "process_authority_id must be process-static");
}
#[test]
fn stage_0_active_lexicon_passes() {
let nsid = Nsid::new("tools.kryphocron.feed.postPrivate").unwrap();
let r = check_stage_0_deprecation(&nsid, 0);
assert!(matches!(r, Ok(())));
}
#[test]
fn stage_0_unknown_nsid_passes() {
let nsid = Nsid::new("com.example.unknown").unwrap();
let r = check_stage_0_deprecation(&nsid, 0);
assert!(matches!(r, Ok(())));
}
}