use std::io::{self, Error, ErrorKind};
use anyhow::{Result, anyhow};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::UnixStream;
use tokio::time::{Duration, timeout};
use super::{SSHSIG_PREAMBLE, agent::AgentIdentity, read_ssh_bytes};
const SSH_ED25519_ALGORITHM: &str = "ssh-ed25519";
const SESSION_BIND_EXTENSION: &[u8] = b"session-bind@openssh.com";
const SSH_MSG_USERAUTH_REQUEST: u8 = 50;
const USERAUTH_PUBLICKEY: &[u8] = b"publickey";
const USERAUTH_PUBLICKEY_HOSTBOUND: &[u8] = b"publickey-hostbound-v00@openssh.com";
const CONNECTION_IO_TIMEOUT: Duration = Duration::from_millis(250);
const MAX_AGENT_FRAME_SIZE: usize = 1 << 20;
const MAX_SESSION_ID_LEN: usize = 128;
pub const SSH_AGENT_FAILURE: u8 = 5;
pub(super) const SSH_AGENT_SUCCESS: u8 = 6;
pub const SSH_AGENTC_REQUEST_IDENTITIES: u8 = 11;
pub const SSH_AGENT_IDENTITIES_ANSWER: u8 = 12;
pub const SSH_AGENTC_SIGN_REQUEST: u8 = 13;
pub const SSH_AGENT_SIGN_RESPONSE: u8 = 14;
pub(super) const SSH_AGENTC_EXTENSION: u8 = 27;
pub(super) const SSH_AGENT_EXTENSION_FAILURE: u8 = 28;
#[cfg_attr(test, derive(Debug))]
pub(super) struct AgentSignRequest<'a> {
pub public_key_blob: &'a [u8],
pub data: &'a [u8],
}
pub(super) struct SessionBind<'a> {
pub host_key_blob: &'a [u8],
pub session_id: &'a [u8],
pub signature_blob: &'a [u8],
pub is_forwarding: bool,
}
#[cfg_attr(test, derive(Debug, PartialEq))]
pub(super) enum SignedData<'a> {
Userauth {
session_id: &'a [u8],
public_key_blob: &'a [u8],
server_host_key_blob: Option<&'a [u8]>,
},
SshSig {
namespace: &'a str,
},
Unrecognized,
}
pub fn encode_agent_frame(message_type: u8, payload: &[u8]) -> Vec<u8> {
let mut frame = Vec::with_capacity(4 + 1 + payload.len());
frame.extend_from_slice(&((payload.len() + 1) as u32).to_be_bytes());
frame.push(message_type);
frame.extend_from_slice(payload);
frame
}
pub async fn read_frame(stream: &mut UnixStream) -> io::Result<Option<Vec<u8>>> {
let mut length_bytes = [0u8; 4];
match stream.read_exact(&mut length_bytes).await {
Ok(_) => {}
Err(error) if error.kind() == ErrorKind::UnexpectedEof => return Ok(None),
Err(error) => return Err(error),
}
let length = u32::from_be_bytes(length_bytes) as usize;
if length > MAX_AGENT_FRAME_SIZE {
return Err(Error::new(
ErrorKind::InvalidData,
"SSH agent frame exceeds maximum size",
));
}
let mut frame = vec![0u8; 4 + length];
frame[..4].copy_from_slice(&length_bytes);
read_exact_with_deadline(stream, &mut frame[4..]).await?;
Ok(Some(frame))
}
pub(crate) async fn write_frame(stream: &mut UnixStream, frame: &[u8]) -> io::Result<()> {
match timeout(CONNECTION_IO_TIMEOUT, stream.write_all(frame)).await {
Ok(Ok(())) => Ok(()),
Ok(Err(error)) => Err(error),
Err(_) => Err(Error::from(ErrorKind::TimedOut)),
}
}
pub fn encode_request_identities_response(identities: &[AgentIdentity]) -> Vec<u8> {
let mut payload = Vec::new();
payload.extend_from_slice(&(identities.len() as u32).to_be_bytes());
for identity in identities {
payload = encode_string(&identity.public_key.blob(), payload);
payload = encode_string(identity.comment.as_bytes(), payload);
}
encode_agent_frame(SSH_AGENT_IDENTITIES_ANSWER, &payload)
}
pub(super) fn parse_sign_request(payload: &[u8]) -> Result<AgentSignRequest<'_>> {
let mut cursor = payload;
let public_key_blob = read_ssh_bytes(&mut cursor)?;
let data = read_ssh_bytes(&mut cursor)?;
read_u32(&mut cursor)?;
if !cursor.is_empty() {
return Err(anyhow!("unexpected trailing SSH agent sign request data"));
}
Ok(AgentSignRequest {
public_key_blob,
data,
})
}
pub(super) fn parse_extension(payload: &[u8]) -> Option<&[u8]> {
let mut cursor = payload;
let name = read_ssh_bytes(&mut cursor).ok()?;
(name == SESSION_BIND_EXTENSION).then_some(cursor)
}
pub(super) fn parse_session_bind(contents: &[u8]) -> Result<SessionBind<'_>> {
let mut cursor = contents;
let host_key_blob = read_ssh_bytes(&mut cursor)?;
let session_id = read_ssh_bytes(&mut cursor)?;
if session_id.len() > MAX_SESSION_ID_LEN {
return Err(anyhow!(
"session-bind session ID is longer than {MAX_SESSION_ID_LEN} bytes"
));
}
let signature_blob = read_ssh_bytes(&mut cursor)?;
let Some((&flag, rest)) = cursor.split_first() else {
return Err(anyhow!("truncated SSH agent boolean"));
};
if !rest.is_empty() {
return Err(anyhow!("unexpected trailing SSH agent session-bind data"));
}
Ok(SessionBind {
host_key_blob,
session_id,
signature_blob,
is_forwarding: flag != 0,
})
}
pub(super) fn classify_signed_data(data: &[u8]) -> SignedData<'_> {
if let Some(userauth) = parse_userauth(data) {
return userauth;
}
if let Some(namespace) = parse_sshsig_namespace(data) {
return SignedData::SshSig { namespace };
}
SignedData::Unrecognized
}
fn parse_userauth(data: &[u8]) -> Option<SignedData<'_>> {
let mut cursor = data;
let session_id = read_ssh_bytes(&mut cursor).ok()?;
let (message_type, mut cursor) = cursor.split_first()?;
if *message_type != SSH_MSG_USERAUTH_REQUEST {
return None;
}
read_ssh_bytes(&mut cursor).ok()?;
if read_ssh_bytes(&mut cursor).ok()? != b"ssh-connection".as_slice() {
return None;
}
let method = read_ssh_bytes(&mut cursor).ok()?;
let is_hostbound = match method {
USERAUTH_PUBLICKEY => false,
USERAUTH_PUBLICKEY_HOSTBOUND => true,
_ => return None,
};
let (has_signature, mut cursor) = cursor.split_first()?;
if *has_signature == 0 {
return None;
}
read_ssh_bytes(&mut cursor).ok()?;
let public_key_blob = read_ssh_bytes(&mut cursor).ok()?;
let server_host_key_blob = if is_hostbound {
Some(read_ssh_bytes(&mut cursor).ok()?)
} else {
None
};
if !cursor.is_empty() {
return None;
}
Some(SignedData::Userauth {
session_id,
public_key_blob,
server_host_key_blob,
})
}
fn parse_sshsig_namespace(data: &[u8]) -> Option<&str> {
let mut cursor = data.strip_prefix(SSHSIG_PREAMBLE)?;
let namespace = read_ssh_bytes(&mut cursor).ok()?;
for _ in 0..3 {
read_ssh_bytes(&mut cursor).ok()?;
}
if !cursor.is_empty() {
return None;
}
str::from_utf8(namespace).ok()
}
pub fn encode_sign_response(signature: &[u8; 64]) -> Vec<u8> {
let mut signature_blob = Vec::new();
signature_blob = encode_string(SSH_ED25519_ALGORITHM.as_bytes(), signature_blob);
signature_blob = encode_string(signature, signature_blob);
let mut payload = Vec::new();
payload = encode_string(&signature_blob, payload);
encode_agent_frame(SSH_AGENT_SIGN_RESPONSE, &payload)
}
async fn read_exact_with_deadline(stream: &mut UnixStream, buf: &mut [u8]) -> io::Result<()> {
match timeout(CONNECTION_IO_TIMEOUT, stream.read_exact(buf)).await {
Ok(Ok(_)) => Ok(()),
Ok(Err(error)) => Err(error),
Err(_) => Err(Error::from(ErrorKind::TimedOut)),
}
}
fn encode_string(bytes: &[u8], mut output: Vec<u8>) -> Vec<u8> {
output.extend_from_slice(&(bytes.len() as u32).to_be_bytes());
output.extend_from_slice(bytes);
output
}
pub(super) fn parse_agent_frame(frame: &[u8]) -> Result<(u8, &[u8])> {
let Some((length, payload)) = frame.split_first_chunk::<4>() else {
return Err(anyhow!("truncated SSH agent frame length"));
};
let length = u32::from_be_bytes(*length) as usize;
if payload.len() < length {
return Err(anyhow!("truncated SSH agent frame payload"));
}
if payload.len() != length {
return Err(anyhow!("unexpected trailing bytes in SSH agent frame"));
}
let Some((kind, body)) = payload.split_first() else {
return Err(anyhow!("truncated SSH agent frame payload"));
};
Ok((*kind, body))
}
fn read_u32(cursor: &mut &[u8]) -> Result<u32> {
let Some((value, rest)) = cursor.split_first_chunk::<4>() else {
return Err(anyhow!("truncated SSH agent unsigned 32-bit integer"));
};
*cursor = rest;
Ok(u32::from_be_bytes(*value))
}