credo 0.8.0

A framework for trust-free distributed cryptographic claims and secret management
Documentation
use std::collections::HashMap;

use litl::impl_debug_as_litl;
use ridl::{
    asymm_encr::{EncrFromAnon, RecipientID},
    signing::SignerID,
    symm_encr::{Encrypted, KeyID, KeySecret},
};
use serde_derive::{Deserialize, Serialize};
use ti64::MsSinceEpoch;

use crate::{ClaimID, ScopeID};

#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
pub struct CredoV1Claim {
    pub by: SignerID,
    // TODO: get rid of this completely and use only time witnesses instead
    pub made_at: MsSinceEpoch,
    #[serde(flatten)]
    pub body: ClaimBody,
}

impl CredoV1Claim {
    pub fn is_revocation_of(&self, revoked_id: &ClaimID, as_of: MsSinceEpoch) -> bool {
        match &self.body {
            ClaimBody::Revocation {
                revoked_claim_id,
                as_of: revocation_as_of,
            } => revoked_claim_id == revoked_id && *revocation_as_of <= as_of,
            _ => false,
        }
    }
}

pub const WRITE_TO_SCOPE: &str = "writeToScope";

#[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "type")]
#[serde(rename_all = "camelCase")]
pub enum ClaimBody {
    Statement {
        path: String,
        value: litl::Val,
    },
    AddSharedSecretRecipient {
        secret_kind: String,
        recipient: RecipientID,
    },
    Permission {
        to: SignerID,
        permitted: PermissionKind,
        as_of: MsSinceEpoch,
    },
    Revocation {
        revoked_claim_id: ClaimID,
        as_of: MsSinceEpoch,
    },
    RevealSharedSecret {
        secret_kind: String,
        key_id: KeyID,
        // TODO: this is a bit redundant, EncrFromAnon already has recipient in it
        encrypted_per_recipient: HashMap<RecipientID, EncrFromAnon<KeySecret>>,
    },
    // TODO: the wording around this is very confusing, fix before #21
    EntrustToSharedSecret {
        secret_kind: String,
        entrusted_secret_name: String,
        for_key_id: KeyID,
        encrypted: Encrypted<litl::Val>,
    },
    TimeWitness {},
    InheritFrom {
        parent: ScopeID,
    },
}

impl_debug_as_litl!(ClaimBody);

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "type")]
#[serde(rename_all = "camelCase")]
pub enum PermissionKind {
    MakeStatement { path_prefix: String },
    AddSharedSecretRecipient { secret_kind: String },
    RevealSharedSecret { secret_kind: String },
    EntrustToSharedSecret { secret_kind: String },
    TimeWitness,
    // TODO: limit to specific parent scope?
    InheritFrom,
    Delegate { delegated: Box<PermissionKind> },
    DelegateInfinitely { delegated: Box<PermissionKind> },
}

impl PermissionKind {
    pub fn permits_claim(&self, example: &ClaimBody) -> bool {
        match (self, example) {
            (PermissionKind::MakeStatement { path_prefix }, ClaimBody::Statement { path, .. }) => {
                path.starts_with(path_prefix)
            }
            (
                PermissionKind::AddSharedSecretRecipient {
                    secret_kind: permission_secret_kind,
                },
                ClaimBody::AddSharedSecretRecipient { secret_kind, .. },
            ) => permission_secret_kind == secret_kind,
            (
                PermissionKind::RevealSharedSecret {
                    secret_kind: permission_secret_kind,
                },
                ClaimBody::RevealSharedSecret { secret_kind, .. },
            ) => permission_secret_kind == secret_kind,
            (
                PermissionKind::EntrustToSharedSecret {
                    secret_kind: permission_secret_kind,
                },
                ClaimBody::EntrustToSharedSecret { secret_kind, .. },
            ) => permission_secret_kind == secret_kind,
            (PermissionKind::TimeWitness, ClaimBody::TimeWitness {}) => true,
            (PermissionKind::InheritFrom, ClaimBody::InheritFrom { .. }) => true,
            (
                PermissionKind::Delegate {
                    delegated: delegated_permission,
                }
                | PermissionKind::DelegateInfinitely {
                    delegated: delegated_permission,
                },
                ClaimBody::Permission {
                    permitted: permitted_permission,
                    ..
                },
            ) => match permitted_permission {
                PermissionKind::Delegate {delegated: other_delegated_permission} => {
                    other_delegated_permission.is_stricter_than_or_equal_to(delegated_permission)
                }
                simple_permitted_permission => simple_permitted_permission.is_stricter_than_or_equal_to(delegated_permission),
            }
            _ => false,
        }
    }

    pub fn is_stricter_than_or_equal_to(&self, other: &PermissionKind) -> bool {
        match (self, other) {
            (
                PermissionKind::MakeStatement { path_prefix },
                PermissionKind::MakeStatement {
                    path_prefix: other_path_prefix,
                },
            ) => path_prefix.starts_with(other_path_prefix),
            (
                PermissionKind::AddSharedSecretRecipient { secret_kind },
                PermissionKind::AddSharedSecretRecipient {
                    secret_kind: secret_kind_other,
                },
            ) => secret_kind == secret_kind_other,
            (
                PermissionKind::RevealSharedSecret { secret_kind },
                PermissionKind::RevealSharedSecret {
                    secret_kind: secret_kind_other,
                },
            ) => secret_kind == secret_kind_other,
            (
                PermissionKind::EntrustToSharedSecret { secret_kind },
                PermissionKind::EntrustToSharedSecret {
                    secret_kind: secret_kind_other,
                },
            ) => secret_kind == secret_kind_other,
            (PermissionKind::TimeWitness, PermissionKind::TimeWitness) => true,
            (PermissionKind::InheritFrom, PermissionKind::InheritFrom) => true,
            _ => false,
        }
    }
}

#[cfg(test)]
mod test {
    use std::collections::HashMap;

    use ridl::{signing::SignerSecret, asymm_encr::RecipientSecret, symm_encr::KeySecret};
    use caro::ObjectID;

    use crate::ClaimID;

    use super::{ClaimBody, PermissionKind};

    #[test]
    fn permissions_permit_same_but_dont_permit_different_claim_type() {
        let statement_permission = PermissionKind::MakeStatement {
            path_prefix: "foo".to_string(),
        };

        let add_shared_secret_recp_permission = PermissionKind::AddSharedSecretRecipient {
            secret_kind: "foo".to_string(),
        };

        let reveal_shared_secret_permission = PermissionKind::RevealSharedSecret {
            secret_kind: "foo".to_string(),
        };

        let entrust_to_shared_secret_permission = PermissionKind::EntrustToSharedSecret {
            secret_kind: "foo".to_string(),
        };

        let time_witness_permission = PermissionKind::TimeWitness;

        let inherit_from_permission = PermissionKind::InheritFrom;

        let statement_claim = ClaimBody::Statement {
            path: "foo".to_string(),
            value: litl::Val::bool(true),
        };

        let add_shared_secret_recp_claim = ClaimBody::AddSharedSecretRecipient {
            secret_kind: "foo".to_string(),
            recipient: ridl::asymm_encr::RecipientSecret::new_random().pub_id(),
        };

        let permission_claim = ClaimBody::Permission {
            to: SignerSecret::new_random().pub_id(),
            permitted: PermissionKind::TimeWitness,
            as_of: ti64::now(),
        };

        let revocation_claim = ClaimBody::Revocation {
            revoked_claim_id: ClaimID::test_random(),
            as_of: ti64::now(),
        };

        let reveal_shared_secret_claim = ClaimBody::RevealSharedSecret {
            secret_kind: "foo".to_string(),
            key_id: ridl::symm_encr::KeySecret::new_random().id,
            encrypted_per_recipient: HashMap::new(),
        };

        let key_secret = ridl::symm_encr::KeySecret::new_random();

        let entrust_to_shared_secret_claim = ClaimBody::EntrustToSharedSecret {
            secret_kind: "foo".to_string(),
            entrusted_secret_name: "foo".to_string(),
            for_key_id: key_secret.id,
            encrypted: key_secret.encrypt(
                &litl::Val::bool(true),
            ),
        };

        let time_witness_claim = ClaimBody::TimeWitness {};

        let inherit_from_claim = ClaimBody::InheritFrom {
            parent: crate::ScopeID(ObjectID::test_random()),
        };

        // should permit

        assert!(statement_permission.permits_claim(&statement_claim));
        assert!(add_shared_secret_recp_permission.permits_claim(&add_shared_secret_recp_claim));
        assert!(reveal_shared_secret_permission.permits_claim(&reveal_shared_secret_claim));
        assert!(entrust_to_shared_secret_permission.permits_claim(&entrust_to_shared_secret_claim));
        assert!(time_witness_permission.permits_claim(&time_witness_claim));
        assert!(inherit_from_permission.permits_claim(&inherit_from_claim));

        // should not permit

        assert!(!statement_permission.permits_claim(&add_shared_secret_recp_claim));
        assert!(!statement_permission.permits_claim(&permission_claim));
        assert!(!statement_permission.permits_claim(&revocation_claim));
        assert!(!statement_permission.permits_claim(&reveal_shared_secret_claim));
        assert!(!statement_permission.permits_claim(&entrust_to_shared_secret_claim));
        assert!(!statement_permission.permits_claim(&time_witness_claim));
        assert!(!statement_permission.permits_claim(&inherit_from_claim));

        assert!(!add_shared_secret_recp_permission.permits_claim(&statement_claim));
        assert!(!add_shared_secret_recp_permission.permits_claim(&permission_claim));
        assert!(!add_shared_secret_recp_permission.permits_claim(&revocation_claim));
        assert!(!add_shared_secret_recp_permission.permits_claim(&reveal_shared_secret_claim));
        assert!(!add_shared_secret_recp_permission.permits_claim(&entrust_to_shared_secret_claim));
        assert!(!add_shared_secret_recp_permission.permits_claim(&time_witness_claim));
        assert!(!add_shared_secret_recp_permission.permits_claim(&inherit_from_claim));

        assert!(!reveal_shared_secret_permission.permits_claim(&statement_claim));
        assert!(!reveal_shared_secret_permission.permits_claim(&add_shared_secret_recp_claim));
        assert!(!reveal_shared_secret_permission.permits_claim(&permission_claim));
        assert!(!reveal_shared_secret_permission.permits_claim(&revocation_claim));
        assert!(!reveal_shared_secret_permission.permits_claim(&entrust_to_shared_secret_claim));
        assert!(!reveal_shared_secret_permission.permits_claim(&time_witness_claim));
        assert!(!reveal_shared_secret_permission.permits_claim(&inherit_from_claim));

        assert!(!entrust_to_shared_secret_permission.permits_claim(&statement_claim));
        assert!(!entrust_to_shared_secret_permission.permits_claim(&add_shared_secret_recp_claim));
        assert!(!entrust_to_shared_secret_permission.permits_claim(&permission_claim));
        assert!(!entrust_to_shared_secret_permission.permits_claim(&revocation_claim));
        assert!(!entrust_to_shared_secret_permission.permits_claim(&reveal_shared_secret_claim));
        assert!(!entrust_to_shared_secret_permission.permits_claim(&time_witness_claim));
        assert!(!entrust_to_shared_secret_permission.permits_claim(&inherit_from_claim));

        assert!(!time_witness_permission.permits_claim(&statement_claim));
        assert!(!time_witness_permission.permits_claim(&add_shared_secret_recp_claim));
        assert!(!time_witness_permission.permits_claim(&permission_claim));
        assert!(!time_witness_permission.permits_claim(&revocation_claim));
        assert!(!time_witness_permission.permits_claim(&reveal_shared_secret_claim));
        assert!(!time_witness_permission.permits_claim(&entrust_to_shared_secret_claim));
        assert!(!time_witness_permission.permits_claim(&inherit_from_claim));

        assert!(!inherit_from_permission.permits_claim(&statement_claim));
        assert!(!inherit_from_permission.permits_claim(&add_shared_secret_recp_claim));
        assert!(!inherit_from_permission.permits_claim(&permission_claim));
        assert!(!inherit_from_permission.permits_claim(&revocation_claim));
        assert!(!inherit_from_permission.permits_claim(&reveal_shared_secret_claim));
        assert!(!inherit_from_permission.permits_claim(&entrust_to_shared_secret_claim));
        assert!(!inherit_from_permission.permits_claim(&time_witness_claim));
    }

    #[test]
    fn statement_permissions_should_only_permit_correct_prefix() {
        let statement_permission = PermissionKind::MakeStatement {
            path_prefix: "foo".to_string(),
        };

        let statement_claim = ClaimBody::Statement {
            path: "foo".to_string(),
            value: litl::Val::bool(true),
        };

        let statement_claim_wrong_prefix = ClaimBody::Statement {
            path: "bar".to_string(),
            value: litl::Val::bool(true),
        };

        assert!(statement_permission.permits_claim(&statement_claim));
        assert!(!statement_permission.permits_claim(&statement_claim_wrong_prefix));
    }

    #[test]
    fn adding_shared_secret_recp_only_permits_same_secret_kind() {
        let add_shared_secret_recp_permission = PermissionKind::AddSharedSecretRecipient {
            secret_kind: "foo".to_string(),
        };

        let add_shared_secret_recp_claim = ClaimBody::AddSharedSecretRecipient {
            secret_kind: "foo".to_string(),
            recipient: RecipientSecret::new_random().pub_id()
        };

        let add_shared_secret_recp_claim_wrong_kind = ClaimBody::AddSharedSecretRecipient {
            secret_kind: "bar".to_string(),
            recipient: RecipientSecret::new_random().pub_id()
        };

        assert!(add_shared_secret_recp_permission.permits_claim(&add_shared_secret_recp_claim));
        assert!(!add_shared_secret_recp_permission.permits_claim(&add_shared_secret_recp_claim_wrong_kind));
    }

    #[test]
    fn entrust_to_shared_secret_only_permits_same_secret_kind() {
        let entrust_to_shared_secret_permission = PermissionKind::EntrustToSharedSecret {
            secret_kind: "foo".to_string(),
        };

        let key_secret = KeySecret::new_random();

        let entrust_to_shared_secret_claim = ClaimBody::EntrustToSharedSecret {
            secret_kind: "foo".to_string(),
            entrusted_secret_name: "bar".to_string(),
            encrypted: key_secret.encrypt(&litl::Val::bool(true)),
            for_key_id: key_secret.id,
        };

        let entrust_to_shared_secret_claim_wrong_kind = ClaimBody::EntrustToSharedSecret {
            secret_kind: "bar".to_string(),
            entrusted_secret_name: "bar".to_string(),
            encrypted: key_secret.encrypt(&litl::Val::bool(true)),
            for_key_id: key_secret.id,
        };

        assert!(entrust_to_shared_secret_permission.permits_claim(&entrust_to_shared_secret_claim));
        assert!(!entrust_to_shared_secret_permission.permits_claim(&entrust_to_shared_secret_claim_wrong_kind));
    }

    #[test]
    fn simple_delegate_permits_permissions() {
        let delegation_permission = PermissionKind::Delegate { delegated: Box::new(PermissionKind::MakeStatement { path_prefix: "foo".to_string() }) };

        let permission_claim = ClaimBody::Permission{
            permitted: PermissionKind::MakeStatement { path_prefix: "foo".to_string() },
            to: SignerSecret::new_random().pub_id(),
            as_of: ti64::now(),
        };

        assert!(delegation_permission.permits_claim(&permission_claim));

        // TODO: add more permission cases?
        // TODO: add tests for correct strictness
    }

    #[test]
    fn delegate_infinitely_permits_permission() {
        let infinite_delegation_permission = PermissionKind::DelegateInfinitely { delegated: Box::new(PermissionKind::MakeStatement { path_prefix: "foo".to_string() }) };

        let permission_claim = ClaimBody::Permission{
            permitted: PermissionKind::MakeStatement { path_prefix: "foo".to_string() },
            to: SignerSecret::new_random().pub_id(),
            as_of: ti64::now(),
        };

        assert!(infinite_delegation_permission.permits_claim(&permission_claim));
        // TODO: add tests for correct strictness
    }

    #[test]
    fn delegate_infinitely_permits_delegation() {
        let infinite_delegation_permission = PermissionKind::DelegateInfinitely { delegated: Box::new(PermissionKind::MakeStatement { path_prefix: "foo".to_string() }) };

        let delegation_permission = PermissionKind::Delegate { delegated: Box::new(PermissionKind::MakeStatement { path_prefix: "foo".to_string() }) };

        let permission_claim = ClaimBody::Permission{
            permitted: delegation_permission,
            to: SignerSecret::new_random().pub_id(),
            as_of: ti64::now(),
        };

        assert!(infinite_delegation_permission.permits_claim(&permission_claim));

        // TODO: add tests for correct strictness
    }

}