Add ssh-agent native auth
This commit is contained in:
+35
-2
@@ -19,6 +19,7 @@ use dosh::protocol::{
|
|||||||
Resize, ResumeRequest, SERVER_TO_CLIENT, TicketAttachBody, TicketAttachEnvelope,
|
Resize, ResumeRequest, SERVER_TO_CLIENT, TicketAttachBody, TicketAttachEnvelope,
|
||||||
TicketAttachOkEnvelope,
|
TicketAttachOkEnvelope,
|
||||||
};
|
};
|
||||||
|
use dosh::ssh_agent;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
@@ -899,8 +900,13 @@ async fn try_native_auth(
|
|||||||
&hello,
|
&hello,
|
||||||
&server_hello.hello,
|
&server_hello.hello,
|
||||||
)?;
|
)?;
|
||||||
let identity = load_first_native_identity(config, cli_identity, &ssh_config)?;
|
let auth = sign_native_user_auth(
|
||||||
let auth = sign_user_auth(&identity, &hello, &server_hello.hello, Vec::new())?;
|
config,
|
||||||
|
cli_identity,
|
||||||
|
&ssh_config,
|
||||||
|
&hello,
|
||||||
|
&server_hello.hello,
|
||||||
|
)?;
|
||||||
let mut pending_id = [0u8; 16];
|
let mut pending_id = [0u8; 16];
|
||||||
pending_id.copy_from_slice(&server_hello.hello.auth_challenge[..16]);
|
pending_id.copy_from_slice(&server_hello.hello.auth_challenge[..16]);
|
||||||
let auth_body = protocol::to_body(&NativeUserAuthBody { auth })?;
|
let auth_body = protocol::to_body(&NativeUserAuthBody { auth })?;
|
||||||
@@ -953,6 +959,33 @@ async fn try_native_auth(
|
|||||||
Ok((frame, cred))
|
Ok((frame, cred))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn sign_native_user_auth(
|
||||||
|
config: &dosh::config::ClientConfig,
|
||||||
|
cli_identity: Option<&Path>,
|
||||||
|
ssh_config: &SshConfig,
|
||||||
|
hello: &dosh::native::NativeClientHello,
|
||||||
|
server_hello: &dosh::native::NativeServerHello,
|
||||||
|
) -> Result<dosh::native::NativeUserAuth> {
|
||||||
|
let mut errors = Vec::new();
|
||||||
|
if config.use_ssh_agent {
|
||||||
|
match ssh_agent::sign_user_auth_with_agent(hello, server_hello, Vec::new()) {
|
||||||
|
Ok(auth) => return Ok(auth),
|
||||||
|
Err(err) => errors.push(format!("ssh-agent: {err:#}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match load_first_native_identity(config, cli_identity, ssh_config) {
|
||||||
|
Ok(identity) => sign_user_auth(&identity, hello, server_hello, Vec::new()),
|
||||||
|
Err(err) => {
|
||||||
|
errors.push(format!("identity files: {err:#}"));
|
||||||
|
Err(anyhow!(
|
||||||
|
"native auth found no usable key: {}",
|
||||||
|
errors.join("; ")
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn load_first_native_identity(
|
fn load_first_native_identity(
|
||||||
config: &dosh::config::ClientConfig,
|
config: &dosh::config::ClientConfig,
|
||||||
cli_identity: Option<&Path>,
|
cli_identity: Option<&Path>,
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ pub mod crypto;
|
|||||||
pub mod native;
|
pub mod native;
|
||||||
pub mod protocol;
|
pub mod protocol;
|
||||||
pub mod pty;
|
pub mod pty;
|
||||||
|
pub mod ssh_agent;
|
||||||
|
|||||||
+15
-11
@@ -546,7 +546,7 @@ fn strip_quotes(raw: &str) -> &str {
|
|||||||
.unwrap_or(raw)
|
.unwrap_or(raw)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_ssh_ed25519_public_blob(blob: &[u8]) -> Result<[u8; 32]> {
|
pub fn parse_ssh_ed25519_public_blob(blob: &[u8]) -> Result<[u8; 32]> {
|
||||||
let mut cursor = blob;
|
let mut cursor = blob;
|
||||||
let key_type = read_ssh_string(&mut cursor)?;
|
let key_type = read_ssh_string(&mut cursor)?;
|
||||||
anyhow::ensure!(key_type == b"ssh-ed25519", "key blob type mismatch");
|
anyhow::ensure!(key_type == b"ssh-ed25519", "key blob type mismatch");
|
||||||
@@ -558,6 +558,13 @@ fn parse_ssh_ed25519_public_blob(blob: &[u8]) -> Result<[u8; 32]> {
|
|||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn ssh_ed25519_public_blob(public_key: &[u8; 32]) -> Vec<u8> {
|
||||||
|
let mut out = Vec::new();
|
||||||
|
write_ssh_string(&mut out, b"ssh-ed25519");
|
||||||
|
write_ssh_string(&mut out, public_key);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
fn read_ssh_string<'a>(cursor: &mut &'a [u8]) -> Result<&'a [u8]> {
|
fn read_ssh_string<'a>(cursor: &mut &'a [u8]) -> Result<&'a [u8]> {
|
||||||
anyhow::ensure!(cursor.len() >= 4, "truncated SSH string length");
|
anyhow::ensure!(cursor.len() >= 4, "truncated SSH string length");
|
||||||
let len = u32::from_be_bytes(cursor[..4].try_into().unwrap()) as usize;
|
let len = u32::from_be_bytes(cursor[..4].try_into().unwrap()) as usize;
|
||||||
@@ -568,21 +575,18 @@ fn read_ssh_string<'a>(cursor: &mut &'a [u8]) -> Result<&'a [u8]> {
|
|||||||
Ok(value)
|
Ok(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
fn ssh_ed25519_authorized_key_line(signing_key: &SigningKey) -> String {
|
|
||||||
let verifying = VerifyingKey::from(signing_key);
|
|
||||||
let mut blob = Vec::new();
|
|
||||||
write_ssh_string(&mut blob, b"ssh-ed25519");
|
|
||||||
write_ssh_string(&mut blob, &verifying.to_bytes());
|
|
||||||
format!("ssh-ed25519 {} test-key", STANDARD.encode(blob))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
fn write_ssh_string(out: &mut Vec<u8>, value: &[u8]) {
|
fn write_ssh_string(out: &mut Vec<u8>, value: &[u8]) {
|
||||||
out.extend_from_slice(&(value.len() as u32).to_be_bytes());
|
out.extend_from_slice(&(value.len() as u32).to_be_bytes());
|
||||||
out.extend_from_slice(value);
|
out.extend_from_slice(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
fn ssh_ed25519_authorized_key_line(signing_key: &SigningKey) -> String {
|
||||||
|
let verifying = VerifyingKey::from(signing_key);
|
||||||
|
let blob = ssh_ed25519_public_blob(&verifying.to_bytes());
|
||||||
|
format!("ssh-ed25519 {} test-key", STANDARD.encode(blob))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn known_host_line(host: &str, key: &HostPublicKey, source: &str, first_seen: u64) -> String {
|
pub fn known_host_line(host: &str, key: &HostPublicKey, source: &str, first_seen: u64) -> String {
|
||||||
format!(
|
format!(
|
||||||
"{} {} {} first-seen={} source={}",
|
"{} {} {} first-seen={} source={}",
|
||||||
|
|||||||
@@ -0,0 +1,281 @@
|
|||||||
|
use crate::native::{
|
||||||
|
ForwardingRequest, NativeClientHello, NativeServerHello, NativeUserAuth,
|
||||||
|
parse_ssh_ed25519_public_blob, user_auth_transcript,
|
||||||
|
};
|
||||||
|
use anyhow::{Context, Result, anyhow, bail};
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use std::os::unix::net::UnixStream;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
const SSH_AGENT_FAILURE: u8 = 5;
|
||||||
|
const SSH2_AGENTC_REQUEST_IDENTITIES: u8 = 11;
|
||||||
|
const SSH2_AGENT_IDENTITIES_ANSWER: u8 = 12;
|
||||||
|
const SSH2_AGENTC_SIGN_REQUEST: u8 = 13;
|
||||||
|
const SSH2_AGENT_SIGN_RESPONSE: u8 = 14;
|
||||||
|
const MAX_AGENT_PACKET: usize = 256 * 1024;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct AgentIdentity {
|
||||||
|
pub key_blob: Vec<u8>,
|
||||||
|
pub public_key: [u8; 32],
|
||||||
|
pub comment: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sign_user_auth_with_agent(
|
||||||
|
client: &NativeClientHello,
|
||||||
|
server: &NativeServerHello,
|
||||||
|
requested_forwardings: Vec<ForwardingRequest>,
|
||||||
|
) -> Result<NativeUserAuth> {
|
||||||
|
let sock = std::env::var("SSH_AUTH_SOCK").context("SSH_AUTH_SOCK is not set")?;
|
||||||
|
sign_user_auth_with_agent_at(sock, client, server, requested_forwardings)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sign_user_auth_with_agent_at(
|
||||||
|
socket_path: impl AsRef<Path>,
|
||||||
|
client: &NativeClientHello,
|
||||||
|
server: &NativeServerHello,
|
||||||
|
requested_forwardings: Vec<ForwardingRequest>,
|
||||||
|
) -> Result<NativeUserAuth> {
|
||||||
|
let mut agent = UnixStream::connect(socket_path.as_ref())
|
||||||
|
.with_context(|| format!("connect ssh-agent {}", socket_path.as_ref().display()))?;
|
||||||
|
let identities = request_ed25519_identities(&mut agent)?;
|
||||||
|
let identity = identities
|
||||||
|
.first()
|
||||||
|
.ok_or_else(|| anyhow!("ssh-agent has no ssh-ed25519 identities"))?;
|
||||||
|
let mut auth = NativeUserAuth {
|
||||||
|
public_key_algorithm: "ssh-ed25519".to_string(),
|
||||||
|
public_key: identity.public_key.to_vec(),
|
||||||
|
signature: Vec::new(),
|
||||||
|
requested_forwardings,
|
||||||
|
};
|
||||||
|
let transcript = user_auth_transcript(client, server, &auth)?;
|
||||||
|
auth.signature = sign_with_agent(&mut agent, identity, &transcript)
|
||||||
|
.with_context(|| format!("ssh-agent sign with {}", identity.comment))?;
|
||||||
|
Ok(auth)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn request_ed25519_identities(agent: &mut UnixStream) -> Result<Vec<AgentIdentity>> {
|
||||||
|
write_agent_packet(agent, &[SSH2_AGENTC_REQUEST_IDENTITIES])?;
|
||||||
|
let payload = read_agent_packet(agent)?;
|
||||||
|
let mut cursor = payload.as_slice();
|
||||||
|
let response = read_u8(&mut cursor)?;
|
||||||
|
if response == SSH_AGENT_FAILURE {
|
||||||
|
bail!("ssh-agent refused identity request");
|
||||||
|
}
|
||||||
|
anyhow::ensure!(
|
||||||
|
response == SSH2_AGENT_IDENTITIES_ANSWER,
|
||||||
|
"unexpected ssh-agent identity response {response}"
|
||||||
|
);
|
||||||
|
let count = read_u32(&mut cursor)? as usize;
|
||||||
|
anyhow::ensure!(count <= 4096, "ssh-agent returned too many identities");
|
||||||
|
let mut identities = Vec::new();
|
||||||
|
for _ in 0..count {
|
||||||
|
let key_blob = read_ssh_string(&mut cursor)?.to_vec();
|
||||||
|
let comment = String::from_utf8_lossy(read_ssh_string(&mut cursor)?).to_string();
|
||||||
|
if let Ok(public_key) = parse_ssh_ed25519_public_blob(&key_blob) {
|
||||||
|
identities.push(AgentIdentity {
|
||||||
|
key_blob,
|
||||||
|
public_key,
|
||||||
|
comment,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
anyhow::ensure!(cursor.is_empty(), "trailing data in ssh-agent identities");
|
||||||
|
Ok(identities)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sign_with_agent(
|
||||||
|
agent: &mut UnixStream,
|
||||||
|
identity: &AgentIdentity,
|
||||||
|
transcript: &[u8],
|
||||||
|
) -> Result<Vec<u8>> {
|
||||||
|
let mut request = Vec::new();
|
||||||
|
request.push(SSH2_AGENTC_SIGN_REQUEST);
|
||||||
|
write_ssh_string(&mut request, &identity.key_blob);
|
||||||
|
write_ssh_string(&mut request, transcript);
|
||||||
|
request.extend_from_slice(&0u32.to_be_bytes());
|
||||||
|
write_agent_packet(agent, &request)?;
|
||||||
|
|
||||||
|
let payload = read_agent_packet(agent)?;
|
||||||
|
let mut cursor = payload.as_slice();
|
||||||
|
let response = read_u8(&mut cursor)?;
|
||||||
|
if response == SSH_AGENT_FAILURE {
|
||||||
|
bail!("ssh-agent refused signature request");
|
||||||
|
}
|
||||||
|
anyhow::ensure!(
|
||||||
|
response == SSH2_AGENT_SIGN_RESPONSE,
|
||||||
|
"unexpected ssh-agent sign response {response}"
|
||||||
|
);
|
||||||
|
let signature_blob = read_ssh_string(&mut cursor)?;
|
||||||
|
anyhow::ensure!(
|
||||||
|
cursor.is_empty(),
|
||||||
|
"trailing data in ssh-agent sign response"
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut signature_cursor = signature_blob;
|
||||||
|
let algorithm = read_ssh_string(&mut signature_cursor)?;
|
||||||
|
anyhow::ensure!(
|
||||||
|
algorithm == b"ssh-ed25519",
|
||||||
|
"ssh-agent returned unsupported signature algorithm {}",
|
||||||
|
String::from_utf8_lossy(algorithm)
|
||||||
|
);
|
||||||
|
let signature = read_ssh_string(&mut signature_cursor)?;
|
||||||
|
anyhow::ensure!(
|
||||||
|
signature_cursor.is_empty(),
|
||||||
|
"trailing data in ssh-agent signature blob"
|
||||||
|
);
|
||||||
|
anyhow::ensure!(
|
||||||
|
signature.len() == 64,
|
||||||
|
"ssh-agent Ed25519 signature was not 64 bytes"
|
||||||
|
);
|
||||||
|
Ok(signature.to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_agent_packet(stream: &mut UnixStream) -> Result<Vec<u8>> {
|
||||||
|
let mut len = [0u8; 4];
|
||||||
|
stream
|
||||||
|
.read_exact(&mut len)
|
||||||
|
.context("read ssh-agent packet length")?;
|
||||||
|
let len = u32::from_be_bytes(len) as usize;
|
||||||
|
anyhow::ensure!(len <= MAX_AGENT_PACKET, "ssh-agent packet too large");
|
||||||
|
let mut payload = vec![0u8; len];
|
||||||
|
stream
|
||||||
|
.read_exact(&mut payload)
|
||||||
|
.context("read ssh-agent packet body")?;
|
||||||
|
Ok(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_agent_packet(stream: &mut UnixStream, payload: &[u8]) -> Result<()> {
|
||||||
|
anyhow::ensure!(
|
||||||
|
payload.len() <= MAX_AGENT_PACKET,
|
||||||
|
"ssh-agent request too large"
|
||||||
|
);
|
||||||
|
stream.write_all(&(payload.len() as u32).to_be_bytes())?;
|
||||||
|
stream.write_all(payload)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_u8(cursor: &mut &[u8]) -> Result<u8> {
|
||||||
|
anyhow::ensure!(!cursor.is_empty(), "truncated u8");
|
||||||
|
let value = cursor[0];
|
||||||
|
*cursor = &cursor[1..];
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_u32(cursor: &mut &[u8]) -> Result<u32> {
|
||||||
|
anyhow::ensure!(cursor.len() >= 4, "truncated u32");
|
||||||
|
let value = u32::from_be_bytes(cursor[..4].try_into().unwrap());
|
||||||
|
*cursor = &cursor[4..];
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ssh_string<'a>(cursor: &mut &'a [u8]) -> Result<&'a [u8]> {
|
||||||
|
let len = read_u32(cursor)? as usize;
|
||||||
|
anyhow::ensure!(cursor.len() >= len, "truncated SSH string");
|
||||||
|
let (value, rest) = cursor.split_at(len);
|
||||||
|
*cursor = rest;
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_ssh_string(out: &mut Vec<u8>, value: &[u8]) {
|
||||||
|
out.extend_from_slice(&(value.len() as u32).to_be_bytes());
|
||||||
|
out.extend_from_slice(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::native::{
|
||||||
|
host_public_key, parse_authorized_keys, sign_server_hello, ssh_ed25519_public_blob,
|
||||||
|
verify_native_user_auth,
|
||||||
|
};
|
||||||
|
use base64::Engine;
|
||||||
|
use base64::engine::general_purpose::STANDARD;
|
||||||
|
use ed25519_dalek::{Signer, SigningKey, VerifyingKey};
|
||||||
|
use std::os::unix::net::UnixListener;
|
||||||
|
use std::thread;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn agent_ed25519_signature_authenticates_native_user() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let socket_path = dir.path().join("agent.sock");
|
||||||
|
let user_signing = SigningKey::from_bytes(&[51u8; 32]);
|
||||||
|
let user_public = VerifyingKey::from(&user_signing).to_bytes();
|
||||||
|
let key_blob = ssh_ed25519_public_blob(&user_public);
|
||||||
|
let listener = UnixListener::bind(&socket_path).unwrap();
|
||||||
|
thread::spawn(move || fake_agent(listener, key_blob, user_signing));
|
||||||
|
|
||||||
|
let host_signing = SigningKey::from_bytes(&[3u8; 32]);
|
||||||
|
let client = test_client_hello();
|
||||||
|
let mut server = test_server_hello(&host_signing);
|
||||||
|
sign_server_hello(&host_signing, &client, &mut server).unwrap();
|
||||||
|
let auth =
|
||||||
|
sign_user_auth_with_agent_at(&socket_path, &client, &server, Vec::new()).unwrap();
|
||||||
|
let authorized_line = format!(
|
||||||
|
"ssh-ed25519 {} agent-key",
|
||||||
|
STANDARD.encode(ssh_ed25519_public_blob(&user_public))
|
||||||
|
);
|
||||||
|
let authorized = parse_authorized_keys(&authorized_line).unwrap();
|
||||||
|
|
||||||
|
verify_native_user_auth(&client, &server, &auth, &authorized).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fake_agent(listener: UnixListener, key_blob: Vec<u8>, signing: SigningKey) {
|
||||||
|
let (mut stream, _) = listener.accept().unwrap();
|
||||||
|
let payload = read_agent_packet(&mut stream).unwrap();
|
||||||
|
assert_eq!(payload, vec![SSH2_AGENTC_REQUEST_IDENTITIES]);
|
||||||
|
let mut response = Vec::new();
|
||||||
|
response.push(SSH2_AGENT_IDENTITIES_ANSWER);
|
||||||
|
response.extend_from_slice(&1u32.to_be_bytes());
|
||||||
|
write_ssh_string(&mut response, &key_blob);
|
||||||
|
write_ssh_string(&mut response, b"test-agent-key");
|
||||||
|
write_agent_packet(&mut stream, &response).unwrap();
|
||||||
|
|
||||||
|
let payload = read_agent_packet(&mut stream).unwrap();
|
||||||
|
let mut cursor = payload.as_slice();
|
||||||
|
assert_eq!(read_u8(&mut cursor).unwrap(), SSH2_AGENTC_SIGN_REQUEST);
|
||||||
|
assert_eq!(read_ssh_string(&mut cursor).unwrap(), key_blob.as_slice());
|
||||||
|
let transcript = read_ssh_string(&mut cursor).unwrap();
|
||||||
|
assert_eq!(read_u32(&mut cursor).unwrap(), 0);
|
||||||
|
assert!(cursor.is_empty());
|
||||||
|
let signature = signing.sign(transcript).to_bytes();
|
||||||
|
let mut signature_blob = Vec::new();
|
||||||
|
write_ssh_string(&mut signature_blob, b"ssh-ed25519");
|
||||||
|
write_ssh_string(&mut signature_blob, &signature);
|
||||||
|
let mut response = Vec::new();
|
||||||
|
response.push(SSH2_AGENT_SIGN_RESPONSE);
|
||||||
|
write_ssh_string(&mut response, &signature_blob);
|
||||||
|
write_agent_packet(&mut stream, &response).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_client_hello() -> NativeClientHello {
|
||||||
|
NativeClientHello {
|
||||||
|
protocol_version: crate::native::NATIVE_PROTOCOL_VERSION,
|
||||||
|
client_random: [1u8; 32],
|
||||||
|
client_ephemeral_public: [2u8; 32],
|
||||||
|
requested_host: "palav".to_string(),
|
||||||
|
requested_user: "palav".to_string(),
|
||||||
|
requested_session: "term".to_string(),
|
||||||
|
requested_mode: "read-write".to_string(),
|
||||||
|
terminal_size: (80, 24),
|
||||||
|
supported_aead: vec!["chacha20poly1305".to_string()],
|
||||||
|
supported_user_key_algorithms: vec!["ssh-ed25519".to_string()],
|
||||||
|
cached_host_key_fingerprint: None,
|
||||||
|
attach_ticket_envelope: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_server_hello(signing: &SigningKey) -> NativeServerHello {
|
||||||
|
NativeServerHello {
|
||||||
|
protocol_version: crate::native::NATIVE_PROTOCOL_VERSION,
|
||||||
|
server_random: [4u8; 32],
|
||||||
|
server_ephemeral_public: [5u8; 32],
|
||||||
|
host_key: host_public_key(signing),
|
||||||
|
chosen_aead: "chacha20poly1305".to_string(),
|
||||||
|
server_key_epoch: 1,
|
||||||
|
auth_challenge: [6u8; 32],
|
||||||
|
rate_limit_remaining: Some(30),
|
||||||
|
host_signature: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+152
-1
@@ -1,17 +1,23 @@
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::io::{Read, Write};
|
||||||
use std::net::UdpSocket;
|
use std::net::UdpSocket;
|
||||||
|
use std::os::unix::net::{UnixListener, UnixStream};
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::{Child, Command, Stdio};
|
use std::process::{Child, Command, Stdio};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use base64::Engine;
|
||||||
|
use base64::engine::general_purpose::STANDARD;
|
||||||
use dosh::auth::{build_bootstrap, load_or_create_server_secret};
|
use dosh::auth::{build_bootstrap, load_or_create_server_secret};
|
||||||
use dosh::config::load_server_config;
|
use dosh::config::load_server_config;
|
||||||
use dosh::crypto;
|
use dosh::crypto;
|
||||||
use dosh::native::{host_public_key, load_or_create_host_key, trust_host};
|
use dosh::native::{host_public_key, load_or_create_host_key, ssh_ed25519_public_blob, trust_host};
|
||||||
use dosh::protocol::{
|
use dosh::protocol::{
|
||||||
self, AttachOk, AttachReject, BootstrapAttachRequest, CLIENT_TO_SERVER, Frame, Input,
|
self, AttachOk, AttachReject, BootstrapAttachRequest, CLIENT_TO_SERVER, Frame, Input,
|
||||||
PacketKind, Resize, ResumeRequest, SERVER_TO_CLIENT,
|
PacketKind, Resize, ResumeRequest, SERVER_TO_CLIENT,
|
||||||
};
|
};
|
||||||
|
use ed25519_dalek::{Signer, SigningKey, VerifyingKey};
|
||||||
|
|
||||||
fn free_udp_port() -> u16 {
|
fn free_udp_port() -> u16 {
|
||||||
let socket = UdpSocket::bind("127.0.0.1:0").unwrap();
|
let socket = UdpSocket::bind("127.0.0.1:0").unwrap();
|
||||||
@@ -113,6 +119,30 @@ fn native_attach_once(dir: &tempfile::TempDir, port: u16) -> std::process::Outpu
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn native_attach_once_with_agent(
|
||||||
|
dir: &tempfile::TempDir,
|
||||||
|
port: u16,
|
||||||
|
agent_sock: &Path,
|
||||||
|
) -> std::process::Output {
|
||||||
|
let client = env!("CARGO_BIN_EXE_dosh-client");
|
||||||
|
Command::new(client)
|
||||||
|
.arg("--auth")
|
||||||
|
.arg("native")
|
||||||
|
.arg("--attach-only")
|
||||||
|
.arg("-v")
|
||||||
|
.arg("--session")
|
||||||
|
.arg("default")
|
||||||
|
.arg("--dosh-host")
|
||||||
|
.arg("127.0.0.1")
|
||||||
|
.arg("--dosh-port")
|
||||||
|
.arg(port.to_string())
|
||||||
|
.arg("local")
|
||||||
|
.env("HOME", dir.path())
|
||||||
|
.env("SSH_AUTH_SOCK", agent_sock)
|
||||||
|
.output()
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
fn write_native_client_auth(dir: &tempfile::TempDir, config_path: &std::path::Path) {
|
fn write_native_client_auth(dir: &tempfile::TempDir, config_path: &std::path::Path) {
|
||||||
let ssh_dir = dir.path().join(".ssh");
|
let ssh_dir = dir.path().join(".ssh");
|
||||||
fs::create_dir_all(&ssh_dir).unwrap();
|
fs::create_dir_all(&ssh_dir).unwrap();
|
||||||
@@ -142,6 +172,109 @@ fn write_native_client_auth(dir: &tempfile::TempDir, config_path: &std::path::Pa
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn write_native_agent_auth(
|
||||||
|
dir: &tempfile::TempDir,
|
||||||
|
config_path: &std::path::Path,
|
||||||
|
signing: &SigningKey,
|
||||||
|
) {
|
||||||
|
let public = VerifyingKey::from(signing).to_bytes();
|
||||||
|
fs::write(
|
||||||
|
dir.path().join("authorized_keys"),
|
||||||
|
format!(
|
||||||
|
"ssh-ed25519 {} agent-key\n",
|
||||||
|
STANDARD.encode(ssh_ed25519_public_blob(&public))
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let config = load_server_config(Some(config_path.to_path_buf())).unwrap();
|
||||||
|
let host_signing = load_or_create_host_key(&config).unwrap();
|
||||||
|
let known_hosts = dir.path().join(".config/dosh/known_hosts");
|
||||||
|
trust_host(
|
||||||
|
&known_hosts,
|
||||||
|
"local",
|
||||||
|
&host_public_key(&host_signing),
|
||||||
|
"test",
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_fake_ssh_agent(dir: &tempfile::TempDir, signing: SigningKey) -> PathBuf {
|
||||||
|
let socket_path = dir.path().join("agent.sock");
|
||||||
|
let public = VerifyingKey::from(&signing).to_bytes();
|
||||||
|
let key_blob = ssh_ed25519_public_blob(&public);
|
||||||
|
let listener = UnixListener::bind(&socket_path).unwrap();
|
||||||
|
thread::spawn(move || fake_ssh_agent(listener, key_blob, signing));
|
||||||
|
socket_path
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fake_ssh_agent(listener: UnixListener, key_blob: Vec<u8>, signing: SigningKey) {
|
||||||
|
let (mut stream, _) = listener.accept().unwrap();
|
||||||
|
let request = read_agent_packet(&mut stream).unwrap();
|
||||||
|
assert_eq!(request, vec![11]);
|
||||||
|
let mut response = Vec::new();
|
||||||
|
response.push(12);
|
||||||
|
response.extend_from_slice(&1u32.to_be_bytes());
|
||||||
|
write_ssh_string(&mut response, &key_blob);
|
||||||
|
write_ssh_string(&mut response, b"integration-agent-key");
|
||||||
|
write_agent_packet(&mut stream, &response).unwrap();
|
||||||
|
|
||||||
|
let request = read_agent_packet(&mut stream).unwrap();
|
||||||
|
let mut cursor = request.as_slice();
|
||||||
|
assert_eq!(read_u8(&mut cursor), 13);
|
||||||
|
assert_eq!(read_ssh_string(&mut cursor), key_blob.as_slice());
|
||||||
|
let transcript = read_ssh_string(&mut cursor);
|
||||||
|
assert_eq!(read_u32(&mut cursor), 0);
|
||||||
|
assert!(cursor.is_empty());
|
||||||
|
let signature = signing.sign(transcript).to_bytes();
|
||||||
|
let mut signature_blob = Vec::new();
|
||||||
|
write_ssh_string(&mut signature_blob, b"ssh-ed25519");
|
||||||
|
write_ssh_string(&mut signature_blob, &signature);
|
||||||
|
let mut response = Vec::new();
|
||||||
|
response.push(14);
|
||||||
|
write_ssh_string(&mut response, &signature_blob);
|
||||||
|
write_agent_packet(&mut stream, &response).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_agent_packet(stream: &mut UnixStream) -> std::io::Result<Vec<u8>> {
|
||||||
|
let mut len = [0u8; 4];
|
||||||
|
stream.read_exact(&mut len)?;
|
||||||
|
let len = u32::from_be_bytes(len) as usize;
|
||||||
|
let mut payload = vec![0u8; len];
|
||||||
|
stream.read_exact(&mut payload)?;
|
||||||
|
Ok(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_agent_packet(stream: &mut UnixStream, payload: &[u8]) -> std::io::Result<()> {
|
||||||
|
stream.write_all(&(payload.len() as u32).to_be_bytes())?;
|
||||||
|
stream.write_all(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_u8(cursor: &mut &[u8]) -> u8 {
|
||||||
|
let value = cursor[0];
|
||||||
|
*cursor = &cursor[1..];
|
||||||
|
value
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_u32(cursor: &mut &[u8]) -> u32 {
|
||||||
|
let value = u32::from_be_bytes(cursor[..4].try_into().unwrap());
|
||||||
|
*cursor = &cursor[4..];
|
||||||
|
value
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ssh_string<'a>(cursor: &mut &'a [u8]) -> &'a [u8] {
|
||||||
|
let len = read_u32(cursor) as usize;
|
||||||
|
let (value, rest) = cursor.split_at(len);
|
||||||
|
*cursor = rest;
|
||||||
|
value
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_ssh_string(out: &mut Vec<u8>, value: &[u8]) {
|
||||||
|
out.extend_from_slice(&(value.len() as u32).to_be_bytes());
|
||||||
|
out.extend_from_slice(value);
|
||||||
|
}
|
||||||
|
|
||||||
fn direct_attach(
|
fn direct_attach(
|
||||||
config: &std::path::Path,
|
config: &std::path::Path,
|
||||||
port: u16,
|
port: u16,
|
||||||
@@ -344,6 +477,24 @@ fn native_attach_only_smoke() {
|
|||||||
assert!(stderr.contains("terminal_ready"), "stderr={stderr}");
|
assert!(stderr.contains("terminal_ready"), "stderr={stderr}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn native_attach_only_smoke_uses_ssh_agent() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let port = free_udp_port();
|
||||||
|
let config = write_server_config(&dir, port);
|
||||||
|
let signing = SigningKey::from_bytes(&[53u8; 32]);
|
||||||
|
write_native_agent_auth(&dir, &config, &signing);
|
||||||
|
let agent_sock = start_fake_ssh_agent(&dir, signing);
|
||||||
|
let mut server = start_server(&dir, &config);
|
||||||
|
let output = native_attach_once_with_agent(&dir, port, &agent_sock);
|
||||||
|
let _ = server.kill();
|
||||||
|
let _ = server.wait();
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
assert!(output.status.success(), "stderr={stderr}");
|
||||||
|
assert!(stderr.contains("native_auth"), "stderr={stderr}");
|
||||||
|
assert!(stderr.contains("terminal_ready"), "stderr={stderr}");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ticket_attach_after_server_restart_smoke() {
|
fn ticket_attach_after_server_restart_smoke() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user