wip: user/login: add cert to ssh agent
parent
355a499aa0
commit
40349f5c41
|
@ -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"
|
||||
|
|
|
@ -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(())
|
||||
}
|
|
@ -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(())
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue