crates/gunnar-pairing-door/src/grpc.rs
//! **[`Boxes`] over the boxes' real control planes** — the generated clients,
//! the one credential signer (`gunnar_grpc::client_signer`) and the membership
//! signer (`gunnar_core::membership`), the same calls `gunnar control user
//! add`, `group add`, `group member add` and `twin certify` make.
//!
//! The planes are dialled at `<utility>:50051` in plain HTTP/2, as the front's
//! CLI dialled them for the xtask: every call that changes anything carries a
//! fresh sshsig credential from the console key — the twin-making Bootstrap
//! calls too (`BecomeTwin`, `InstallTwinCertificate`, h-bootstrap-open
//! 2026-09-28: the twin takes them only from a key it was fed, and the console
//! key is one, `offer.<front>.console_pubkey`).
//! `client_signer::plain_channel` is NOT used: it refuses any off-box target
//! by design, and this is the one caller that has to cross the utility network.
use crate::certify::{Boxes, Issued};
use gunnar_core::ssh_key;
use gunnar_grpc::client_signer::{authed, ControlChannel, CredentialSigner};
use gunnar_proto::access_client::AccessClient;
use gunnar_proto::admin_client::AdminClient;
use gunnar_proto::bootstrap_client::BootstrapClient;
use std::time::Duration;
use tonic::transport::Channel;
use tonic::{Code, Status};
pub struct GrpcBoxes {
key: ssh_key::PrivateKey,
rt: tokio::runtime::Runtime,
timeout: Duration,
}
impl GrpcBoxes {
pub fn new(key: ssh_key::PrivateKey, timeout: Duration) -> Result<GrpcBoxes, String> {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| format!("a runtime for the certify calls: {e}"))?;
Ok(GrpcBoxes { key, rt, timeout })
}
fn channel(&self, at: &str) -> Result<ControlChannel, String> {
let _g = self.rt.enter();
let endpoint = Channel::from_shared(format!("http://{at}"))
.map_err(|e| format!("{at:?} is not a control-plane address: {e}"))?
.connect_timeout(self.timeout)
.timeout(self.timeout);
Ok(tonic::service::interceptor::InterceptedService::new(
endpoint.connect_lazy(),
CredentialSigner,
))
}
}
/// A refusal in words that keep "could not be reached" apart from "refused".
pub fn say(status: &Status) -> String {
match status.code() {
Code::Unavailable | Code::DeadlineExceeded => format!(
"could not be reached ({:?}): {}",
status.code(),
status.message()
),
code => format!("{code:?}: {}", status.message()),
}
}
/// An idempotent enrolment step whose thing is already there.
pub fn already(status: &Status) -> bool {
status.code() == Code::AlreadyExists || {
let m = status.message().to_ascii_lowercase();
m.contains("already exists")
|| m.contains("already a member")
|| m.contains("already registered")
}
}
fn tolerate<T>(r: Result<T, Status>, what: &str) -> Result<(), String> {
match r {
Ok(_) => Ok(()),
Err(s) if already(&s) => Ok(()),
Err(s) => Err(format!("{what}: {}", say(&s))),
}
}
fn now_ms() -> i64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0)
}
impl Boxes for GrpcBoxes {
fn twin_key(&self, twin_ctl: &str, primary_for_twin: &str) -> Result<String, String> {
let mut twin = BootstrapClient::new(self.channel(twin_ctl)?);
let r = self
.rt
.block_on(twin.become_twin(authed(
&self.key,
gunnar_proto::BecomeTwinRequest {
primary: primary_for_twin.to_string(),
},
)))
.map_err(|s| say(&s))?;
Ok(r.into_inner().twin_public_key)
}
fn enrol(&self, primary_ctl: &str, principal: &str, key: &str) -> Result<(), String> {
let mut access = AccessClient::new(self.channel(primary_ctl)?);
let rt = &self.rt;
tolerate(
rt.block_on(access.add_principal(authed(
&self.key,
gunnar_proto::AddPrincipalRequest {
name: principal.into(),
kind: gunnar_proto::PrincipalKind::Machine as i32,
id: principal.into(),
},
))),
"the principal",
)?;
// **The twin's key REPLACES the principal's other ssh keys** (twin reborn,
// 2026-09-30). ONE twin per principal — [`crate::certify::TWIN_PRINCIPAL`]
// is the one name the owner chose for now — so a NEW key here is a new
// twin box, and the one it replaces is dead (nuked: its disk may survive
// as an image or a backup). Its key is revoked FIRST: left registered it
// still signs `Mirror.LogFetch` on the plain control plane, and while it is
// there `Access.AddKey` refuses the new key without an attestation by the
// old one — which died with the box, so the door retried for ever. The
// same key again (a re-run for the same twin) revokes nothing and adds
// nothing new. Certificates are `Admin.IssueTwinCertificate`'s to replace.
let fingerprint = gunnar_core::Fingerprint::of_openssh(key.trim())
.map_err(|e| format!("the twin's key {key:?}: {e}"))?;
let held = rt
.block_on(access.list_keys(authed(
&self.key,
gunnar_proto::ListKeysRequest {
principal_id: principal.into(),
},
)))
.map_err(|s| format!("the keys {principal} holds: {}", say(&s)))?
.into_inner()
.keys;
for old in held.iter().filter(|k| {
k.principal_id == principal && k.algo != "x509" && k.fingerprint != fingerprint.as_str()
}) {
rt.block_on(access.revoke_key(authed(
&self.key,
gunnar_proto::RevokeKeyRequest {
fingerprint: old.fingerprint.clone(),
},
)))
.map_err(|s| {
format!(
"revoking {principal}'s previous key {}: {}",
old.fingerprint,
say(&s)
)
})?;
}
tolerate(
rt.block_on(access.add_key(authed(
&self.key,
gunnar_proto::AddKeyRequest {
principal_id: principal.into(),
openssh_public_key: key.into(),
comment: "the twin's key, enrolled by the pairing door".into(),
expires_at_unix_ms: None,
attestation: None,
},
))),
"the twin's key",
)?;
tolerate(
rt.block_on(access.add_principal(authed(
&self.key,
gunnar_proto::AddPrincipalRequest {
name: gunnar_grpc::TWIN_GROUP.into(),
kind: gunnar_proto::PrincipalKind::Group as i32,
id: String::new(),
},
))),
"the group",
)?;
// The membership change is signed into the membership log by the
// acting admin — the console key's own principal, looked up by it.
let fingerprint = self
.key
.public_key()
.fingerprint(ssh_key::HashAlg::Sha256)
.to_string();
let keys = rt
.block_on(access.list_keys(authed(
&self.key,
gunnar_proto::ListKeysRequest {
principal_id: String::new(),
},
)))
.map_err(|s| format!("the console key's principal: {}", say(&s)))?
.into_inner()
.keys;
let actor = keys
.into_iter()
.find(|k| k.fingerprint == fingerprint)
.map(|k| k.principal_id)
.ok_or_else(|| {
format!("the console key {fingerprint} is not registered on the primary")
})?;
let event = gunnar_core::MembershipEvent {
group: gunnar_grpc::TWIN_GROUP.into(),
member: principal.into(),
action: gunnar_core::MembershipAction::Add,
actor,
at_unix_ms: now_ms(),
};
let signed = gunnar_core::membership::sign_event(&self.key, event)
.map_err(|e| format!("signing the membership: {e}"))?;
let signed = gunnar_proto::SignedMembershipEvent {
event: Some(gunnar_proto::MembershipEvent {
group: signed.event.group,
member: signed.event.member,
action: gunnar_proto::MembershipAction::Add as i32,
actor: signed.event.actor,
at_unix_ms: signed.event.at_unix_ms,
}),
key_fingerprint: signed.key_fingerprint,
signature: signed.signature,
};
tolerate(
rt.block_on(access.add_group_member(authed(
&self.key,
gunnar_proto::AddGroupMemberRequest {
group_id: gunnar_grpc::TWIN_GROUP.into(),
member_id: principal.into(),
signed: Some(signed),
},
))),
"the membership",
)
}
fn twin_csr(&self, twin_ctl: &str) -> Result<String, String> {
let mut twin = BootstrapClient::new(self.channel(twin_ctl)?);
Ok(self
.rt
.block_on(twin.twin_csr(gunnar_proto::TwinCsrRequest {}))
.map_err(|s| say(&s))?
.into_inner()
.csr_pem)
}
fn issue(&self, primary_ctl: &str, principal: &str, csr_pem: &str) -> Result<Issued, String> {
let mut admin = AdminClient::new(self.channel(primary_ctl)?);
let r = self
.rt
.block_on(admin.issue_twin_certificate(authed(
&self.key,
gunnar_proto::IssueTwinCertificateRequest {
principal: principal.into(),
csr_pem: csr_pem.into(),
},
)))
.map_err(|s| say(&s))?
.into_inner();
Ok(Issued {
cert_pem: r.cert_pem,
issuer_pem: r.issuer_pem,
port: r.replication_port,
host: r.replication_host,
})
}
fn install(&self, twin_ctl: &str, issued: &Issued) -> Result<i64, String> {
let mut twin = BootstrapClient::new(self.channel(twin_ctl)?);
let r = self
.rt
.block_on(twin.install_twin_certificate(authed(
&self.key,
gunnar_proto::InstallTwinCertificateRequest {
cert_pem: issued.cert_pem.clone(),
ca_pem: issued.issuer_pem.clone(),
replication_port: issued.port,
replication_host: issued.host.clone(),
},
)))
.map_err(|s| say(&s))?;
Ok(r.into_inner().days_left)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn already_there_is_fine_and_unreachable_is_said_apart_from_refused() {
assert!(already(&Status::already_exists("principal gunnar-twin")));
assert!(already(&Status::failed_precondition(
"gunnar-twin is already a member of twin"
)));
assert!(!already(&Status::permission_denied("not an admin")));
assert!(say(&Status::unavailable("tcp connect error")).starts_with("could not be reached"));
assert!(say(&Status::unauthenticated("no key")).starts_with("Unauthenticated:"));
}
/// A real dial at a port nobody listens on: the door's error names it
/// unreachable, within the timeout, and never hangs the thread.
#[test]
fn a_box_that_is_not_there_is_unreachable_not_refused() {
let key =
ssh_key::PrivateKey::random(&mut gunnar_core::os_rng(), ssh_key::Algorithm::Ed25519)
.unwrap();
let boxes = GrpcBoxes::new(key, Duration::from_secs(2)).unwrap();
let port = std::net::TcpListener::bind("127.0.0.1:0")
.unwrap()
.local_addr()
.unwrap()
.port();
let e = boxes
.twin_key(&format!("127.0.0.1:{port}"), "10.44.0.2:50051")
.unwrap_err();
assert!(e.starts_with("could not be reached"), "{e}");
let e = boxes
.enrol(
&format!("127.0.0.1:{port}"),
"gunnar-twin",
"ssh-ed25519 AAAA",
)
.unwrap_err();
assert!(e.contains("could not be reached"), "{e}");
}
}