Files
sausage/userland/ssh-client/src/identity.rs
T

129 lines
4.7 KiB
Rust

//! Public SSH identities and signing through the host's standard HTTP API.
use crate::{Session, Stream, connect_tcp, ssh_wire::fail};
use base64::{Engine, engine::general_purpose::STANDARD};
use serde::Deserialize;
use serde_json::{Value, json};
use std::io;
use wasi::http::{outgoing_handler, types::*};
#[derive(Deserialize)]
pub struct Identity {
pub item: String,
pub public_key: String,
pub user: Option<String>,
}
fn post(route: &str, payload: Value) -> io::Result<Value> {
let base = std::env::var("SAUSAGE_HOST_API").unwrap_or_else(|_| "http://host.sausage".into());
let url = url::Url::parse(&format!("{}{route}", base.trim_end_matches('/')))
.map_err(|_| fail("invalid host API URL"))?;
let bytes = serde_json::to_vec(&payload).map_err(|e| fail(&e.to_string()))?;
let headers = Fields::new();
headers
.set("content-type", &[b"application/json".to_vec()])
.map_err(|_| fail("HTTP headers"))?;
headers
.set("content-length", &[bytes.len().to_string().into_bytes()])
.map_err(|_| fail("HTTP headers"))?;
let request = OutgoingRequest::new(headers);
request
.set_method(&Method::Post)
.map_err(|_| fail("HTTP method"))?;
request
.set_scheme(Some(&Scheme::Http))
.map_err(|_| fail("HTTP scheme"))?;
request
.set_authority(Some(
&url[url::Position::BeforeHost..url::Position::AfterPort],
))
.map_err(|_| fail("HTTP authority"))?;
request
.set_path_with_query(Some(url.path()))
.map_err(|_| fail("HTTP path"))?;
let body = request.body().map_err(|_| fail("HTTP body"))?;
let stream = body.write().map_err(|_| fail("HTTP stream"))?;
let response =
outgoing_handler::handle(request, None).map_err(|e| fail(&format!("host API: {e:?}")))?;
stream
.blocking_write_and_flush(&bytes)
.map_err(|_| fail("host API upload"))?;
drop(stream);
OutgoingBody::finish(body, None).map_err(|_| fail("host API upload finish"))?;
response.subscribe().block();
let response = response
.get()
.ok_or_else(|| fail("host API response missing"))?
.map_err(|_| fail("host API response consumed"))?
.map_err(|e| fail(&format!("host API: {e:?}")))?;
let status = response.status();
let body = response
.consume()
.map_err(|_| fail("host API response body"))?;
let stream = body
.stream()
.map_err(|_| fail("host API response stream"))?;
let mut bytes = Vec::new();
loop {
match stream.blocking_read(65536) {
Ok(data) if data.is_empty() => stream.subscribe().block(),
Ok(data) => bytes.extend(data),
Err(wasi::io::streams::StreamError::Closed) => break,
Err(_) => return Err(fail("host API response read")),
}
}
drop(stream);
let trailers = IncomingBody::finish(body);
trailers.subscribe().block();
if !matches!(trailers.get(), Some(Ok(Ok(_)))) {
return Err(fail("host API response finish"));
}
if status != 200 {
return Err(fail(&format!(
"host API HTTP {status}: {}",
String::from_utf8_lossy(&bytes)
)));
}
serde_json::from_slice(&bytes).map_err(|_| fail("invalid host API JSON"))
}
impl Identity {
pub fn load(host: &str) -> io::Result<Self> {
serde_json::from_value(post("/ssh/identity", json!({"host": host}))?)
.map_err(|_| fail("invalid SSH identity"))
}
pub fn sign(&self, host: &str, payload: Vec<u8>) -> io::Result<Vec<u8>> {
let result = post(
"/ssh/sign",
json!({"host":host,"item":self.item,"payload":STANDARD.encode(payload)}),
)?;
STANDARD
.decode(
result["signature"]
.as_str()
.ok_or_else(|| fail("missing signature"))?,
)
.map_err(|_| fail("invalid signature"))
}
}
pub fn connect(
host: &str,
port: u16,
user: Option<&str>,
accept_new: bool,
) -> io::Result<Session<Box<dyn Stream>>> {
let identity = Identity::load(host)?;
let user = user
.or(identity.user.as_deref())
.filter(|u| !u.is_empty())
.ok_or_else(|| fail("specify an SSH user"))?;
let public = ssh_key::PublicKey::from_bytes(
&STANDARD
.decode(&identity.public_key)
.map_err(|_| fail("invalid public key"))?,
)
.map_err(|_| fail("invalid public key"))?;
let mut session = Session::start(connect_tcp(host, port)?, host.into(), port, accept_new)?;
futures_lite::future::block_on(session.authenticate_with(user, &public, |payload| {
std::future::ready(identity.sign(host, payload))
}))?;
Ok(session)
}