blindplane-access 0.1.0

Signed enterprise access grants, capability policies, revocation and encrypted audit events for Blindplane
Documentation
//! Signed revocation and HPKE role-grant behavior.

use blindplane_access::{
    AccessError, AccessGrant, AccessIssuer, AccessValidationPolicy, GrantSpec, Permissions,
    PrincipalKeypair, PrincipalKind, RevocationSpec, RevocationState, RoleKeypair,
};

fn principal(id: &str, seed: u8) -> PrincipalKeypair {
    PrincipalKeypair::from_secret_bytes(
        "acme",
        id,
        PrincipalKind::User,
        1,
        [seed; 32],
        [seed.wrapping_add(1); 32],
    )
    .unwrap()
}

fn grant_spec() -> GrantSpec {
    GrantSpec {
        grant_id: "grant-alice-audit".into(),
        scope: "audit".into(),
        permissions: Permissions::READ | Permissions::OBSERVE,
        authorization_epoch: 7,
        not_before: 100,
        not_after: 200,
    }
}

fn revocation(
    issuer: &AccessIssuer,
    minimum_authorization_epoch: u64,
    minimum_role_key_epoch: u64,
) -> RevocationState {
    RevocationState::issue(
        issuer,
        "acme",
        "alice",
        "audit",
        RevocationSpec {
            revision: 3,
            minimum_authorization_epoch,
            minimum_role_key_epoch,
            issued_at: 90,
        },
    )
    .unwrap()
}

#[test]
fn current_grant_restores_the_exact_role_key() {
    let alice = principal("alice", 7);
    let issuer = AccessIssuer::from_seed("tenant-admin", [11; 32]).unwrap();
    let role =
        RoleKeypair::from_secret_bytes("acme", "audit", "tenant-audit", 4, [13; 32]).unwrap();
    let grant = AccessGrant::issue(&issuer, alice.principal(), &role, grant_spec()).unwrap();
    let encoded = grant.encode();
    let decoded = AccessGrant::decode(&encoded, &AccessValidationPolicy::default()).unwrap();
    assert_eq!(decoded, grant);

    let state = revocation(&issuer, 7, 4);
    let verified = state
        .verify(&issuer.trusted(), "acme", "alice", "audit")
        .unwrap();
    let opened = decoded
        .open_role(&alice, &issuer.trusted(), 150, &verified)
        .unwrap();

    assert_eq!(opened.role_id(), "tenant-audit");
    assert_eq!(opened.key_epoch(), 4);
    assert_eq!(opened.public_key(), role.public_key());
    assert!(grant.permissions().contains(Permissions::READ));
    assert!(grant.permissions().contains(Permissions::OBSERVE));
    assert!(!grant.permissions().contains(Permissions::ADMINISTER));
}

#[test]
fn wrong_subject_and_revoked_epochs_cannot_open_a_role() {
    let alice = principal("alice", 7);
    let bob = principal("bob", 17);
    let issuer = AccessIssuer::from_seed("tenant-admin", [11; 32]).unwrap();
    let role =
        RoleKeypair::from_secret_bytes("acme", "audit", "tenant-audit", 4, [13; 32]).unwrap();
    let grant = AccessGrant::issue(&issuer, alice.principal(), &role, grant_spec()).unwrap();

    let current_state = revocation(&issuer, 7, 4);
    let current = current_state
        .verify(&issuer.trusted(), "acme", "alice", "audit")
        .unwrap();
    assert_eq!(
        grant.open_role(&bob, &issuer.trusted(), 150, &current),
        Err(AccessError::SubjectMismatch)
    );

    let auth_state = revocation(&issuer, 8, 4);
    let newer_authorization = auth_state
        .verify(&issuer.trusted(), "acme", "alice", "audit")
        .unwrap();
    assert_eq!(
        grant.open_role(&alice, &issuer.trusted(), 150, &newer_authorization),
        Err(AccessError::Revoked)
    );

    let role_state = revocation(&issuer, 7, 5);
    let newer_role = role_state
        .verify(&issuer.trusted(), "acme", "alice", "audit")
        .unwrap();
    assert_eq!(
        grant.open_role(&alice, &issuer.trusted(), 150, &newer_role),
        Err(AccessError::Revoked)
    );
}

#[test]
fn grant_and_revocation_signatures_are_verified_during_decode() {
    let alice = principal("alice", 7);
    let issuer = AccessIssuer::from_seed("tenant-admin", [11; 32]).unwrap();
    let role =
        RoleKeypair::from_secret_bytes("acme", "audit", "tenant-audit", 4, [13; 32]).unwrap();
    let grant = AccessGrant::issue(&issuer, alice.principal(), &role, grant_spec()).unwrap();

    let mut grant_bytes = grant.encode();
    *grant_bytes.last_mut().unwrap() ^= 1;
    assert_eq!(
        AccessGrant::decode(&grant_bytes, &AccessValidationPolicy::default()),
        Err(AccessError::InvalidSignature)
    );

    let mut revocation_bytes = revocation(&issuer, 7, 4).encode();
    *revocation_bytes.last_mut().unwrap() ^= 1;
    assert_eq!(
        RevocationState::decode(&revocation_bytes, &AccessValidationPolicy::default()),
        Err(AccessError::InvalidSignature)
    );
}

#[test]
fn grants_require_permissions_and_a_valid_time_window() {
    let alice = principal("alice", 7);
    let issuer = AccessIssuer::from_seed("tenant-admin", [11; 32]).unwrap();
    let role =
        RoleKeypair::from_secret_bytes("acme", "audit", "tenant-audit", 4, [13; 32]).unwrap();
    let mut invalid = grant_spec();
    invalid.permissions = Permissions::NONE;
    assert_eq!(
        AccessGrant::issue(&issuer, alice.principal(), &role, invalid),
        Err(AccessError::InvalidPermissions)
    );

    let mut invalid = grant_spec();
    invalid.not_before = 201;
    assert_eq!(
        AccessGrant::issue(&issuer, alice.principal(), &role, invalid),
        Err(AccessError::InvalidEpoch)
    );
}