credo 0.8.0

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

use credo::{
    ClaimBody, Credential, CredentialSource, Credo, PermissionKind, SimpleCredentialSource,
    WRITE_TO_SCOPE,
};
use futures::{
    future::{self},
    FutureExt, StreamExt,
};
use litl::json;
use mofo::Mofo;
use caro::Remote;
use tracing::trace;

macro_rules! timeout {
    ($f:expr) => {
        tokio::time::timeout(Duration::from_millis(500), $f).map(|r| r.expect("Timed out"))
    };
}

#[tokio::test]
async fn can_create_child_scopes() {
    // traceful::init_test();

    let background = Mofo::new();
    let credo = Credo::new("credo".to_string(), background.clone());
    background
        .run_until(async {
            let initial_recipient = Credential::new_random();
            let credential_source = Box::new(SimpleCredentialSource::new());

            let scope = credo
                .create_scope(
                    initial_recipient.clone(),
                    vec![PermissionKind::MakeStatement {
                        path_prefix: "test".to_string(),
                    }],
                    None,
                    credential_source.clone_ref(),
                )
                .await;

            let updates = scope.updates("test".to_owned());

            let mut updates = updates.skip_while(|s| future::ready(s.valid_claims.len() < 8));
            assert_eq!(
                timeout!(updates.next()).await.unwrap().valid_claims.len(),
                8
            );

            let child_scope = credo
                .create_scope(
                    initial_recipient.clone(),
                    vec![PermissionKind::InheritFrom],
                    Some(scope.id()),
                    credential_source.clone_ref(),
                )
                .await;

            let child_updates = child_scope.updates("test-child".to_owned());

            let mut child_updates =
                child_updates.skip_while(|s| future::ready(s.valid_claims.len() < 18));

            let child_valid_claims = timeout!(child_updates.next()).await.unwrap().valid_claims;
            assert_eq!(child_valid_claims.len(), 18);

            let child_claim_id = child_scope
                .make_claim_after(
                    ClaimBody::Statement {
                        path: "test".to_string(),
                        value: json!("hello world").into(),
                    },
                    vec![],
                )
                .await
                .unwrap();

            let child_valid_claims = timeout!(child_updates.next()).await.unwrap().valid_claims;
            assert_eq!(child_valid_claims.len(), 19);
            assert!(child_valid_claims.contains_key(&child_claim_id));

            assert_eq!(
                child_valid_claims.get(&child_claim_id).unwrap().0.body,
                ClaimBody::Statement {
                    path: "test".to_string(),
                    value: json!("hello world").into(),
                }
            );
        })
        .await;
}

// TODO: this isn't nearly strict enough, just move the revocation test at the end to the next (proper, two-node) test
#[tokio::test]
async fn child_scopes_get_notified_and_influenced_by_parent_changes() {
    // traceful::init_test();

    let background = Mofo::new();
    let credo = Credo::new("credo".to_string(), background.clone());
    background
        .run_until(async {
            let initial_recipient = Credential::new_random();
            let credential_source = Box::new(SimpleCredentialSource::new());

            let parent_scope = credo
                .create_scope(
                    initial_recipient.clone(),
                    vec![PermissionKind::Delegate {
                        delegated: Box::new(PermissionKind::MakeStatement {
                            path_prefix: "test".to_string(),
                        }),
                    }],
                    None,
                    credential_source.clone_ref(),
                )
                .await;

            let updates = parent_scope.updates("test".to_owned());

            let mut updates = updates.skip_while(|s| future::ready(s.valid_claims.len() < 8));
            assert_eq!(
                timeout!(updates.next()).await.unwrap().valid_claims.len(),
                8
            );

            let second_recipient = Credential::new_random();

            let permission_id = parent_scope
                .make_claim_after(
                    ClaimBody::Permission {
                        permitted: PermissionKind::MakeStatement {
                            path_prefix: "test".to_string(),
                        },
                        to: second_recipient.for_making_claims.pub_id(),
                        as_of: ti64::now(),
                    },
                    vec![],
                )
                .await
                .unwrap();

            parent_scope
                .make_claim(ClaimBody::AddSharedSecretRecipient {
                    secret_kind: WRITE_TO_SCOPE.to_owned(),
                    recipient: second_recipient.for_accepting_secrets.pub_id(),
                })
                .await
                .unwrap();
            parent_scope
                .re_reveal_shared_secret(WRITE_TO_SCOPE)
                .await
                .unwrap();

            // we create the child scope with the initial recipient to make sure that the second recipient
            // inherits permissions from the parent scope
            let child_scope_by_initial = credo
                .create_scope(
                    initial_recipient.clone(),
                    vec![PermissionKind::InheritFrom],
                    Some(parent_scope.id()),
                    credential_source,
                )
                .await;

            let new_credential_source = Box::new(SimpleCredentialSource::new());
            new_credential_source
                .add_credential(parent_scope.id(), second_recipient)
                .await;

            let child_scope = credo.get_scope(&child_scope_by_initial.id(), new_credential_source);

            let child_updates = child_scope.updates("test-child".to_owned());

            let mut child_updates =
                child_updates.skip_while(|s| future::ready(s.valid_claims.len() < 21));

            let child_valid_claims = timeout!(child_updates.next()).await.unwrap().valid_claims;
            assert_eq!(child_valid_claims.len(), 21);

            let child_claim_id = child_scope
                .make_claim_after(
                    ClaimBody::Statement {
                        path: "test".to_string(),
                        value: json!("hello world").into(),
                    },
                    vec![],
                )
                .await
                .unwrap();

            let mut child_updates =
                child_updates.skip_while(|s| future::ready(s.valid_claims.len() < 22));

            let child_valid_claims = timeout!(child_updates.next()).await.unwrap().valid_claims;
            assert_eq!(child_valid_claims.len(), 22);
            assert!(child_valid_claims.contains_key(&child_claim_id));

            assert_eq!(
                child_valid_claims.get(&child_claim_id).unwrap().0.body,
                ClaimBody::Statement {
                    path: "test".to_string(),
                    value: json!("hello world").into(),
                }
            );

            let revocation_id = parent_scope
                .make_claim_after(
                    ClaimBody::Revocation {
                        revoked_claim_id: permission_id,
                        as_of: ti64::now(),
                    },
                    vec![],
                )
                .await
                .unwrap();

            let child_valid_claims = timeout!(child_updates.next()).await.unwrap().valid_claims;

            assert_eq!(child_valid_claims.len(), 21);
            assert!(!child_valid_claims.contains_key(&child_claim_id));
            assert!(child_valid_claims.contains_key(&revocation_id));
        })
        .await;
}

// this is similar to the previous test but intentionally split across two nodes to ensure credentials still behave correctly
#[tokio::test]
async fn child_scopes_can_be_used_from_other_nodes_with_invited_credentials() {
    // traceful::init_test_trace();

    let background = Mofo::new();
    let credo1 = Credo::new("credo1".to_string(), background.clone());
    let credo2 = Credo::new("credo2".to_string(), background.clone());

    background
        .run_until(async {
            let (credo1_as_remote, credo2_as_remote) =
                Remote::new_connected_test_pair("credo1", "credo2");
            credo1.add_remote(credo2_as_remote).await;
            credo2.add_remote(credo1_as_remote).await;

            let initial_recipient = Credential::new_random();
            let credential_source = Box::new(SimpleCredentialSource::new());

            let scope = credo1
                .create_scope(
                    initial_recipient.clone(),
                    vec![PermissionKind::Delegate {
                        delegated: Box::new(PermissionKind::MakeStatement {
                            path_prefix: "test".to_string(),
                        }),
                    }],
                    None,
                    credential_source.clone_ref(),
                )
                .await;

            let updates = scope.updates("test".to_owned());

            let mut updates = updates.skip_while(|s| future::ready(s.valid_claims.len() < 8));
            assert_eq!(
                timeout!(updates.next()).await.unwrap().valid_claims.len(),
                8
            );

            let second_recipient = Credential::new_random();

            let permission_id = scope
                .make_claim(ClaimBody::Permission {
                    permitted: PermissionKind::MakeStatement {
                        path_prefix: "test".to_string(),
                    },
                    to: second_recipient.for_making_claims.pub_id(),
                    as_of: ti64::now(),
                })
                .await
                .unwrap();

            let allow_writing_id = scope
                .make_claim(ClaimBody::AddSharedSecretRecipient {
                    secret_kind: WRITE_TO_SCOPE.to_string(),
                    recipient: second_recipient.for_accepting_secrets.pub_id(),
                })
                .await
                .unwrap();

            scope.re_reveal_shared_secret(WRITE_TO_SCOPE).await.unwrap();

            let expected_write_key_id_from_parent = scope
                .current_shared_secrets_for(WRITE_TO_SCOPE)
                .await
                .unwrap()
                .get(&scope.id())
                .as_ref()
                .unwrap()
                .as_ref()
                .unwrap()
                .id;

            // we create the child scope with the initial recipient to make sure that the second recipient
            // inherits permissions from the parent scope
            let child_scope = credo1
                .create_scope(
                    initial_recipient.clone(),
                    vec![PermissionKind::InheritFrom],
                    Some(scope.id()),
                    credential_source.clone_ref(),
                )
                .await;

            let write_keys_id_in_child = child_scope
                .current_shared_secrets_for(WRITE_TO_SCOPE)
                .await
                .unwrap();

            assert_eq!(
                write_keys_id_in_child
                    .get(&scope.id())
                    .as_ref()
                    .unwrap()
                    .as_ref()
                    .unwrap()
                    .id,
                expected_write_key_id_from_parent
            );

            let credential_source2 = Box::new(SimpleCredentialSource::new());

            credential_source2
                .add_credential(scope.id(), second_recipient.clone())
                .await;

            let child_scope_2 = credo2.get_scope(&child_scope.id(), credential_source2);

            let child_updates = child_scope_2.updates("test-child".to_owned());

            let mut child_updates = child_updates.skip_while(|s| {
                trace!("Got {} valid claims", s.valid_claims.len());
                future::ready(s.valid_claims.len() < 20)
            });

            let child_state = timeout!(child_updates.next()).await.unwrap();
            trace!(child_state = ?child_state, "Got newest child state");
            assert_eq!(child_state.valid_claims.len(), 21);
            assert!(child_state.valid_claims.contains_key(&permission_id));
            assert!(child_state.valid_claims.contains_key(&allow_writing_id));
            assert!(child_state
                .secret_recipients
                .get(WRITE_TO_SCOPE)
                .unwrap()
                .contains(&second_recipient.for_accepting_secrets.pub_id()));

            assert!(child_state.valid_claims.iter().any(
                |(_, (claim, _))| matches!(&claim.body, ClaimBody::EntrustToSharedSecret {
                        secret_kind,
                        for_key_id,
                        ..
                    } if secret_kind == WRITE_TO_SCOPE
                        && for_key_id == &expected_write_key_id_from_parent)
            ));

            assert!(child_state.valid_claims.iter().any(
                |(_, (claim, _))| matches!(&claim.body, ClaimBody::RevealSharedSecret {
                        secret_kind,
                        key_id,
                        encrypted_per_recipient,
                    } if secret_kind == WRITE_TO_SCOPE
                        && key_id == &expected_write_key_id_from_parent
                        && encrypted_per_recipient
                            .contains_key(&second_recipient.for_accepting_secrets.pub_id()))
            ));

            let child_claim_id = child_scope_2
                .make_claim(ClaimBody::Statement {
                    path: "test".to_string(),
                    value: json!("hello world").into(),
                })
                .await
                .unwrap();

            let mut child_updates =
                child_updates.skip_while(|s| future::ready(s.valid_claims.len() < 22));

            let child_valid_claims = timeout!(child_updates.next()).await.unwrap().valid_claims;
            assert_eq!(child_valid_claims.len(), 22);
            assert!(child_valid_claims.contains_key(&child_claim_id));

            assert_eq!(
                child_valid_claims.get(&child_claim_id).unwrap().0.body,
                ClaimBody::Statement {
                    path: "test".to_string(),
                    value: json!("hello world").into(),
                }
            );
        })
        .await;
}

// TODO: #59 add test that checks revocation to secret access, proper current recipient set etc. for child scope