cmn-substrate 0.3.0

CMN protocol core — Ed25519 signatures, BLAKE3 tree hashing, JSON schema validation, URI parsing, and JCS canonicalization. Zero I/O, WASM-compatible.
Documentation
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CapsuleHostingKind {
    SelfHosted,
    Replicate,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum KeyTrustRefreshPolicy {
    Expired,
    Always,
    Offline,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum KeyTrustWitnessPolicy {
    Allow,
    RequireDomain,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DomainKeyConfirmation {
    Confirmed,
    Rejected,
    Unreachable,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum KeyTrustClass {
    FirstClass,
    SecondClass,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum KeyTrustWarning {
    SynapseSource,
    SynapseWitness,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum KeyTrustFailure {
    OfflineCacheRequired,
    DomainRejected,
    DomainUnreachableWitnessDisabled,
    DomainUnreachableWitnessRejected,
    DomainUnreachableNoSynapse,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyTrustDecision {
    Trusted {
        trust_class: KeyTrustClass,
        cache_key: bool,
        warning: Option<KeyTrustWarning>,
    },
    Untrusted {
        reason: KeyTrustFailure,
    },
}

pub fn classify_capsule_hosting(uri_domain: &str, core_domain: &str) -> CapsuleHostingKind {
    if uri_domain == core_domain {
        CapsuleHostingKind::SelfHosted
    } else {
        CapsuleHostingKind::Replicate
    }
}

pub fn evaluate_signed_capsule_validity(
    core_signature_valid: bool,
    capsule_signature_valid: bool,
) -> bool {
    core_signature_valid && capsule_signature_valid
}

pub fn needs_key_trust_refresh(
    key_trusted_in_cache: bool,
    refresh_policy: KeyTrustRefreshPolicy,
) -> std::result::Result<bool, KeyTrustFailure> {
    match refresh_policy {
        KeyTrustRefreshPolicy::Expired => Ok(!key_trusted_in_cache),
        KeyTrustRefreshPolicy::Always => Ok(true),
        KeyTrustRefreshPolicy::Offline if key_trusted_in_cache => Ok(false),
        KeyTrustRefreshPolicy::Offline => Err(KeyTrustFailure::OfflineCacheRequired),
    }
}

pub fn decide_key_trust(
    domain_confirmation: DomainKeyConfirmation,
    witness_policy: KeyTrustWitnessPolicy,
    from_synapse: bool,
    synapse_confirms_key: Option<bool>,
) -> KeyTrustDecision {
    match domain_confirmation {
        DomainKeyConfirmation::Confirmed => KeyTrustDecision::Trusted {
            trust_class: KeyTrustClass::FirstClass,
            cache_key: true,
            warning: None,
        },
        DomainKeyConfirmation::Rejected => KeyTrustDecision::Untrusted {
            reason: KeyTrustFailure::DomainRejected,
        },
        DomainKeyConfirmation::Unreachable => {
            if matches!(witness_policy, KeyTrustWitnessPolicy::RequireDomain) {
                return KeyTrustDecision::Untrusted {
                    reason: KeyTrustFailure::DomainUnreachableWitnessDisabled,
                };
            }

            if from_synapse {
                return KeyTrustDecision::Trusted {
                    trust_class: KeyTrustClass::SecondClass,
                    cache_key: false,
                    warning: Some(KeyTrustWarning::SynapseSource),
                };
            }

            match synapse_confirms_key {
                Some(true) => KeyTrustDecision::Trusted {
                    trust_class: KeyTrustClass::SecondClass,
                    cache_key: false,
                    warning: Some(KeyTrustWarning::SynapseWitness),
                },
                Some(false) => KeyTrustDecision::Untrusted {
                    reason: KeyTrustFailure::DomainUnreachableWitnessRejected,
                },
                None => KeyTrustDecision::Untrusted {
                    reason: KeyTrustFailure::DomainUnreachableNoSynapse,
                },
            }
        }
    }
}

#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {

    use super::*;

    #[test]
    fn test_classify_capsule_hosting() {
        assert_eq!(
            classify_capsule_hosting("cmn.dev", "cmn.dev"),
            CapsuleHostingKind::SelfHosted
        );
        assert_eq!(
            classify_capsule_hosting("mirror.dev", "cmn.dev"),
            CapsuleHostingKind::Replicate
        );
    }

    #[test]
    fn test_needs_key_trust_refresh() {
        assert!(!needs_key_trust_refresh(true, KeyTrustRefreshPolicy::Expired).unwrap());
        assert!(needs_key_trust_refresh(false, KeyTrustRefreshPolicy::Always).unwrap());
        assert_eq!(
            needs_key_trust_refresh(false, KeyTrustRefreshPolicy::Offline),
            Err(KeyTrustFailure::OfflineCacheRequired)
        );
    }

    #[test]
    fn test_decide_key_trust() {
        assert_eq!(
            decide_key_trust(
                DomainKeyConfirmation::Confirmed,
                KeyTrustWitnessPolicy::Allow,
                false,
                None
            ),
            KeyTrustDecision::Trusted {
                trust_class: KeyTrustClass::FirstClass,
                cache_key: true,
                warning: None,
            }
        );
        assert_eq!(
            decide_key_trust(
                DomainKeyConfirmation::Unreachable,
                KeyTrustWitnessPolicy::Allow,
                false,
                Some(true)
            ),
            KeyTrustDecision::Trusted {
                trust_class: KeyTrustClass::SecondClass,
                cache_key: false,
                warning: Some(KeyTrustWarning::SynapseWitness),
            }
        );
        assert_eq!(
            decide_key_trust(
                DomainKeyConfirmation::Rejected,
                KeyTrustWitnessPolicy::Allow,
                false,
                None
            ),
            KeyTrustDecision::Untrusted {
                reason: KeyTrustFailure::DomainRejected,
            }
        );
    }
}