crates/gunnar-grpc/tests/web_credentials.rs
//! **A person's passkeys live at gunnar, on their principal** (owner,
//! 2026-10-04: "passkeys + invites + passkey↔principal pin → gunnar-server";
//! the console keeps only cookie sessions).
//!
//! Until this file a person's WebAuthn credentials, and which principal each
//! one logged in as, lived only in the console's own state file: a front
//! reinstall lost every login, the CLI could see none of them, and removing a
//! principal at gunnar left its passkeys working at the console. Here they are
//! rows in gunnar's identity table beside the person's SSH keys:
//!
//! * a server admin, or the principal itself (the self-scope), adds, lists and
//! revokes them;
//! * only a USER may hold one — no human credential on a machine, a group or
//! a deploy key;
//! * the public key must be a P-256 point (SEC1 uncompressed), and a
//! credential id is registered once;
//! * a refusal to revoke somebody else's credential is word for word the one
//! for a credential that does not exist;
//! * removing the principal removes its credentials.
use std::sync::Arc;
use gunnar_grpc::auth::AuthGate;
use gunnar_grpc::{
sign_credential_for, AccessService, AuthMode, Caller, ControlPlane, EventBus,
MemoryControlPlane, ServerConfig, ServiceContext, CREDENTIAL_METADATA_KEY,
};
use gunnar_proto::access_server::Access as _;
use gunnar_proto::*;
use tonic::{Code, Request};
fn mint() -> ssh_key::PrivateKey {
ssh_key::PrivateKey::random(
&mut rand::rand_core::UnwrapErr(rand::rngs::SysRng),
ssh_key::Algorithm::Ed25519,
)
.expect("mint an ed25519 key")
}
fn now_ms() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("after 1970")
.as_millis() as i64
}
/// A passkey's public half: a real P-256 point, SEC1 uncompressed.
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()
}
struct Harness {
plane: Arc<MemoryControlPlane>,
ctx: ServiceContext,
}
impl Harness {
fn new() -> Harness {
let plane = Arc::new(MemoryControlPlane::new());
let control: Arc<dyn ControlPlane> = plane.clone();
let ctx = ServiceContext::new(
control,
Arc::new(EventBus::new(16)),
ServerConfig::default(),
);
assert_eq!(ctx.config.auth, AuthMode::Required, "the shipped default");
Harness { plane, ctx }
}
fn principal(&self, id: &str, kind: PrincipalKind) -> Caller {
let key = mint();
self.plane.add_principal(id, id, kind).expect("principal");
self.plane
.add_key(
id,
&key.public_key().to_openssh().expect("openssh"),
id,
None,
"",
None,
)
.expect("key");
let credential = sign_credential_for(
&key,
now_ms(),
&format!("nonce-{id}-web-credentials"),
"Admin.Health",
)
.expect("sign");
let mut request = Request::new(());
request.metadata_mut().insert(
CREDENTIAL_METADATA_KEY,
credential.to_header_value().parse().expect("ascii"),
);
AuthGate::new(Arc::clone(&self.ctx.control), self.ctx.config.auth)
.authenticate_bound(request.metadata(), now_ms(), Some("Admin.Health"))
.expect("a registered key verifies")
}
fn person(&self, id: &str) -> Caller {
self.principal(id, PrincipalKind::User)
}
fn admin(&self) -> Caller {
let boss = self.person("boss");
self.plane.grant_server("boss", GrantLevel::Admin);
boss
}
fn authed<T>(caller: &Caller, body: T) -> Request<T> {
let mut request = Request::new(body);
request.extensions_mut().insert(caller.clone());
request
}
fn access(&self) -> AccessService {
AccessService::new(self.ctx.clone())
}
async fn add(
&self,
caller: &Caller,
principal: &str,
id: &[u8],
public_key: Vec<u8>,
) -> Result<WebCredential, tonic::Status> {
self.access()
.add_credential(Harness::authed(
caller,
AddCredentialRequest {
principal_id: principal.into(),
credential_id: id.to_vec(),
public_key,
label: format!("{principal}'s key"),
sign_count: 0,
},
))
.await
.map(|r| r.into_inner().credential.expect("a credential"))
}
async fn list(&self, caller: &Caller, principal: &str) -> Result<Vec<Vec<u8>>, tonic::Status> {
self.access()
.list_credentials(Harness::authed(
caller,
ListCredentialsRequest {
principal_id: principal.into(),
},
))
.await
.map(|r| {
let mut ids: Vec<Vec<u8>> = r
.into_inner()
.credentials
.into_iter()
.map(|c| c.credential_id)
.collect();
ids.sort();
ids
})
}
async fn revoke(&self, caller: &Caller, id: &[u8]) -> Result<(), tonic::Status> {
self.access()
.revoke_credential(Harness::authed(
caller,
RevokeCredentialRequest {
credential_id: id.to_vec(),
},
))
.await
.map(|_| ())
}
}
/// ★ **A server admin adds a passkey to a person; the person sees their own
/// and nobody else's; the admin sees them all.**
#[tokio::test]
async fn an_admin_adds_a_passkey_and_a_person_lists_their_own() {
let h = Harness::new();
let boss = h.admin();
let kim = h.person("kim");
let ola = h.person("ola");
let added = h
.add(&boss, "kim", b"kim-yubikey", passkey(1))
.await
.expect("admin adds kim's");
assert_eq!(added.principal_id, "kim");
assert_eq!(added.credential_id, b"kim-yubikey".to_vec());
assert_eq!(added.label, "kim's key");
assert!(added.last_used_unix_ms.is_none(), "never used yet");
h.add(&boss, "ola", b"ola-phone", passkey(2))
.await
.expect("admin adds ola's");
assert_eq!(
h.list(&kim, "kim").await.expect("kim's own"),
vec![b"kim-yubikey".to_vec()]
);
for (asked, what) in [("ola", "another person's"), ("", "every credential")] {
let refused = h.list(&kim, asked).await.expect_err(what);
assert_eq!(
refused.code(),
Code::PermissionDenied,
"{what}: {refused:?}"
);
}
assert_eq!(h.list(&boss, "").await.expect("all").len(), 2);
let _ = ola;
}
/// ★ **The self-scope: a person adds a passkey to themselves**, and to nobody
/// else.
#[tokio::test]
async fn a_person_adds_a_passkey_to_themselves_and_to_nobody_else() {
let h = Harness::new();
let kim = h.person("kim");
h.person("ola");
h.add(&kim, "kim", b"kim-laptop", passkey(3))
.await
.expect("kim adds her own");
let refused = h
.add(&kim, "ola", b"planted", passkey(4))
.await
.expect_err("a passkey on another person is a server admin's act");
assert_eq!(refused.code(), Code::PermissionDenied, "{refused:?}");
assert!(h.list(&h.admin(), "ola").await.expect("list").is_empty());
}
/// **No human credential on a machine principal** — nor a group, nor a deploy
/// key; a key that is not a P-256 point and a credential id registered twice
/// are refused by name.
#[tokio::test]
async fn a_machine_holds_no_passkey_and_a_bad_key_or_a_second_id_is_refused() {
let h = Harness::new();
let boss = h.admin();
h.principal("ci", PrincipalKind::Machine);
let refused = h
.add(&boss, "ci", b"ci-key", passkey(5))
.await
.expect_err("no human credential on a machine principal");
assert_eq!(refused.code(), Code::FailedPrecondition, "{refused:?}");
assert!(refused.message().contains("machine"), "{refused:?}");
h.person("kim");
let mut off_curve = passkey(6);
off_curve[64] ^= 0x01;
for (bad, what) in [
(off_curve, "a point that is not on the curve"),
(passkey(6)[..33].to_vec(), "a truncated key"),
(vec![0u8; 65], "zeroes"),
] {
let refused = h.add(&boss, "kim", b"kim-bad", bad).await.expect_err(what);
assert_eq!(refused.code(), Code::InvalidArgument, "{what}: {refused:?}");
}
h.add(&boss, "kim", b"kim-key", passkey(7))
.await
.expect("a good one");
let twice = h
.add(&boss, "kim", b"kim-key", passkey(8))
.await
.expect_err("a credential id is registered once");
assert_eq!(twice.code(), Code::AlreadyExists, "{twice:?}");
let nobody = h
.add(&boss, "ghost", b"ghost-key", passkey(9))
.await
.expect_err("no such principal");
assert_eq!(nobody.code(), Code::NotFound, "{nobody:?}");
}
/// ★ **A person revokes their own passkey; somebody else's is refused in the
/// words a missing one is; an admin revokes anyone's.**
#[tokio::test]
async fn a_person_revokes_their_own_passkey_and_the_refusal_names_nobody() {
let h = Harness::new();
let boss = h.admin();
let kim = h.person("kim");
h.person("ola");
h.add(&boss, "kim", b"kim-a", passkey(10))
.await
.expect("kim-a");
h.add(&boss, "ola", b"ola-a", passkey(11))
.await
.expect("ola-a");
h.revoke(&kim, b"kim-a").await.expect("kim revokes her own");
assert!(h.list(&kim, "kim").await.expect("list").is_empty());
let theirs = h
.revoke(&kim, b"ola-a")
.await
.expect_err("ola's is not kim's");
let nobodys = h
.revoke(&kim, b"no-such")
.await
.expect_err("nor is a missing one");
assert_eq!(theirs.code(), Code::PermissionDenied, "{theirs:?}");
assert_eq!(
theirs.message(),
nobodys.message(),
"the refusal is no oracle"
);
assert!(!theirs.message().contains("ola"), "{theirs:?}");
assert_eq!(h.list(&boss, "ola").await.expect("list").len(), 1);
h.revoke(&boss, b"ola-a")
.await
.expect("an admin revokes anyone's");
let missing = h.revoke(&boss, b"ola-a").await.expect_err("gone");
assert_eq!(missing.code(), Code::NotFound, "{missing:?}");
}
/// Removing the principal removes its passkeys.
#[tokio::test]
async fn removing_a_principal_removes_its_passkeys() {
let h = Harness::new();
let boss = h.admin();
h.person("kim");
h.add(&boss, "kim", b"kim-a", passkey(12))
.await
.expect("kim-a");
h.add(&boss, "kim", b"kim-b", passkey(13))
.await
.expect("kim-b");
h.access()
.remove_principal(Harness::authed(
&boss,
RemovePrincipalRequest {
principal_id: "kim".into(),
},
))
.await
.expect("removed");
assert!(
h.list(&boss, "").await.expect("list").is_empty(),
"kim's passkeys outlived her"
);
}
/// ★ **A person renames their own passkey; somebody else's is refused in the
/// words a missing one is; an admin renames anyone's; a label is not empty.**
#[tokio::test]
async fn a_person_renames_their_own_passkey() {
let h = Harness::new();
let boss = h.admin();
let kim = h.person("kim");
h.person("ola");
h.add(&boss, "kim", b"kim-a", passkey(20))
.await
.expect("kim-a");
h.add(&boss, "ola", b"ola-a", passkey(21))
.await
.expect("ola-a");
let rename = |caller: &Caller, id: &[u8], label: &str| {
let access = h.access();
let request = Harness::authed(
caller,
RenameCredentialRequest {
credential_id: id.to_vec(),
label: label.into(),
},
);
async move {
access
.rename_credential(request)
.await
.map(|r| r.into_inner())
}
};
let renamed = rename(&kim, b"kim-a", "work laptop")
.await
.expect("kim renames hers");
assert_eq!(renamed.credential.expect("row").label, "work laptop");
let theirs = rename(&kim, b"ola-a", "mine now")
.await
.expect_err("not kim's");
let nobodys = rename(&kim, b"no-such", "x").await.expect_err("missing");
assert_eq!(theirs.code(), Code::PermissionDenied, "{theirs:?}");
assert_eq!(theirs.message(), nobodys.message());
let empty = rename(&kim, b"kim-a", " ")
.await
.expect_err("an empty label");
assert_eq!(empty.code(), Code::InvalidArgument, "{empty:?}");
rename(&boss, b"ola-a", "ola's phone")
.await
.expect("an admin renames anyone's");
let missing = rename(&boss, b"no-such", "x").await.expect_err("missing");
assert_eq!(missing.code(), Code::NotFound, "{missing:?}");
}
/// ★ **The box keeps one admin who can log in**: the LAST passkey held by any
/// server-Admin person is refused, by anybody — while another admin's
/// passkey, or a second of the same admin's, lets it go; a person who is not
/// an admin may remove their only one.
#[tokio::test]
async fn the_last_admin_passkey_is_not_revoked() {
let h = Harness::new();
let boss = h.admin();
let kim = h.person("kim");
h.add(&boss, "boss", b"boss-a", passkey(30))
.await
.expect("boss's only passkey");
for (who, what) in [(&boss, "boss himself"), (&boss, "again")] {
let refused = h.revoke(who, b"boss-a").await.expect_err(what);
assert_eq!(
refused.code(),
Code::FailedPrecondition,
"{what}: {refused:?}"
);
assert!(refused.message().contains("last"), "{refused:?}");
}
h.add(&boss, "boss", b"boss-b", passkey(31))
.await
.expect("a second");
h.revoke(&boss, b"boss-a").await.expect("one of two goes");
let refused = h
.revoke(&boss, b"boss-b")
.await
.expect_err("the last one again");
assert_eq!(refused.code(), Code::FailedPrecondition, "{refused:?}");
let eva = h.person("eva");
h.plane.grant_server("eva", GrantLevel::Admin);
h.add(&eva, "eva", b"eva-a", passkey(32))
.await
.expect("eva's");
h.revoke(&boss, b"boss-b")
.await
.expect("another admin can still log in");
h.add(&kim, "kim", b"kim-only", passkey(33))
.await
.expect("kim's only");
h.revoke(&kim, b"kim-only")
.await
.expect("kim is not an admin");
}