use std::{collections::HashMap, pin::Pin, str};
use anyhow::Context;
use bytes::{Buf, BufMut, Bytes, BytesMut};
use openssl::{
hash::{DigestBytes, MessageDigest},
ssl::Ssl,
};
use tokio::{
io::{AsyncRead, AsyncReadExt, AsyncSeek, AsyncSeekExt, AsyncWrite, AsyncWriteExt},
net::{TcpStream, ToSocketAddrs},
};
use tokio_openssl::SslStream;
use tracing::instrument;
use crate::v1::client::Command;
use crate::v1::error::{ClientError, ConnectionError as Error};
pub const PROTOCOL_VERSION: u32 = 1;
pub(crate) const CHUNK_INNER_MASK: u32 = 1 << 31;
pub(crate) const MAX_CHUNK_SIZE: u32 = CHUNK_INNER_MASK - 1;
pub(crate) const MAX_READ_BUF: u64 = 1024 * 1024;
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) enum Chunk {
Unknown,
Inner(u32),
Outer(u32),
}
impl From<u32> for Chunk {
fn from(value: u32) -> Self {
if value & CHUNK_INNER_MASK == CHUNK_INNER_MASK {
let payload = Self::Inner(value - CHUNK_INNER_MASK);
tracing::debug!(?payload, "New inner payload detected");
payload
} else {
let payload = Self::Outer(value);
tracing::debug!(?payload, "New outer payload detected");
payload
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct Response {
pub status_code: u32,
pub fields: HashMap<String, Vec<u8>>,
}
struct HmacKeys {
header_key: [u8; 64],
payload_key: [u8; 64],
}
impl core::fmt::Debug for HmacKeys {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HmacKeys")
.field("header_key", &format!("{} bytes", self.header_key.len()))
.field("payload_key", &format!("{} bytes", self.payload_key.len()))
.finish()
}
}
impl HmacKeys {
fn new() -> Result<Self, Error> {
let mut header_key = [0; 64];
openssl::rand::rand_priv_bytes(&mut header_key)?;
let mut payload_key = [0; 64];
openssl::rand::rand_priv_bytes(&mut payload_key)?;
Ok(Self {
header_key,
payload_key,
})
}
fn validate_header(&self, data: &[u8], signature: &[u8]) -> Result<(), ClientError> {
let key = openssl::pkey::PKey::hmac(&self.header_key)?;
let mut signer = openssl::sign::Signer::new(MessageDigest::sha512(), &key)?;
signer.update(data)?;
let hmac = signer.sign_to_vec()?;
if openssl::memcmp::eq(&hmac, signature) {
tracing::debug!("HMAC signature validated on response headers");
Ok(())
} else {
tracing::error!("HMAC signature on response headers failed!");
Err(ClientError::InvalidSignature)
}
}
fn payload_signer(&self) -> Result<openssl::sign::Signer<'_>, Error> {
let key = openssl::pkey::PKey::hmac(&self.payload_key)?;
Ok(openssl::sign::Signer::new(MessageDigest::sha512(), &key)?)
}
fn validate_payload_signer(
signer: openssl::sign::Signer<'_>,
signature: &[u8],
) -> Result<(), ClientError> {
let hmac = signer.sign_to_vec()?;
if openssl::memcmp::eq(&hmac, signature) {
tracing::debug!("HMAC signature validated on response payload");
Ok(())
} else {
tracing::error!("HMAC signature on response payload failed!");
Err(ClientError::InvalidSignature)
}
}
}
mod state {
#[derive(Debug)]
pub(crate) struct New;
#[derive(Debug)]
pub(crate) struct InnerFinished;
}
pub(crate) struct Connection<State = state::New> {
stream: SslStream<TcpStream>,
response_signing_keys: HmacKeys,
state: std::marker::PhantomData<State>,
}
pub(crate) struct InnerConnection {
stream: SslStream<TcpStream>,
outer_header_hash: DigestBytes,
payload_hash: DigestBytes,
response_signing_keys: HmacKeys,
}
impl Connection<state::New> {
#[instrument(err, skip_all)]
pub async fn connect<A: ToSocketAddrs + std::fmt::Debug>(
addr: A,
ssl: Ssl,
) -> Result<Self, Error> {
let response_signing_keys = HmacKeys::new()?;
let stream = TcpStream::connect(&addr).await?;
tracing::info!(?addr, "TCP connection to the Sigul bridge established");
let mut stream = tokio_openssl::SslStream::new(ssl, stream)?;
Pin::new(&mut stream).connect().await?;
tracing::info!("TLS session with the Sigul bridge established");
Ok(Connection {
stream,
response_signing_keys,
state: std::marker::PhantomData,
})
}
}
impl Connection<state::New> {
#[instrument(err, skip_all)]
pub async fn outer_request<P: AsyncRead + AsyncSeek + Unpin>(
mut self,
command: Command,
mut payload: Option<P>,
) -> Result<InnerConnection, ClientError> {
let operation_bytes = crate::serdes::to_bytes(&command)?;
let mut outer_header_hash = openssl::hash::Hasher::new(MessageDigest::sha512())?;
outer_header_hash.update(&PROTOCOL_VERSION.to_be_bytes())?;
outer_header_hash.update(&operation_bytes)?;
let outer_header_hash = outer_header_hash.finish()?;
let header_length =
std::mem::size_of::<u32>() + operation_bytes.len() + std::mem::size_of::<u64>();
self.stream
.write_u32(
header_length
.try_into()
.context("headers must be less than u32::MAX bytes")?,
)
.await?;
tracing::trace!(header_length, "Sent initial chunk size");
self.stream.write_u32(PROTOCOL_VERSION).await?;
tracing::trace!(version = PROTOCOL_VERSION, "Sent protocol version header");
self.stream.write_all(operation_bytes.as_slice()).await?;
tracing::trace!(
operation_header_length = operation_bytes.len(),
"Sent command fields"
);
let payload_length = if let Some(payload) = &mut payload {
let payload_length = payload.seek(std::io::SeekFrom::End(0)).await?;
payload.rewind().await?;
payload_length
} else {
0
};
self.stream
.write_all(payload_length.to_be_bytes().as_slice())
.await?;
tracing::trace!(payload_length, "Sent payload length");
let mut payload_hash = openssl::hash::Hasher::new(MessageDigest::sha512())?;
if let Some(mut payload) = payload {
let mut buf = BytesMut::with_capacity(MAX_READ_BUF.try_into().unwrap())
.limit(MAX_READ_BUF.try_into().unwrap());
let mut payload_bytes_sent = 0;
loop {
let bytes_read = payload.read_buf(&mut buf).await?;
if bytes_read == 0 {
tracing::trace!("Payload stream reached EOF");
break;
}
let mut unlimited_buf = buf.into_inner();
let chunk = unlimited_buf.split().freeze();
buf = unlimited_buf.limit(MAX_READ_BUF.try_into().unwrap());
tracing::trace!(
chunk_length = chunk.len(),
payload_bytes_sent,
"Sending payload chunk"
);
payload_hash.update(&chunk)?;
self.stream.write_u32(chunk.len() as u32).await?;
self.stream.write_all(&chunk).await?;
payload_bytes_sent += chunk.len();
tracing::trace!(
chunk_length = chunk.len(),
payload_bytes_sent,
"Finished sending payload chunk"
);
}
}
let payload_hash = payload_hash.finish()?;
self.stream.flush().await?;
tracing::info!(
header_length,
payload_length,
?command,
"Sent request to Sigul bridge"
);
Ok(InnerConnection {
stream: self.stream,
outer_header_hash,
payload_hash,
response_signing_keys: self.response_signing_keys,
})
}
}
impl InnerConnection {
pub async fn inner_request(
self,
ssl: Ssl,
mut request: HashMap<&str, &[u8]>,
) -> Result<Connection<state::InnerFinished>, ClientError> {
let header_hash = &*self.outer_header_hash;
let payload_hash = &*self.payload_hash;
request.insert("header-auth-sha512", header_hash);
request.insert("payload-auth-sha512", payload_hash);
request.insert("header-auth-key", &self.response_signing_keys.header_key);
request.insert("payload-auth-key", &self.response_signing_keys.payload_key);
let payload = crate::serdes::to_bytes(&request)?;
let mut nestls = crate::v1::nestls::Nestls::connect(self.stream, ssl).await?;
let stream = nestls.inner_mut();
stream.write_all(&payload).await?;
tracing::debug!(
payload_bytes = payload.len(),
"Sigul server payload sent via inner TLS session"
);
stream.flush().await?;
tracing::debug!("Inner TLS connection flushed");
let mut buf = vec![];
let response = stream.read_to_end(&mut buf).await?;
tracing::debug!(
response_size = response,
?buf,
"Inner TLS session end-of-stream reached"
);
assert!(buf.is_empty());
let outer_stream = nestls.into_outer().await?;
Ok(Connection {
stream: outer_stream,
response_signing_keys: self.response_signing_keys,
state: std::marker::PhantomData,
})
}
}
impl Connection<state::InnerFinished> {
const SIGNATURE_LENGTH: usize = 64;
#[instrument(err, skip_all, level = "debug")]
pub(crate) async fn response<P: AsyncWrite + AsyncWriteExt + Unpin>(
mut self,
mut payload: P,
) -> Result<Response, ClientError> {
let chunk_size = self.stream.read_u32().await?;
assert_eq!(Chunk::from(chunk_size), Chunk::Outer(chunk_size));
tracing::trace!(chunk_size=?Chunk::from(chunk_size), "Response chunk received");
let mut read_buffer = vec![
0_u8;
chunk_size
.try_into()
.context("header chunk exceeded platform usize")?
];
self.stream.read_exact(&mut read_buffer).await?;
tracing::trace!("Response headers received");
let mut response_headers = Bytes::from(read_buffer);
let response_headers_signature = response_headers.split_off(
response_headers
.len()
.checked_sub(Self::SIGNATURE_LENGTH)
.ok_or_else(|| {
anyhow::anyhow!("Response headers weren't long enough to include a signature")
})?,
);
debug_assert_eq!(response_headers_signature.len(), Self::SIGNATURE_LENGTH);
self.response_signing_keys
.validate_header(&response_headers, &response_headers_signature)?;
let status_code = response_headers.get_u32();
tracing::info!(?status_code, "Sigul server returned status code");
let fields = crate::serdes::from_bytes(&response_headers)?;
let chunk_size = self.stream.read_u32().await?;
debug_assert_eq!(chunk_size as usize, std::mem::size_of::<u64>());
let payload_length = self.stream.read_u64().await?;
tracing::debug!(payload_length, "Sigul server is sending payload");
let mut payload_hmac = self.response_signing_keys.payload_signer()?;
if payload_length > 0 {
let mut current_chunk = self.stream.read_u32().await?;
let mut total_read = 0_u64;
let mut read_buf = vec![];
let mut stream = self.stream.take(MAX_READ_BUF.min(current_chunk.into()));
loop {
let bytes_read: u32 = stream
.read_to_end(&mut read_buf)
.await?
.try_into()
.context("read more than u32::MAX bytes")?;
tracing::debug!(current_chunk, bytes_read, "Read payload chunk");
payload_hmac.update(&read_buf)?;
payload.write_all(&read_buf).await?;
read_buf.clear();
total_read = total_read
.checked_add(bytes_read.into())
.context("payload size overflowed a u64")?;
current_chunk = current_chunk
.checked_sub(bytes_read)
.context("read across a chunk boundry")?;
if total_read == payload_length {
tracing::info!(payload_length, "Sigul server payload received");
break;
}
if current_chunk == 0 {
stream.set_limit(4);
current_chunk = stream.read_u32().await?;
stream.set_limit(MAX_READ_BUF.min(current_chunk.into()));
tracing::debug!(current_chunk, "Awaiting next payload chunk");
}
}
self.stream = stream.into_inner();
}
payload.shutdown().await?;
let sig_chunk = self.stream.read_u32().await? as usize;
assert_eq!(sig_chunk, 64);
let mut payload_signature = vec![0_u8; sig_chunk];
self.stream.read_exact(&mut payload_signature).await?;
tracing::trace!("Read payload signature");
HmacKeys::validate_payload_signer(payload_hmac, &payload_signature)?;
if self.stream.read_u32().await? != 0 {
panic!("Bug: response framing is incorrect!");
}
if status_code == 0 {
Ok(Response {
status_code,
fields,
})
} else {
Err(crate::v1::error::Sigul::from(status_code).into())
}
}
}