use std::sync::Arc;
use gunnar_grpc::ControlPlane as _;
use gunnar_server::control_plane::CoreControlPlane;
use gunnar_server::stores::ServerStores;
fn passkey(seed: u8) -> Vec<u8> {
let key = p256::ecdsa::SigningKey::from_slice(&[seed; 32]).expect("a scalar");
key.verifying_key().to_sec1_point(false).as_bytes().to_vec()
}
fn session_key() -> gunnar_core::ssh_key::PrivateKey {
gunnar_core::ssh_key::PrivateKey::from_openssh(gunnar_core::mint_ed25519().expect("mint"))
.expect("parses")
}
fn person(stores: &ServerStores, id: &str, kind: gunnar_core::PrincipalKind) {
stores
.auth()
.add_principal(gunnar_core::Principal {
id: id.to_owned(),
name: id.to_owned(),
kind,
created: 0,
})
.expect("principal");
}
#[test]
fn a_passkey_survives_a_restart_and_goes_with_its_principal() {
let dir = tempfile::tempdir().expect("tempdir");
{
let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
person(&stores, "kim", gunnar_core::PrincipalKind::User);
let plane = CoreControlPlane::new(Arc::clone(&stores));
let added = plane
.add_credential("kim", b"kim-yubikey", &passkey(1), "YubiKey", 7)
.expect("kim's passkey");
assert_eq!(added.credential_id, b"kim-yubikey".to_vec());
assert_eq!(added.sign_count, 7);
}
let stores = Arc::new(ServerStores::open(dir.path()).expect("stores, reopened"));
let plane = CoreControlPlane::new(Arc::clone(&stores));
let listed = plane.credentials(Some("kim")).expect("listed");
assert_eq!(listed.len(), 1, "{listed:?}");
assert_eq!(listed[0].label, "YubiKey");
let stored = plane
.credential(b"kim-yubikey")
.expect("read")
.expect("the passkey is still there");
assert_eq!(stored.public_key, passkey(1), "the key came back byte for byte");
assert_eq!(stored.credential.principal_id, "kim");
plane.remove_principal("kim").expect("removed");
assert!(plane.credentials(None).expect("listed").is_empty(), "the passkey outlived kim");
assert!(plane.credential(b"kim-yubikey").expect("read").is_none());
}
#[test]
fn a_machine_holds_no_passkey_and_an_id_is_registered_once() {
let dir = tempfile::tempdir().expect("tempdir");
let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
person(&stores, "ci", gunnar_core::PrincipalKind::Machine);
person(&stores, "kim", gunnar_core::PrincipalKind::User);
let plane = CoreControlPlane::new(Arc::clone(&stores));
let refused = plane
.add_credential("ci", b"ci-key", &passkey(2), "ci", 0)
.expect_err("a machine holds no passkey");
assert!(
matches!(refused, gunnar_grpc::ControlError::FailedPrecondition(_)),
"{refused:?}"
);
plane
.add_credential("kim", b"kim-key", &passkey(3), "laptop", 0)
.expect("kim's");
let twice = plane
.add_credential("kim", b"kim-key", &passkey(4), "again", 0)
.expect_err("an id once");
assert!(matches!(twice, gunnar_grpc::ControlError::AlreadyExists(_)), "{twice:?}");
let ghost = plane
.add_credential("ghost", b"ghost-key", &passkey(5), "ghost", 0)
.expect_err("no such principal");
assert!(matches!(ghost, gunnar_grpc::ControlError::NotFound(_)), "{ghost:?}");
}
#[test]
fn a_web_sessions_key_expires_and_the_counter_is_kept() {
use gunnar_grpc::Identity as _;
let dir = tempfile::tempdir().expect("tempdir");
let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
person(&stores, "kim", gunnar_core::PrincipalKind::User);
let plane = CoreControlPlane::new(Arc::clone(&stores));
plane
.add_credential("kim", b"kim-yubikey", &passkey(6), "YubiKey", 0)
.expect("kim's passkey");
let session = session_key();
let now = gunnar_core::now_ms();
let outcome = plane
.web_login(gunnar_grpc::WebLoginCommit {
credential_id: b"kim-yubikey".to_vec(),
sign_count: 4,
now_unix_ms: now,
session_openssh: session.public_key().to_openssh().expect("openssh"),
comment: "web session".into(),
expires_at_unix_ms: now + 1_500,
})
.expect("the login commits");
assert_eq!(outcome.principal.id, "kim");
let held = plane.registered_key(&outcome.fingerprint).expect("asked");
assert_eq!(held.map(|k| k.principal_id).as_deref(), Some("kim"), "the session key is kim's");
std::thread::sleep(std::time::Duration::from_millis(1_800));
assert!(
plane.registered_key(&outcome.fingerprint).expect("asked").is_none(),
"an expired session key still authenticates"
);
let stored = plane.credential(b"kim-yubikey").expect("read").expect("there");
assert_eq!(stored.credential.sign_count, 4);
assert!(stored.credential.last_used_unix_ms.is_some());
let rollback = plane
.web_login(gunnar_grpc::WebLoginCommit {
credential_id: b"kim-yubikey".to_vec(),
sign_count: 4,
now_unix_ms: gunnar_core::now_ms(),
session_openssh: session_key().public_key().to_openssh().expect("openssh"),
comment: "web session".into(),
expires_at_unix_ms: gunnar_core::now_ms() + 60_000,
})
.expect_err("a counter that did not move is refused in the update itself");
assert!(matches!(rollback, gunnar_grpc::ControlError::PermissionDenied(_)), "{rollback:?}");
}
#[test]
fn an_invite_is_redeemed_once_with_its_grant_in_one_update() {
use gunnar_grpc::Identity as _;
let dir = tempfile::tempdir().expect("tempdir");
let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
person(&stores, "kim", gunnar_core::PrincipalKind::User);
let plane = CoreControlPlane::new(Arc::clone(&stores));
let hash = gunnar_grpc::webauthn::sha256_hex(b"the-secret");
plane
.create_invite(gunnar_grpc::NewInvite {
id: "inv-1".into(),
principal_id: "kim".into(),
created_by: "boss".into(),
created_at_unix_ms: 1,
expires_at_unix_ms: gunnar_core::now_ms() + 60_000,
note: String::new(),
server_level: gunnar_proto::GrantLevel::Write,
secret_hash: hash.clone(),
})
.expect("kim's invite");
let registration = |id: &[u8]| gunnar_grpc::WebRegistration {
invite_secret_hash: Some(hash.clone()),
principal_id: None,
credential_id: id.to_vec(),
public_key: passkey(7),
label: "phone".into(),
sign_count: 0,
now_unix_ms: gunnar_core::now_ms(),
};
let registered = plane.web_register(registration(b"kim-phone")).expect("redeemed");
assert_eq!(registered.principal.id, "kim");
assert_eq!(registered.server_level, gunnar_proto::GrantLevel::Write);
assert!(plane
.may("kim", &gunnar_grpc::Access::Server, gunnar_proto::GrantLevel::Write)
.expect("asked"));
assert!(plane.invites().expect("listed").is_empty(), "the invite was not consumed");
let again = plane.web_register(registration(b"kim-tablet")).expect_err("once");
assert!(matches!(again, gunnar_grpc::ControlError::PermissionDenied(_)), "{again:?}");
assert_eq!(plane.credentials(Some("kim")).expect("listed").len(), 1);
}
#[test]
fn revoking_a_passkey_revokes_its_session_keys() {
use gunnar_grpc::Identity as _;
let dir = tempfile::tempdir().expect("tempdir");
let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
person(&stores, "kim", gunnar_core::PrincipalKind::User);
let plane = CoreControlPlane::new(Arc::clone(&stores));
for (id, seed) in [(&b"kim-yubikey"[..], 8), (&b"kim-phone"[..], 9)] {
plane.add_credential("kim", id, &passkey(seed), "k", 0).expect("passkey");
}
let login = |id: &[u8]| {
plane
.web_login(gunnar_grpc::WebLoginCommit {
credential_id: id.to_vec(),
sign_count: 0,
now_unix_ms: gunnar_core::now_ms(),
session_openssh: session_key().public_key().to_openssh().expect("openssh"),
comment: "web session".into(),
expires_at_unix_ms: gunnar_core::now_ms() + 60_000,
})
.expect("logged in")
.fingerprint
};
let (one, two, other) = (login(b"kim-yubikey"), login(b"kim-yubikey"), login(b"kim-phone"));
plane.revoke_credential(b"kim-yubikey").expect("revoked").expect("it was there");
assert!(plane.registered_key(&one).expect("asked").is_none());
assert!(plane.registered_key(&two).expect("asked").is_none());
assert!(plane.registered_key(&other).expect("asked").is_some(), "the phone's session went too");
}
fn approval(machine: &str, id: &[u8], seed: u8) -> gunnar_grpc::MachineApproval {
gunnar_grpc::MachineApproval {
machine: machine.to_owned(),
server_level: gunnar_proto::GrantLevel::Admin,
credential_id: id.to_vec(),
public_key: passkey(seed),
label: format!("installer kit on {}", machine.trim_start_matches("kit-")),
sign_count: 0,
now_unix_ms: gunnar_core::now_ms(),
step_up: None,
}
}
#[test]
fn an_approved_machine_enrolment_makes_a_machine_with_its_credential_and_survives_a_restart() {
use gunnar_grpc::Identity as _;
let dir = tempfile::tempdir().expect("tempdir");
{
let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
person(&stores, "kim", gunnar_core::PrincipalKind::User);
let plane = CoreControlPlane::new(Arc::clone(&stores));
plane.add_credential("kim", b"kim-yubikey", &passkey(1), "YubiKey", 5).expect("kim's passkey");
let mut backwards = approval("kit-dwarf", b"kit-dwarf-key", 3);
backwards.step_up = Some((b"kim-yubikey".to_vec(), 4));
let refused = plane.approve_machine_enrolment(backwards).expect_err("a rolled-back counter");
assert!(matches!(refused, gunnar_grpc::ControlError::PermissionDenied(_)), "{refused:?}");
assert!(plane.principal("kit-dwarf").expect("read").is_none());
let mut fresh = approval("kit-dwarf", b"kit-dwarf-key", 3);
fresh.step_up = Some((b"kim-yubikey".to_vec(), 6));
let approved = plane.approve_machine_enrolment(fresh).expect("approved");
assert!(approved.created);
assert_eq!(approved.principal.kind, gunnar_proto::PrincipalKind::Machine as i32);
assert_eq!(approved.credential.principal_id, "kit-dwarf");
assert_eq!(plane.credential(b"kim-yubikey").expect("read").expect("there").credential.sign_count, 6, "the step-up's counter is kept");
let again = plane.approve_machine_enrolment(approval("kit-dwarf", b"kit-dwarf-key", 3)).expect_err("an id once");
assert!(matches!(again, gunnar_grpc::ControlError::AlreadyExists(_)), "{again:?}");
}
let stores = Arc::new(ServerStores::open(dir.path()).expect("stores, reopened"));
let plane = CoreControlPlane::new(Arc::clone(&stores));
let machine = plane.principal("kit-dwarf").expect("read").expect("the machine is still there");
assert_eq!(machine.kind, gunnar_proto::PrincipalKind::Machine as i32);
assert!(plane.may("kit-dwarf", &gunnar_grpc::Access::Server, gunnar_proto::GrantLevel::Admin).expect("asked"));
assert_eq!(plane.credentials(Some("kit-dwarf")).expect("listed").len(), 1);
assert!(plane.credentials(Some("kim")).expect("listed").iter().all(|c| c.credential_id != b"kit-dwarf-key".to_vec()));
let session = plane
.web_login(gunnar_grpc::WebLoginCommit {
credential_id: b"kit-dwarf-key".to_vec(),
sign_count: 0,
now_unix_ms: gunnar_core::now_ms(),
session_openssh: session_key().public_key().to_openssh().expect("openssh"),
comment: "web session".into(),
expires_at_unix_ms: gunnar_core::now_ms() + 60_000,
})
.expect("the machine logs in");
assert_eq!(session.principal.id, "kit-dwarf");
person(&stores, "kit-person", gunnar_core::PrincipalKind::User);
let refused = plane.approve_machine_enrolment(approval("kit-person", b"other-key", 4)).expect_err("a person under the id");
assert!(matches!(refused, gunnar_grpc::ControlError::FailedPrecondition(_)), "{refused:?}");
assert!(plane.credential(b"other-key").expect("read").is_none());
}
#[test]
fn an_approved_machine_leaves_the_owners_first_invite_pending() {
let dir = tempfile::tempdir().expect("tempdir");
let stores = Arc::new(ServerStores::open(dir.path()).expect("stores"));
let plane = CoreControlPlane::new(Arc::clone(&stores));
plane.claim_operator_named(Some("rickard"), "").expect("the operator");
let hash = gunnar_grpc::webauthn::sha256_hex(b"break-glass");
assert_eq!(plane.operator_bootstrap_invite("rickard", &hash, gunnar_core::now_ms(), gunnar_grpc::seam::OPERATOR_INVITE_TTL_MS).expect("recorded"), gunnar_grpc::BootstrapInvite::Recorded);
plane.approve_machine_enrolment(approval("kit-fresh", b"kit-fresh-key", 5)).expect("approved");
assert!(plane.invites().expect("listed").iter().any(|i| i.invite_id == gunnar_grpc::seam::OPERATOR_INVITE_ID), "the owner's first invite is still pending");
assert_eq!(plane.operator_bootstrap_invite("rickard", &hash, gunnar_core::now_ms(), gunnar_grpc::seam::OPERATOR_INVITE_TTL_MS).expect("asked"), gunnar_grpc::BootstrapInvite::Renewed);
assert!(plane.credentials(Some("rickard")).expect("listed").is_empty(), "nothing on the operator");
}