user/login: Add cert to SSH agent
dustin/sshca-cli/pipeline/pr-master There was a failure building this commit Details
dustin/sshca-cli/pipeline/head This commit looks good Details

An SSH certificate is useless on its own, as without the private key,
clients cannot sign servers' authentication requests.  Since `sshca-cli
user login` creates a new key pair each time it is run, the private key
needs to be kept at least as long as the certificate is valid.  To that
end, the command will now add both to the user's SSH agent.  It
communicates with the agent via the UNIX stream socket specified by the
`SSH_AUTH_SOCK` environment variable.

Although there is a Rust crate, [ssh-agent-client-rs][0] that implements
the client side of the SSH agent protocol, it does not support adding
certificates to the agent.  In fact, that functionality is not even
documented in the IETF draft specification for the protocol.  Thus, I
had to figure it out by reading the code of the OpenSSH `ssh-add` tool,
and observing the messages passed between it and `ssh-agent`.

[0]: https://crates.io/crates/ssh-agent-client-rs
dev/auto-reload
Dustin 2024-01-30 21:00:17 -06:00
parent 123ca813a7
commit d443542ee0
3 changed files with 142 additions and 4 deletions

View File

@ -22,7 +22,7 @@ ssh-encoding = "0.2.0"
ssh-key = { version = "0.6.4", features = ["ed25519"] }
tera = { version = "1.19.1", default-features = false }
thiserror = "1.0.50"
tokio = { version = "1.33.0", features = ["rt", "macros"] }
tokio = { version = "1.33.0", features = ["rt", "macros", "net"] }
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
uuid = "1.5.0"

133
src/user/agent.rs Normal file
View File

@ -0,0 +1,133 @@
//! SSH Agent client
//!
//! The `sshca-cli user login` command will automatically add the
//! signed certificate it received from SSHCA to the user's SSH agent.
use ssh_encoding::{Encode, Writer};
use ssh_key::{private::KeypairData, Certificate, PrivateKey};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::UnixStream;
use tracing::{debug, info, trace};
/// Error type for SSH agent communication
#[derive(Debug, thiserror::Error)]
pub enum AgentError {
#[error("SSH agent not found: {0}")]
NotFound(std::env::VarError),
#[error("Could not connect to SSH agent: {0}")]
Connect(std::io::Error),
#[error("Encoding error: {0}")]
Encoding(#[from] ssh_encoding::Error),
#[error("Invalid message length: {0}")]
IvalidMessageLength(#[from] std::num::TryFromIntError),
#[error("Error communicating with SSH agent: {0}")]
Io(#[from] std::io::Error),
#[error("SSH agent returned failure")]
Failure,
}
/// SSH Agent message types
#[repr(u8)]
enum AgentMessageType {
AddIdentity = 17,
}
/// SSH Agent response code
enum AgentResponseCode {
Success,
Failure,
InvalidFormat,
}
impl From<u8> for AgentResponseCode {
fn from(v: u8) -> Self {
// https://github.com/openssh/openssh-portable/blob/V_9_0_P1/authfd.c#L74
match v {
6 => Self::Success,
30 | 102 | 229 => Self::Failure,
_ => Self::InvalidFormat,
}
}
}
/// Add an SSH certificate and private key to the SSH Agent
///
/// This function adds an SSH certificate and its corresponding private
/// key to the SSH Agent.
///
/// If an error occurs while attempting to add the key to the agent,
/// including if no agent is configured, [`AgentError`] is returned.
pub async fn add_to_agent(
key: &PrivateKey,
cert: &Certificate,
) -> Result<(), AgentError> {
let sock_path =
std::env::var("SSH_AUTH_SOCK").map_err(AgentError::NotFound)?;
debug!("Connecting to SSH agent at {:?}", sock_path);
let mut sock = UnixStream::connect(sock_path)
.await
.map_err(AgentError::Connect)?;
trace!("Serializing SSH2_AGENTC_ADD_IDENTITY message");
let mut buf: Vec<u8> = vec![];
buf.push(AgentMessageType::AddIdentity as u8);
serialize_key_cert(key, cert, &mut buf)?;
let len = u32::try_from(buf.len())?;
debug!("Sending key to SSH agent");
sock.write_all(&len.to_be_bytes()).await?;
sock.write_all(&buf[..]).await?;
let mut res_len = [0u8; 4];
trace!("Waiting for SSH agent response");
sock.read_exact(&mut res_len[..]).await?;
let res_len = usize::try_from(u32::from_be_bytes(res_len))?;
let mut res = vec![0u8; res_len];
trace!("Reading {} bytes from SSH agent", res_len);
sock.read_exact(&mut res).await?;
trace!("Received SSH agent response: {:?}", res);
match res[0].into() {
AgentResponseCode::Success => {
info!("Successfully added SSH user certificate to SSH Agent");
Ok(())
}
_ => Err(AgentError::Failure),
}
}
/// Serialize an SSH certificate and private key to send to SSH Agent
///
/// This function takes a byte buffer and fills it with the wire
/// representation of an SSH certificate and its corresponding provate
/// key, in order to send them to the SSH Agent.
///
/// The [draft-miller-ssh-agent-11][0] protocol does not specify how
/// certificates are sent to the SSH Agent. The message format used
/// here was discovered by reading the OpenSSH portable code,
/// specifically [sshkey.c][1], and observing communications between
/// `ssh-add` and `ssh-agent`.
///
/// [0]: https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent-11
/// [1]: https://github.com/openssh/openssh-portable/blob/V_9_0_P1/sshkey.c#L3230
fn serialize_key_cert(
key: &PrivateKey,
cert: &Certificate,
buf: &mut impl Writer,
) -> Result<(), ssh_encoding::Error> {
cert.algorithm().to_certificate_type().encode(buf)?;
cert.encode_prefixed(buf)?;
match key.key_data() {
KeypairData::Dsa(k) => k.encode(buf)?,
KeypairData::Ecdsa(k) => k.encode(buf)?,
KeypairData::Ed25519(k) => k.encode(buf)?,
KeypairData::Encrypted(k) => k.encode(buf)?,
KeypairData::Rsa(k) => k.encode(buf)?,
KeypairData::SkEcdsaSha2NistP256(k) => k.encode(buf)?,
KeypairData::SkEd25519(k) => k.encode(buf)?,
KeypairData::Other(k) => k.encode(buf)?,
&_ => todo!(),
};
key.comment().encode(buf)?;
Ok(())
}

View File

@ -2,6 +2,7 @@
//!
//! The `sshca user` sub-command handles user-based operations, such
//! as signing an SSH user certificate.
mod agent;
mod cert;
mod login;
@ -10,7 +11,7 @@ use std::time::Duration;
use argh::FromArgs;
use ssh_key::rand_core::OsRng;
use ssh_key::{Algorithm, PrivateKey};
use tracing::debug;
use tracing::{debug, error};
use crate::MainResult;
@ -74,8 +75,12 @@ async fn login(args: LoginArgs) -> MainResult {
.map_err(|e| format!("Error generating key pair: {0}", e))?;
let pubkey = privkey.public_key();
let cert = cert::sign_key(&token, pubkey).await?;
if let Ok(c) = cert.to_openssh() {
println!("{}", c);
if let Err(e) = agent::add_to_agent(&privkey, &cert).await {
error!("Failed to add certificate to SSH agent: {}", e);
if let Ok(c) = cert.to_openssh() {
println!("{}", c);
}
}
Ok(())
}