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

nordisk/gunnar

crates/gunnar-cli/tests/add_key_attested.rs

#![cfg(all(feature = "server", feature = "control"))]

//! **`Access.AddKey` the target principal attests — on the REAL plane**
//! (2026-09-12).
//!
//! `gunnar-core`'s `key_attestation` unit tests prove the arithmetic and
//! `gunnar-grpc`'s services test proves the shape on the double. This drives
//! the shipped `CoreControlPlane` with a real identity table and real keys:
//! the attestation the CLI would print is what the server verifies, and every
//! refusal is asserted on the key table — a refusal that wrote the key anyway
//! looks identical to the caller.
//!
//! Red first, in this order: an admin's bare word on a keyed principal
//! (FAILED_PRECONDITION); an attestation by the admin's own key
//! (INVALID_ARGUMENT); an attestation the target made for a *different* key
//! (INVALID_ARGUMENT); a forged one (INVALID_ARGUMENT). Then green: the
//! target's honest attestation, and the target adding to themselves with no
//! attestation at all.

mod common;

use std::sync::Arc;

use common::Harness;
use gunnar_cli::server::CoreControlPlane;
use gunnar_core::key_attestation::{sign_attestation, KeyAttestation};
use gunnar_core::{
    mint_ed25519, ssh_key, Fingerprint, Grant, HashKind, Level, Principal, PrincipalKind, Scope,
};
use gunnar_grpc::{AuthMode, Caller, ControlPlane, EventBus, ServerConfig, ServiceContext};
use gunnar_proto::access_server::Access as _;
use gunnar_proto::AddKeyRequest;
use tonic::{Code, Request};

fn key() -> ssh_key::PrivateKey {
    ssh_key::PrivateKey::from_openssh(mint_ed25519().expect("mint")).expect("parse")
}

fn openssh(key: &ssh_key::PrivateKey) -> String {
    key.public_key().to_openssh().expect("openssh")
}

fn fingerprint(key: &ssh_key::PrivateKey) -> String {
    key.public_key()
        .fingerprint(ssh_key::HashAlg::Sha256)
        .to_string()
}

fn now<F: std::future::Future>(future: F) -> F::Output {
    tokio::runtime::Builder::new_current_thread()
        .enable_all()
        .build()
        .expect("runtime")
        .block_on(future)
}

fn as_caller<T>(message: T, principal: &str) -> Request<T> {
    let mut request = Request::new(message);
    request.extensions_mut().insert(Caller {
        principal_id: principal.to_owned(),
        fingerprint: String::new(),
        machine: false,
        authenticated: true,
    });
    request
}

fn access(plane: &Arc<CoreControlPlane>) -> gunnar_grpc::AccessService {
    let control: Arc<dyn ControlPlane> = plane.clone();
    gunnar_grpc::AccessService::new(ServiceContext::new(
        control,
        Arc::new(EventBus::new(16)),
        ServerConfig {
            auth: AuthMode::Required,
            ..ServerConfig::default()
        },
    ))
}

/// The wire form of an attestation, re-encoded by hand rather than through the
/// server's converter, so what the RPC is handed is what a client sends.
fn wire(signed: &gunnar_core::SignedKeyAttestation) -> gunnar_proto::SignedKeyAttestation {
    gunnar_proto::SignedKeyAttestation {
        attestation: Some(gunnar_proto::KeyAttestation {
            principal_id: signed.attestation.principal_id.clone(),
            fingerprint: signed.attestation.fingerprint.clone(),
            at_unix_ms: signed.attestation.at_unix_ms,
        }),
        key_fingerprint: signed.key_fingerprint.clone(),
        signature: signed.signature.clone(),
    }
}

fn attest(
    by: &ssh_key::PrivateKey,
    principal: &str,
    new: &str,
) -> gunnar_proto::SignedKeyAttestation {
    wire(
        &sign_attestation(
            by,
            KeyAttestation {
                principal_id: principal.into(),
                fingerprint: new.into(),
                at_unix_ms: 1_700_000_000_000,
            },
        )
        .expect("sign"),
    )
}

fn add(
    principal: &str,
    key: &ssh_key::PrivateKey,
    attestation: Option<gunnar_proto::SignedKeyAttestation>,
) -> AddKeyRequest {
    AddKeyRequest {
        principal_id: principal.into(),
        openssh_public_key: openssh(key),
        comment: String::new(),
        expires_at_unix_ms: None,
        attestation,
    }
}

#[test]
fn an_admin_cannot_mint_access_as_another_person_without_their_word() {
    let harness = Harness::start("team", "proj", HashKind::Sha1);
    let auth = harness.stores.auth();
    let roots = key();
    let alices = key();
    for id in ["root", "alice"] {
        auth.add_principal(Principal::new(id, id, PrincipalKind::User).unwrap())
            .expect("principal");
    }
    auth.register_key("root", &openssh(&roots), None)
        .expect("root's key");
    auth.grant(Grant {
        principal: "root".into(),
        scope: Scope::Server,
        level: Level::Admin,
    })
    .expect("root is admin");
    let plane = Arc::new(CoreControlPlane::new(Arc::clone(&harness.stores)));
    let access = access(&plane);
    let keys_of = |id: &str| auth.keys_of(id).expect("keys").len();

    // alice's FIRST key: nobody to ask, the admin's word stands.
    now(access.add_key(as_caller(add("alice", &alices, None), "root")))
        .expect("a first key needs no attestation");
    assert_eq!(keys_of("alice"), 1);

    // RED 1: the admin's bare word on a keyed principal.
    let second = key();
    let status = now(access.add_key(as_caller(add("alice", &second, None), "root")))
        .expect_err("a second key on the admin's word alone must be refused");
    assert_eq!(status.code(), Code::FailedPrecondition, "{status}");
    assert!(
        status.message().contains("attest-key"),
        "the refusal must say how to satisfy it: {}",
        status.message()
    );
    assert_eq!(keys_of("alice"), 1, "the refusal must have written nothing");

    // RED 2: attested by the ADMIN's key. Only the target can attest.
    let by_root = attest(&roots, "alice", &fingerprint(&second));
    let status = now(access.add_key(as_caller(add("alice", &second, Some(by_root)), "root")))
        .expect_err("the admin attesting for alice is not alice attesting");
    assert_eq!(status.code(), Code::InvalidArgument, "{status}");
    assert_eq!(keys_of("alice"), 1);

    // RED 3: alice attested a DIFFERENT key than the one being added.
    let other = key();
    let for_other = attest(&alices, "alice", &fingerprint(&other));
    let status = now(access.add_key(as_caller(add("alice", &second, Some(for_other)), "root")))
        .expect_err("an attestation for one key may not be spent on another");
    assert_eq!(status.code(), Code::InvalidArgument, "{status}");
    assert_eq!(keys_of("alice"), 1);

    // RED 4: forged — alice's fingerprint on root's signature.
    let mut forged = attest(&roots, "alice", &fingerprint(&second));
    forged.key_fingerprint = fingerprint(&alices);
    let status = now(access.add_key(as_caller(add("alice", &second, Some(forged)), "root")))
        .expect_err("a signature that does not verify must be refused");
    assert_eq!(status.code(), Code::InvalidArgument, "{status}");
    assert_eq!(keys_of("alice"), 1);

    // GREEN: alice's honest attestation, made with the key she holds.
    let honest = attest(&alices, "alice", &fingerprint(&second));
    let added = now(access.add_key(as_caller(add("alice", &second, Some(honest)), "root")))
        .expect("the target's own attestation is accepted")
        .into_inner()
        .key
        .unwrap();
    assert_eq!(added.principal_id, "alice");
    // Applied output: the key authenticates as alice on the table the SSH
    // listener reads.
    let fp = Fingerprint::parse(&fingerprint(&second)).unwrap();
    assert_eq!(
        auth.principal_by_fingerprint(&fp)
            .expect("read")
            .map(|p| p.id),
        Some("alice".to_owned())
    );
    assert_eq!(keys_of("alice"), 2);

    // GREEN: the target adding to themselves needs no attestation — the call
    // is already their signed word. (alice needs Admin to reach AddKey at all;
    // that rule is unchanged.)
    auth.grant(Grant {
        principal: "alice".into(),
        scope: Scope::Server,
        level: Level::Admin,
    })
    .expect("alice is admin now");
    let third = key();
    now(access.add_key(as_caller(add("alice", &third, None), "alice")))
        .expect("a principal adding to themselves is the attestation");
    assert_eq!(keys_of("alice"), 3);
}