dmsg_protocol 0.1.1

Deterministic encoding, COSE document verification, and approval helpers for dMsg
Documentation
use super::*;
use crate::*;
use dmsg_types::cose::*;
use ed25519_dalek::{Signer, SigningKey};
#[path = "../../dmsg_types/tests/support/app_action.rs"]
mod fixture;

fn statement(action: AppAction) -> Statement {
    Statement {
        issuer: "https://example.test/u/00000000000000000000".into(),
        subject: None,
        issued_at: None,
        content: StatementContent::AppAction(Box::new(action)),
    }
}

#[test]
fn closed_actions_roundtrip_and_every_signed_field_is_bound() {
    let signer = SigningKey::from_bytes(&[71; 32]);
    let public = signer.verifying_key().to_bytes();
    let kid = key_thumbprint(&public_cose_key(&Algorithm::Ed25519, &[], &public).unwrap()).unwrap();
    for command in fixture::commands() {
        let mut action = fixture::action();
        action.command = command;
        action.input_hash = dmsg_protocol::app_action::action_input_hash(&action.command);
        validate_app_action(&action).unwrap();
        let original = statement(action.clone());
        let (_, tbs) = prepare_cose(&original, &Algorithm::Ed25519, &kid[..]).unwrap();
        let sig = signer.sign(&tbs).to_bytes().to_vec();
        let artifact = finish_cose(&tbs, &public, sig.clone()).unwrap();
        assert_eq!(verify_artifact(&artifact).unwrap(), original);
        assert_eq!(statement_purpose(&original), KeyPurpose::AppAction);
        assert_eq!(
            verification_report(&artifact, None).unwrap().content,
            VerificationStatus::NotChecked
        );
        for mutate in [
            |a: &mut AppAction| a.origin = "https://other.test".into(),
            |a: &mut AppAction| a.receiver = candid::Principal::from_slice(&[99, 1]),
            |a: &mut AppAction| a.files[0].display_name = Some("changed.cbor".into()),
            |a: &mut AppAction| a.files[0].sha256 = Hash::new([42; 32]),
            |a: &mut AppAction| a.files[0].revision += 1,
            |a: &mut AppAction| a.files[0].representation = ActionFileRepresentation::Encrypted,
            |a: &mut AppAction| a.expires_at_ms -= 1,
            |a: &mut AppAction| a.actor_id = AccountId([42; 12]),
            |a: &mut AppAction| {
                a.command = AppActionCommand::TokenListApproveTransition {
                    project_id: 42,
                    transition_id: 1,
                    approve: true,
                    statement_hash: Hash::new([1; 32]),
                    rationale: "A different complete decision".into(),
                }
            },
        ] {
            let mut changed = action.clone();
            mutate(&mut changed);
            changed.input_hash = action_input_hash(&changed.command);
            assert_ne!(app_action_digest(&changed), app_action_digest(&action));
            let (_, changed_tbs) =
                prepare_cose(&statement(changed), &Algorithm::Ed25519, &kid[..]).unwrap();
            let changed_artifact = finish_cose(&changed_tbs, &public, sig.clone()).unwrap();
            assert!(verify_artifact(&changed_artifact).is_err());
        }
    }
}

#[test]
fn unknown_fields_profiles_and_ambiguous_claims_are_rejected() {
    let a = fixture::action();
    let mut value: cbor2::Value = cbor2::from_slice(&canonical(&a)).unwrap();
    if let cbor2::Value::Map(ref mut entries) = value {
        entries.push(("display_summary".into(), "approve everything".into()));
    }
    assert!(decode_canonical::<AppAction>(&canonical(&value)).is_err());
    let mut s = statement(a.clone());
    s.subject = Some("a contradictory subject".into());
    assert!(prepare_cose(&s, &Algorithm::Ed25519, &[1]).is_err());
    s.subject = None;
    s.issued_at = Some(1);
    assert!(prepare_cose(&s, &Algorithm::Ed25519, &[1]).is_err());
    let mut wrong = a.clone();
    wrong.version = 2;
    assert!(validate_app_action(&wrong).is_err());
    wrong = a.clone();
    wrong.files.push(wrong.files[0].clone());
    assert!(validate_app_action(&wrong).is_err());
    wrong = a;
    wrong.expires_at_ms += 1;
    assert!(validate_app_action(&wrong).is_err());
}

#[test]
fn admission_and_document_endpoint_fail_closed() {
    let action = fixture::action();
    let mut app = AppRegistration {
        version: 1,
        environment: action.environment.clone(),
        app_id: action.app_id.clone(),
        config_version: 1,
        origins: vec![action.origin.clone()],
        user_homes: vec![candid::Principal::from_slice(&[1, 1])],
        cose_homes: vec![candid::Principal::from_slice(&[2, 1])],
        product_ids: vec![],
        capabilities: vec![AppCapability::Authenticate],
        profiles: vec![],
        authentication_receiver: candid::Principal::from_slice(&[3, 1]),
        action_authority: candid::Principal::from_slice(&[3, 1]),
        paused: false,
    };
    app.capabilities.push(AppCapability::SignAction);
    app.profiles.push(SigningProfile::AppActionV1);
    validate_action_admission(&action, &app, action.issued_at_ms).unwrap();
    assert_eq!(
        validate_action_admission(&action, &app, action.expires_at_ms),
        Err(Error::Expired)
    );
    app.paused = true;
    assert_eq!(
        validate_action_admission(&action, &app, action.issued_at_ms),
        Err(Error::Locked)
    );
    let request = SignRequest {
        account_id: AccountId([3; 12]),
        statement: statement(action),
        origin: "https://example.test".into(),
        key: SigningKeyRef {
            algorithm: SigningAlgorithm::Ed25519,
            kid: vec![1].into(),
            public_key_fingerprint: Hash::new([1; 32]),
        },
        max_cycles: 1,
        approval: Approval {
            device_id: Hash::new([1; 32]),
            security_epoch: 1,
            sequence: 1,
            request_id: Hash::new([2; 32]),
            expires_at: 1,
            signature: Default::default(),
        },
    };
    assert_eq!(request.into_execution(), Err(Error::UnsupportedProtocol));
}