use crate::crypto::{EventSignatureBytes, EventSigner, SignerId};
use crate::error::{Result, ShoreError};
use crate::keys::ssh::protocol::{
ed25519_key_blob, parse_identities_answer, parse_sign_response, request_identities_bytes,
sign_request_bytes,
};
use crate::keys::ssh::transport::connect_agent;
const MAX_AGENT_FRAME: usize = 1 << 20;
pub(crate) trait AgentDuplex: std::io::Read + std::io::Write {
fn send_frame(&mut self, frame: &[u8]) -> Result<()> {
self.write_all(frame).map_err(stream_error)?;
self.flush().map_err(stream_error)?;
Ok(())
}
fn recv_frame(&mut self) -> Result<Vec<u8>> {
let mut len_buf = [0u8; 4];
self.read_exact(&mut len_buf).map_err(stream_error)?;
let len = u32::from_be_bytes(len_buf) as usize;
if len > MAX_AGENT_FRAME {
return Err(ShoreError::Message(
"ssh-agent reply exceeds the maximum message size".to_owned(),
));
}
let mut frame = vec![0u8; 4 + len];
frame[..4].copy_from_slice(&len_buf);
self.read_exact(&mut frame[4..]).map_err(stream_error)?;
Ok(frame)
}
}
impl<T: std::io::Read + std::io::Write> AgentDuplex for T {}
type AgentConnector = Box<dyn Fn() -> Result<Box<dyn AgentDuplex>> + Send + Sync>;
fn real_connector() -> Result<Box<dyn AgentDuplex>> {
connect_agent().map(|stream| Box::new(stream) as Box<dyn AgentDuplex>)
}
pub struct SshAgentSigner {
signer_id: SignerId,
public_key: [u8; 32],
connect: AgentConnector,
}
impl SshAgentSigner {
pub(crate) fn new(signer_id: SignerId, public_key: [u8; 32]) -> Self {
Self {
signer_id,
public_key,
connect: Box::new(real_connector),
}
}
#[cfg(test)]
pub(crate) fn with_connector(
signer_id: SignerId,
public_key: [u8; 32],
connect: impl Fn() -> Result<Box<dyn AgentDuplex>> + Send + Sync + 'static,
) -> Self {
Self {
signer_id,
public_key,
connect: Box::new(connect),
}
}
}
impl EventSigner for SshAgentSigner {
fn signer_id(&self) -> &SignerId {
&self.signer_id
}
fn sign_event_message(&self, message: &[u8]) -> Result<EventSignatureBytes> {
let mut stream = (self.connect)()?;
stream.send_frame(&sign_request_bytes(&self.public_key, message))?;
let response = stream.recv_frame()?;
parse_sign_response(&response)
}
}
pub enum AgentUnavailable {
Socket,
KeyAbsent,
}
pub fn preflight_ssh_agent_signer(
public_key: [u8; 32],
) -> std::result::Result<SshAgentSigner, AgentUnavailable> {
preflight_with(public_key, real_connector)
}
pub fn agent_has_key(public_key: [u8; 32]) -> Option<bool> {
agent_has_key_with(public_key, real_connector)
}
pub(crate) fn preflight_with(
public_key: [u8; 32],
connect: impl Fn() -> Result<Box<dyn AgentDuplex>>,
) -> std::result::Result<SshAgentSigner, AgentUnavailable> {
let blobs = list_identities(&connect).map_err(|_| AgentUnavailable::Socket)?;
if !blobs.contains(&ed25519_key_blob(&public_key)) {
return Err(AgentUnavailable::KeyAbsent);
}
Ok(SshAgentSigner::new(
SignerId::from_ed25519_public_key(public_key),
public_key,
))
}
pub(crate) fn agent_has_key_with(
public_key: [u8; 32],
connect: impl Fn() -> Result<Box<dyn AgentDuplex>>,
) -> Option<bool> {
let blobs = list_identities(&connect).ok()?;
Some(blobs.contains(&ed25519_key_blob(&public_key)))
}
fn list_identities(connect: &impl Fn() -> Result<Box<dyn AgentDuplex>>) -> Result<Vec<Vec<u8>>> {
let mut stream = connect()?;
stream.send_frame(&request_identities_bytes())?;
let answer = stream.recv_frame()?;
parse_identities_answer(&answer)
}
fn stream_error(error: std::io::Error) -> ShoreError {
ShoreError::Message(format!("ssh-agent stream error: {error}"))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crypto::{EventVerificationStatus, SignerId, verify_ed25519_strict};
use crate::keys::ssh::protocol::fake::FakeSshAgent;
const SEED: [u8; 32] = [42_u8; 32];
fn pubkey() -> [u8; 32] {
ed25519_dalek::SigningKey::from_bytes(&SEED)
.verifying_key()
.to_bytes()
}
fn expected_signer() -> SignerId {
SignerId::from_ed25519_public_key(pubkey())
}
fn duplex_connector(
make: impl Fn() -> FakeSshAgent + Send + Sync + 'static,
) -> impl Fn() -> Result<Box<dyn AgentDuplex>> + Send + Sync {
move || Ok(Box::new(make().into_duplex()) as Box<dyn AgentDuplex>)
}
#[test]
fn sign_event_message_round_trips_and_verifies_strict() {
let signer = SshAgentSigner::with_connector(
expected_signer(),
pubkey(),
duplex_connector(|| FakeSshAgent::with_key(SEED)),
);
let message = crate::session::event::pre_authentication_encoding(
crate::session::event::EVENT_TO_BE_SIGNED_V1_PAYLOAD_TYPE,
br#"{"schema":"shore.event","version":1}"#,
);
let sig = signer.sign_event_message(&message).unwrap();
assert!(sig.is_base64());
assert_eq!(
verify_ed25519_strict(signer.signer_id(), &message, sig.as_str()).unwrap(),
EventVerificationStatus::Valid
);
}
#[test]
fn signer_id_is_the_parsed_did_key() {
let signer = SshAgentSigner::with_connector(
expected_signer(),
pubkey(),
duplex_connector(|| FakeSshAgent::with_key(SEED)),
);
assert_eq!(signer.signer_id(), &expected_signer());
}
#[test]
fn a_locked_agent_surfaces_as_err_from_sign_event_message() {
let signer = SshAgentSigner::with_connector(
expected_signer(),
pubkey(),
duplex_connector(|| FakeSshAgent::with_key(SEED).locked()),
);
assert!(signer.sign_event_message(b"DSSEv1 4 test 5 hi").is_err());
}
#[test]
fn a_connect_failure_surfaces_as_err_from_sign_event_message() {
let signer = SshAgentSigner::with_connector(expected_signer(), pubkey(), || {
Err(ShoreError::Message("no agent".to_owned()))
});
assert!(signer.sign_event_message(b"DSSEv1 4 test 5 hi").is_err());
}
#[test]
fn preflight_returns_a_signer_for_a_loaded_key_without_probing_sign() {
let result = preflight_with(
pubkey(),
duplex_connector(|| FakeSshAgent::with_key(SEED).refuses_sign()),
);
assert!(result.is_ok());
}
#[test]
fn preflight_reports_key_absent_for_an_empty_or_locked_agent() {
let absent = preflight_with(pubkey(), duplex_connector(FakeSshAgent::empty));
assert!(matches!(absent, Err(AgentUnavailable::KeyAbsent)));
let locked = preflight_with(
pubkey(),
duplex_connector(|| FakeSshAgent::with_key(SEED).locked()),
);
assert!(matches!(locked, Err(AgentUnavailable::KeyAbsent)));
}
#[test]
fn preflight_reports_socket_on_a_connect_failure() {
let result = preflight_with(pubkey(), || Err(ShoreError::Message("no agent".to_owned())));
assert!(matches!(result, Err(AgentUnavailable::Socket)));
}
#[test]
fn agent_has_key_distinguishes_loaded_absent_and_unknown() {
let loaded =
agent_has_key_with(pubkey(), duplex_connector(|| FakeSshAgent::with_key(SEED)));
assert_eq!(loaded, Some(true));
let absent = agent_has_key_with(pubkey(), duplex_connector(FakeSshAgent::empty));
assert_eq!(absent, Some(false));
let unknown =
agent_has_key_with(pubkey(), || Err(ShoreError::Message("no agent".to_owned())));
assert_eq!(unknown, None);
}
}