use std::net::SocketAddr;
use std::sync::Arc;
use std::time::{Duration, Instant};
use gunnar_core::ssh_key::PrivateKey;
use gunnar_core::{Principal, PrincipalKind};
use gunnar_crypt::members::Level;
use gunnar_crypt::userkey::{security_key, UserKey};
use gunnar_grpc::client_signer::{authed, plain_channel};
use gunnar_grpc::{EventBus, ServerConfig, ServiceContext};
use gunnar_proto::access_client::AccessClient;
use gunnar_proto::{
AddUnlockMethodRequest, CreateEncryptionKeyRequest, GetEncryptionKeyRequest,
RemoveUnlockMethodRequest, UnlockMethod,
};
use gunnar_server::stores::ServerStores;
use gunnar_server::CoreControlPlane;
use tonic::Code;
fn free_addr() -> SocketAddr {
let l = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
l.local_addr().expect("addr")
}
fn mint() -> (PrivateKey, String) {
let pem = gunnar_core::mint_ed25519().expect("mint");
let k = PrivateKey::from_openssh(std::str::from_utf8(&pem).unwrap()).expect("parse");
let line = k.public_key().to_openssh().expect("line");
(k, line)
}
fn person(stores: &ServerStores, id: &str) -> PrivateKey {
stores
.auth()
.add_principal(Principal::new(id, id, PrincipalKind::User).expect("legal id"))
.expect("principal");
let (key, line) = mint();
stores
.auth()
.register_key(id, &line, None)
.expect("the person's SSH key");
key
}
fn ssh_method(u: &UserKey, line: &str) -> UnlockMethod {
UnlockMethod {
kind: "ssh".into(),
label: "laptop".into(),
method_ref: line.trim().to_owned(),
wrap: u.wrap_to_ssh(line).expect("an ssh wrap"),
..Default::default()
}
}
fn yubikey_method(u: &UserKey, cred: &[u8], cred_b64: &str) -> UnlockMethod {
UnlockMethod {
kind: "security-key".into(),
label: "YubiKey 5C, keyring".into(),
method_ref: cred_b64.into(),
rp_id: "console.mock.invalid".into(),
wrap: u
.wrap_to_security_key(&security_key::recipient(&[7; 32], cred))
.expect("a security-key wrap"),
..Default::default()
}
}
#[test]
fn a_person_makes_reads_and_trims_their_own_encryption_key_over_the_real_router() {
let d = tempfile::tempdir().unwrap();
let stores = Arc::new(ServerStores::open(d.path()).expect("stores"));
let (_admin, admin_line) = mint();
stores
.auth()
.bootstrap_admin(&admin_line)
.expect("the first admin");
let alice = person(&stores, "alice");
let alice_line = alice.public_key().to_openssh().unwrap();
let bob = person(&stores, "bob");
let bob_line = bob.public_key().to_openssh().unwrap();
let rt = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build()
.expect("rt");
let addr = free_addr();
let bus = Arc::new(EventBus::new(16));
let plane: Arc<dyn gunnar_grpc::ControlPlane> =
Arc::new(CoreControlPlane::new(Arc::clone(&stores)));
let ctx = ServiceContext::new(plane, bus, ServerConfig::default());
let (tx, rx) = tokio::sync::oneshot::channel::<()>();
rt.spawn(async move {
let _ = gunnar_grpc::router(ctx, || 0)
.serve_with_shutdown(addr, async {
let _ = rx.await;
})
.await;
});
let t0 = Instant::now();
while std::net::TcpStream::connect(addr).is_err() {
assert!(t0.elapsed() < Duration::from_secs(10), "the box listens");
std::thread::sleep(Duration::from_millis(25));
}
let at = addr.to_string();
let alice_key = UserKey::generate(Level::Classic).expect("alice's key pair");
let bob_key = UserKey::generate(Level::Classic).expect("bob's key pair");
rt.block_on(async {
let mut access = AccessClient::new(plain_channel(&at).expect("a channel"));
let none = access
.get_encryption_key(authed(&alice, GetEncryptionKeyRequest {}))
.await
.expect("Get")
.into_inner();
assert!(
!none.exists && none.public_key.is_empty() && none.methods.is_empty(),
"{none:?}"
);
let anon = access
.get_encryption_key(tonic::Request::new(GetEncryptionKeyRequest {}))
.await
.unwrap_err();
assert_eq!(anon.code(), Code::Unauthenticated, "{anon:?}");
let made = access
.create_encryption_key(authed(
&alice,
CreateEncryptionKeyRequest {
public_key: alice_key.public(),
first: Some(ssh_method(&alice_key, &alice_line)),
},
))
.await
.expect("Access.CreateEncryptionKey")
.into_inner();
assert_eq!(made.public_key, alice_key.public());
assert_eq!(made.methods.len(), 1, "{made:?}");
assert_eq!(
(made.methods[0].kind.as_str(), made.methods[0].id.len()),
("ssh", 16)
);
let again = access
.create_encryption_key(authed(
&alice,
CreateEncryptionKeyRequest {
public_key: alice_key.public(),
first: Some(ssh_method(&alice_key, &alice_line)),
},
))
.await
.unwrap_err();
assert_eq!(again.code(), Code::AlreadyExists, "{again:?}");
let added = access
.add_unlock_method(authed(
&alice,
AddUnlockMethodRequest {
method: Some(yubikey_method(&alice_key, b"yubi-one", "eXViaS1vbmU")),
},
))
.await
.expect("Access.AddUnlockMethod")
.into_inner();
assert_eq!(added.methods.len(), 2, "{added:?}");
let held = access
.add_unlock_method(authed(
&alice,
AddUnlockMethodRequest {
method: Some(yubikey_method(&alice_key, b"yubi-one", "eXViaS1vbmU")),
},
))
.await
.unwrap_err();
assert_eq!(
held.code(),
Code::AlreadyExists,
"the same security key twice: {held:?}"
);
let read = access
.get_encryption_key(authed(&alice, GetEncryptionKeyRequest {}))
.await
.expect("Get")
.into_inner();
assert!(read.exists);
assert_eq!(read.public_key, alice_key.public());
let kinds: Vec<&str> = read.methods.iter().map(|m| m.kind.as_str()).collect();
assert_eq!(kinds, ["ssh", "security-key"], "{read:?}");
let yubi = read
.methods
.iter()
.find(|m| m.kind == "security-key")
.unwrap()
.clone();
assert_eq!(
(yubi.rp_id.as_str(), yubi.label.as_str()),
("console.mock.invalid", "YubiKey 5C, keyring")
);
assert!(yubi.added_ms > 0, "gunnar stamps when it took the method");
let ssh_id = read
.methods
.iter()
.find(|m| m.kind == "ssh")
.unwrap()
.id
.clone();
let bobs = access
.get_encryption_key(authed(&bob, GetEncryptionKeyRequest {}))
.await
.expect("bob's Get")
.into_inner();
assert!(
!bobs.exists && bobs.methods.is_empty(),
"bob never sees alice's key: {bobs:?}"
);
let steal = access
.remove_unlock_method(authed(
&bob,
RemoveUnlockMethodRequest {
id: yubi.id.clone(),
},
))
.await
.unwrap_err();
assert_ne!(steal.code(), Code::Ok);
let plant = access
.add_unlock_method(authed(
&bob,
AddUnlockMethodRequest {
method: Some(yubikey_method(&alice_key, b"bobs-key", "Ym9icy1rZXk")),
},
))
.await
.unwrap_err();
assert_eq!(
plant.code(),
Code::FailedPrecondition,
"bob has no key to add to: {plant:?}"
);
let bob_made = access
.create_encryption_key(authed(
&bob,
CreateEncryptionKeyRequest {
public_key: bob_key.public(),
first: Some(ssh_method(&bob_key, &bob_line)),
},
))
.await
.expect("bob's own key")
.into_inner();
assert_eq!(bob_made.public_key, bob_key.public());
let steal = access
.remove_unlock_method(authed(
&bob,
RemoveUnlockMethodRequest {
id: yubi.id.clone(),
},
))
.await
.unwrap_err();
assert_eq!(
steal.code(),
Code::NotFound,
"alice's method is not bob's: {steal:?}"
);
let still = access
.get_encryption_key(authed(&alice, GetEncryptionKeyRequest {}))
.await
.expect("Get")
.into_inner();
assert_eq!(
(still.public_key.as_str(), still.methods.len()),
(alice_key.public().as_str(), 2),
"{still:?}"
);
let trimmed = access
.remove_unlock_method(authed(&alice, RemoveUnlockMethodRequest { id: ssh_id }))
.await
.expect("Access.RemoveUnlockMethod")
.into_inner();
assert_eq!(trimmed.methods.len(), 1);
assert_eq!(trimmed.methods[0].id, yubi.id);
let last = access
.remove_unlock_method(authed(
&alice,
RemoveUnlockMethodRequest {
id: yubi.id.clone(),
},
))
.await
.unwrap_err();
assert_eq!(
last.code(),
Code::FailedPrecondition,
"the last method is kept: {last:?}"
);
});
let keys = stores.encryption_keys();
let a = keys.get("alice").unwrap().expect("alice's key");
assert_eq!(
(a.public_key.as_str(), a.methods.len()),
(alice_key.public().as_str(), 1)
);
let b = keys.get("bob").unwrap().expect("bob's key");
assert_eq!(
(b.public_key.as_str(), b.methods.len()),
(bob_key.public().as_str(), 1)
);
let _ = tx.send(());
}