credo 0.8.0

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

use ridl::signing::SignerID;
use serde_derive::{Deserialize, Serialize};
use thiserror::Error;
use ti64::MsSinceEpoch;
use tracing::warn;

use crate::{
    claims_and_permissions_v1::{ClaimBody, CredoV1Claim, PermissionKind},
    scope::{scope_state::ValidClaims, ScopeID},
    ClaimID,
};

use super::scope_state::InvalidClaims;

#[derive(Debug)]
pub struct CombinedClaimSet {
    pub bootstrap_claims: HashSet<ClaimID>,
    pub claims: HashMap<ClaimID, (CredoV1Claim, ScopeID)>,
}

impl CombinedClaimSet {
    pub fn is_claim_valid_as_of(
        &self,
        id: &ClaimID,
        as_of: MsSinceEpoch,
    ) -> Result<(), ClaimInvalidReason> {
        // debug!("Is claim {id:?} valid?");
        //TODO-V1(design): #36 revocation rule: I can revoke claims made myself and those based on permissions delegated *by me*

        let (claim, _) = self.claims.get(id).unwrap();

        if claim.made_at > as_of {
            warn!(claim_id = ?id, "Claim is from the future");
            return Err(ClaimInvalidReason::ClaimFromTheFuture);
        }

        let is_revoked_by = self
            .claims
            .iter()
            .find_map(|(other_id, (other, _other_source))| {
                if other.is_revocation_of(id, as_of)
                    && self.is_claim_valid_as_of(other_id, as_of).is_ok()
                {
                    Some(*other_id)
                } else {
                    None
                }
            });

        if let Some(revoked_by) = is_revoked_by {
            // debug!("-> No: revoked");
            return Err(ClaimInvalidReason::RevokedBy(revoked_by));
        };

        if self.bootstrap_claims.contains(id) {
            // debug!("-> Yes: is a bootstrap claim");
            return Ok(());
        };

        if let ClaimBody::Revocation { .. } = claim.body {
            return self.is_revocation_valid_as_of(claim, as_of);
        }

        let permitted_by_any = self
            .claims
            .iter()
            .any(|(other_id, (other, _other_source))| {
                other_id != id
                    && match other {
                        CredoV1Claim {
                            body:
                                ClaimBody::Permission {
                                    to,
                                    as_of: permission_as_of,
                                    permitted: permission_kind,
                                },
                            ..
                        } => {
                            let to_correct_recipient = to == &claim.by;
                            let after_permission_as_of = permission_as_of <= &as_of;
                            let permits_correct_claim_kind =
                                permission_kind.permits_claim(&claim.body);
                            // info!(
                            //     "{:?} <- {:?} {} {} {}",
                            //     id,
                            //     other_id,
                            //     to_correct_recipient,
                            //     after_permission_as_of,
                            //     permits_correct_claim_kind
                            // );
                            if to_correct_recipient
                                && after_permission_as_of
                                && permits_correct_claim_kind
                            {
                                // debug!("-> Checking if permitting claim {other_id:?} is valid");
                                // TODO(correctness): #34 still might cause infinite loop if there are multiple valid predecessors
                                self.is_claim_valid_as_of(other_id, as_of).is_ok()
                            } else {
                                // debug!("-> {other_id:?} doesn't apply");
                                false
                            }
                        }
                        _ => false,
                    }
            });

        if permitted_by_any {
            Ok(())
        } else {
            // TODO: collect more precise upstream reason(s)
            Err(ClaimInvalidReason::NotPermittedByAnyOtherValidClaim)
        }
    }

    fn is_revocation_valid_as_of(
        &self,
        claim: &CredoV1Claim,
        as_of: MsSinceEpoch,
    ) -> Result<(), ClaimInvalidReason> {
        let (revoked_claim_id, revoked_as_of) = match &claim.body {
            ClaimBody::Revocation {
                revoked_claim_id,
                as_of,
            } => (revoked_claim_id, as_of),
            _ => unreachable!(),
        };

        if *revoked_as_of > as_of {
            return Err(ClaimInvalidReason::RevocationNotYetValid);
        }

        let (revoked_claim, _revoked_source) = self
            .claims
            .get(revoked_claim_id)
            .expect("Expected revoked claim to be present");

        if revoked_claim.by == claim.by {
            // debug!("-> Yes: revocation of claim made by same signer");
            Ok(())
        } else {
            todo!("Implement more complex revocation cases")
        }
    }

    pub fn claims_valid_as_of(&self, as_of: MsSinceEpoch) -> (ValidClaims, InvalidClaims) {
        // TODO(optimization): this is accidentally quadratic
        let mut valid_claims = HashMap::new();
        let mut invalid_claims = HashMap::new();

        for (claim_id, (claim, source)) in self.claims.iter() {
            match self.is_claim_valid_as_of(claim_id, as_of) {
                Ok(()) => {
                    valid_claims.insert(*claim_id, (claim.clone(), *source));
                }
                Err(reason) => {
                    invalid_claims.insert(*claim_id, reason);
                }
            }
        }

        (valid_claims, invalid_claims)
    }

    // TODO: include timestamp?
    pub(crate) fn current_permissions_of(&self, recipient: &SignerID) -> Vec<PermissionKind> {
        self.claims
            .iter()
            .filter_map(|(id, (claim, _source))| match &claim.body {
                ClaimBody::Permission {
                    to,
                    permitted: kind,
                    ..
                } => {
                    if to == recipient && self.is_claim_valid_as_of(id, ti64::now()).is_ok() {
                        Some(kind.clone())
                    } else {
                        None
                    }
                }
                _ => None,
            })
            .collect()
    }
}

#[derive(Clone, Error, Debug, Serialize, Deserialize)]
pub enum ClaimInvalidReason {
    #[error("Claim is from the future")]
    ClaimFromTheFuture,
    #[error("Claim is revoked by {0:?}")]
    RevokedBy(ClaimID),
    #[error("Revocation is not yet valid")]
    RevocationNotYetValid,
    #[error("Claim is not permitted by any other claim")]
    NotPermittedByAnyOtherValidClaim,
}