Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar

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");
}