use std::sync::Arc;
use crate::message::{ClientMessage, ServerMessage, StreamHalf};
use tokio::sync::{mpsc, oneshot};
use tracing::info;
use url::Url;
use crate::error::Error;
use enclavia_protocol::attestation::{self, Pcrs};
use enclavia_protocol::chain::{ChainLinkJson, EnclaveChainRow, RecordedLink};
use uuid::Uuid;
use crate::http::{self, Method};
use crate::noise;
use crate::request::RequestBuilder;
use crate::stream::UpgradedStream;
use crate::transport::{self, OutboundCommand};
use crate::ws::{self, Ws};
const MAX_UPGRADE_HEAD: usize = 64 * 1024;
struct ClientInner {
cmd_tx: mpsc::Sender<OutboundCommand>,
host: String,
}
#[derive(Clone)]
pub struct Client {
inner: Arc<ClientInner>,
}
impl Client {
pub async fn connect(url: &str, pcrs: Pcrs) -> Result<Self, Error> {
ClientBuilder::new(url).pcrs(pcrs).build().await
}
pub fn builder(url: &str) -> ClientBuilder {
ClientBuilder::new(url)
}
pub fn get(&self, path: &str) -> RequestBuilder {
RequestBuilder::new(self.clone(), Method::Get, path.to_string())
}
pub fn post(&self, path: &str) -> RequestBuilder {
RequestBuilder::new(self.clone(), Method::Post, path.to_string())
}
pub fn put(&self, path: &str) -> RequestBuilder {
RequestBuilder::new(self.clone(), Method::Put, path.to_string())
}
pub fn delete(&self, path: &str) -> RequestBuilder {
RequestBuilder::new(self.clone(), Method::Delete, path.to_string())
}
pub fn patch(&self, path: &str) -> RequestBuilder {
RequestBuilder::new(self.clone(), Method::Patch, path.to_string())
}
pub fn request(&self, method: Method, path: &str) -> RequestBuilder {
RequestBuilder::new(self.clone(), method, path.to_string())
}
pub async fn upgrade(
&self,
method: Method,
path: &str,
headers: &[(String, String)],
) -> Result<UpgradedStream, Error> {
let mut hdrs: Vec<(String, String)> = headers.to_vec();
if !hdrs.iter().any(|(k, _)| k.eq_ignore_ascii_case("host")) {
hdrs.insert(0, ("Host".into(), self.host().to_string()));
}
let raw = http::serialize_request(method, path, &hdrs, None);
let mut stream = self.open_stream(raw).await?;
let id = stream.id();
let mut head_buf: Vec<u8> = Vec::new();
let (status, head_len) = loop {
match http::try_parse_response_head(&head_buf)? {
Some(pair) => break pair,
None => {
if head_buf.len() >= MAX_UPGRADE_HEAD {
let _ = self
.inner
.cmd_tx
.try_send(OutboundCommand::StreamClose { id, half: StreamHalf::Both });
return Err(Error::HttpParse(format!(
"response head exceeded {MAX_UPGRADE_HEAD} bytes before \\r\\n\\r\\n"
)));
}
match stream.recv_chunk().await {
Some(Ok(chunk)) => head_buf.extend_from_slice(&chunk),
Some(Err(e)) => return Err(e),
None => {
return Err(Error::ConnectionClosed);
}
}
}
}
};
if status != 101 {
let _ = self
.inner
.cmd_tx
.try_send(OutboundCommand::StreamClose { id, half: StreamHalf::Both });
head_buf.truncate(head_len);
return Err(Error::UpgradeFailed {
status,
head: head_buf,
});
}
if head_len < head_buf.len() {
stream.prepend_read(&head_buf[head_len..]);
}
Ok(stream)
}
pub async fn open_stream(&self, payload: Vec<u8>) -> Result<UpgradedStream, Error> {
let (id_tx, id_rx) = oneshot::channel();
let (stream_tx, stream_rx) = mpsc::channel::<Result<Vec<u8>, Error>>(64);
self.inner
.cmd_tx
.send(OutboundCommand::OpenStream {
payload,
id_tx,
stream_tx,
})
.await
.map_err(|_| Error::ConnectionClosed)?;
let id = id_rx.await.map_err(|_| Error::ConnectionClosed)??;
Ok(UpgradedStream::new(
id,
self.inner.cmd_tx.clone(),
stream_rx,
Vec::new(),
))
}
pub(crate) fn host(&self) -> &str {
&self.inner.host
}
pub(crate) async fn send_request(
&self,
payload: Vec<u8>,
response_tx: oneshot::Sender<Result<Vec<u8>, Error>>,
) -> Result<(), Error> {
self.inner
.cmd_tx
.send(OutboundCommand::Request { payload, response_tx })
.await
.map_err(|_| Error::ConnectionClosed)
}
}
#[derive(Clone)]
struct TrustUpgrades {
backend_url: String,
enclave_id: Uuid,
}
pub struct ClientBuilder {
url: String,
pcrs: Option<Pcrs>,
debug_mode: bool,
extra_headers: Vec<(String, String)>,
trust_upgrades: Option<TrustUpgrades>,
}
impl ClientBuilder {
fn new(url: &str) -> Self {
Self {
url: url.to_string(),
pcrs: None,
debug_mode: false,
extra_headers: Vec::new(),
trust_upgrades: None,
}
}
pub fn pcrs(mut self, pcrs: Pcrs) -> Self {
self.pcrs = Some(pcrs);
self
}
pub fn debug_mode(mut self, debug: bool) -> Self {
self.debug_mode = debug;
self
}
pub fn trust_upgrades(
mut self,
backend_url: impl Into<String>,
enclave_id: Uuid,
) -> Self {
self.trust_upgrades = Some(TrustUpgrades {
backend_url: backend_url.into(),
enclave_id,
});
self
}
pub fn header(
mut self,
name: impl Into<String>,
value: impl Into<String>,
) -> Self {
self.extra_headers.push((name.into(), value.into()));
self
}
pub async fn build(self) -> Result<Client, Error> {
let pcrs = self.pcrs.unwrap_or(Pcrs {
pcr0: Vec::new(),
pcr1: Vec::new(),
pcr2: Vec::new(),
});
let parsed_url =
Url::parse(&self.url).map_err(|e| Error::InvalidUrl(e.to_string()))?;
let host = parsed_url
.host_str()
.ok_or_else(|| Error::InvalidUrl("Missing host".into()))?
.to_string();
info!(url = %self.url, "Connecting to enclavia proxy");
let mut ws = Ws::connect(&self.url, &self.extra_headers).await?;
info!("WebSocket connected");
let (mut transport, handshake_hash) =
noise::perform_handshake(&mut ws).await?;
let attestation_response: ServerMessage =
transport::send_and_receive(
&mut ws,
&mut transport,
&ClientMessage::RequestAttestation,
)
.await?;
let attestation_data = match attestation_response {
ServerMessage::Attestation { data, .. } => data,
_ => return Err(Error::UnexpectedMessage),
};
match attestation::verify_against(
&attestation_data,
&handshake_hash,
&pcrs,
self.debug_mode,
) {
Ok(()) => info!("Attestation verified (pinned PCRs match)"),
Err(pinned_err) => match &self.trust_upgrades {
Some(tu) => {
if pcrs.pcr0.is_empty() && pcrs.pcr1.is_empty() && pcrs.pcr2.is_empty() {
return Err(Error::TrustUpgrades(
"trust_upgrades requires pinned PCRs (call .pcrs(..))".into(),
));
}
verify_via_upgrade_chain(
&attestation_data,
&handshake_hash,
&pcrs,
self.debug_mode,
tu,
)
.await?;
info!("Attestation verified (descends from pinned PCRs via upgrade chain)");
}
None => return Err(Error::Attestation(pinned_err.to_string())),
},
}
let (cmd_tx, cmd_rx) = mpsc::channel(64);
ws::spawn_transport(transport::run_transport(ws, transport, cmd_rx));
Ok(Client {
inner: Arc::new(ClientInner { cmd_tx, host }),
})
}
}
async fn verify_via_upgrade_chain(
attestation_data: &[u8],
handshake_hash: &[u8],
pinned: &Pcrs,
debug_mode: bool,
tu: &TrustUpgrades,
) -> Result<(), Error> {
let base = tu.backend_url.trim_end_matches('/');
let http = reqwest::Client::new();
let fetch_err = |what: &str, e: reqwest::Error| {
Error::TrustUpgrades(format!("fetching {what}: {e}"))
};
let row: EnclaveChainRow = http
.get(format!("{base}/enclaves/{}", tu.enclave_id))
.send()
.await
.and_then(reqwest::Response::error_for_status)
.map_err(|e| fetch_err("enclave row", e))?
.json()
.await
.map_err(|e| fetch_err("enclave row", e))?;
let wire: Vec<ChainLinkJson> = http
.get(format!("{base}/enclaves/{}/upgrade-chain", tu.enclave_id))
.send()
.await
.and_then(reqwest::Response::error_for_status)
.map_err(|e| fetch_err("upgrade chain", e))?
.json()
.await
.map_err(|e| fetch_err("upgrade chain", e))?;
let mut links: Vec<RecordedLink> = Vec::with_capacity(wire.len());
for w in &wire {
links.push(
w.into_recorded_link()
.map_err(|e| Error::TrustUpgrades(format!("decoding chain link: {e}")))?,
);
}
let tip = enclavia_protocol::chain::verify_pcr_descent(
pinned,
&links,
row.control_public_key.as_deref(),
&row.pcrs,
&row.image_digest,
row.upgradable,
chrono::Utc::now(),
debug_mode,
)
.map_err(|e| Error::TrustUpgrades(e.to_string()))?;
attestation::verify_against(attestation_data, handshake_hash, &tip, debug_mode).map_err(|e| {
Error::TrustUpgrades(format!(
"running enclave does not match the verified chain tip: {e}"
))
})?;
Ok(())
}