use std::path::{Path, PathBuf};
use a3s_box_core::error::{BoxError, Result};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::UnixStream;
use crate::tee::attestation::{AttestationReport, AttestationRequest};
#[derive(Debug)]
pub struct AttestationClient {
socket_path: PathBuf,
}
impl AttestationClient {
pub async fn connect(socket_path: &Path) -> Result<Self> {
let _stream = UnixStream::connect(socket_path).await.map_err(|e| {
BoxError::AttestationError(format!(
"Failed to connect to agent at {}: {}",
socket_path.display(),
e,
))
})?;
Ok(Self {
socket_path: socket_path.to_path_buf(),
})
}
pub fn socket_path(&self) -> &Path {
&self.socket_path
}
pub async fn get_report(&self, request: &AttestationRequest) -> Result<AttestationReport> {
let body = serde_json::to_string(request).map_err(|e| {
BoxError::AttestationError(format!("Failed to serialize attestation request: {}", e))
})?;
let http_request = format!(
"POST /attest HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
body.len(),
body,
);
let mut stream = UnixStream::connect(&self.socket_path).await.map_err(|e| {
BoxError::AttestationError(format!(
"Attestation connection failed to {}: {}",
self.socket_path.display(),
e,
))
})?;
stream
.write_all(http_request.as_bytes())
.await
.map_err(|e| {
BoxError::AttestationError(format!("Attestation request write failed: {}", e))
})?;
let mut response = Vec::with_capacity(8192);
let mut buf = vec![0u8; 8192];
loop {
let n = stream.read(&mut buf).await.map_err(|e| {
BoxError::AttestationError(format!("Attestation response read failed: {}", e))
})?;
if n == 0 {
break;
}
response.extend_from_slice(&buf[..n]);
if response.len() > 1024 * 1024 {
break;
}
}
let response_str = String::from_utf8_lossy(&response);
let body_str = response_str
.find("\r\n\r\n")
.map(|pos| &response_str[pos + 4..])
.ok_or_else(|| {
BoxError::AttestationError(
"Malformed attestation response: no HTTP body".to_string(),
)
})?;
if !response_str.starts_with("HTTP/1.1 200") && !response_str.starts_with("HTTP/1.0 200") {
return Err(BoxError::AttestationError(format!(
"Attestation request failed: {}",
body_str.chars().take(200).collect::<String>(),
)));
}
let report: AttestationReport = serde_json::from_str(body_str).map_err(|e| {
BoxError::AttestationError(format!("Failed to parse attestation response: {}", e))
})?;
Ok(report)
}
}
async fn connect_ratls(
socket_path: &Path,
policy: crate::tee::AttestationPolicy,
allow_simulated: bool,
) -> Result<tokio_rustls::client::TlsStream<UnixStream>> {
let client_config = crate::tee::ratls::create_client_config(policy, allow_simulated)?;
let connector = tokio_rustls::TlsConnector::from(std::sync::Arc::new(client_config));
let stream = UnixStream::connect(socket_path).await.map_err(|e| {
BoxError::AttestationError(format!(
"Failed to connect to RA-TLS server at {}: {}",
socket_path.display(),
e,
))
})?;
let server_name = rustls::pki_types::ServerName::try_from("localhost")
.map_err(|e| BoxError::AttestationError(format!("Invalid server name: {}", e)))?;
connector
.connect(server_name, stream)
.await
.map_err(|e| BoxError::AttestationError(format!("RA-TLS handshake failed: {}", e)))
}
#[derive(Debug)]
pub struct RaTlsAttestationClient {
socket_path: PathBuf,
}
impl RaTlsAttestationClient {
pub fn new(socket_path: &Path) -> Self {
Self {
socket_path: socket_path.to_path_buf(),
}
}
pub fn socket_path(&self) -> &Path {
&self.socket_path
}
pub async fn verify(
&self,
policy: crate::tee::AttestationPolicy,
allow_simulated: bool,
) -> Result<crate::tee::VerificationResult> {
use a3s_box_core::tee::{AttestRequest, AttestRoute};
let mut tls_stream = connect_ratls(&self.socket_path, policy, allow_simulated).await?;
let req = AttestRequest {
route: AttestRoute::Status,
payload: serde_json::Value::Null,
};
let payload = serde_json::to_vec(&req).map_err(|e| {
BoxError::AttestationError(format!("Failed to serialize status request: {}", e))
})?;
write_tls_frame(&mut tls_stream, 0x01, &payload).await?;
let _response = read_tls_frame(&mut tls_stream).await?;
let (_, tls_conn) = tls_stream.get_ref();
let peer_certs = tls_conn.peer_certificates();
if let Some(certs) = peer_certs {
if let Some(cert) = certs.first() {
let report = crate::tee::ratls::extract_report_from_cert(cert.as_ref())?;
let nonce = if report.report.len() >= 0x90 {
&report.report[0x50..0x90]
} else {
&[]
};
return crate::tee::verify_attestation(
&report,
nonce,
&crate::tee::AttestationPolicy::default(),
allow_simulated,
);
}
}
Ok(crate::tee::VerificationResult {
verified: true,
platform: crate::tee::PlatformInfo::default(),
policy_result: crate::tee::PolicyResult {
passed: true,
violations: vec![],
},
signature_valid: true,
cert_chain_valid: true,
nonce_valid: true,
report_age_valid: true,
failures: vec![],
})
}
pub async fn fetch_report(&self, allow_simulated: bool) -> Result<AttestationReport> {
use a3s_box_core::tee::{AttestRequest, AttestRoute};
let mut tls_stream = connect_ratls(
&self.socket_path,
crate::tee::AttestationPolicy::default(),
allow_simulated,
)
.await?;
let req = AttestRequest {
route: AttestRoute::Status,
payload: serde_json::Value::Null,
};
let payload = serde_json::to_vec(&req).map_err(|e| {
BoxError::AttestationError(format!("Failed to serialize status request: {}", e))
})?;
write_tls_frame(&mut tls_stream, 0x01, &payload).await?;
let _ = read_tls_frame(&mut tls_stream).await?;
let (_, tls_conn) = tls_stream.get_ref();
let cert = tls_conn
.peer_certificates()
.and_then(|certs| certs.first())
.ok_or_else(|| {
BoxError::AttestationError(
"RA-TLS server presented no certificate to extract a report from".to_string(),
)
})?;
crate::tee::ratls::extract_report_from_cert(cert.as_ref())
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct SecretEntry {
pub name: String,
pub value: String,
#[serde(default = "default_true")]
pub set_env: bool,
}
fn default_true() -> bool {
true
}
#[derive(Debug, Clone, serde::Deserialize)]
pub struct SecretInjectionResult {
pub injected: usize,
#[serde(default)]
pub errors: Vec<String>,
}
#[derive(Debug)]
pub struct SecretInjector {
socket_path: PathBuf,
}
impl SecretInjector {
pub fn new(socket_path: &Path) -> Self {
Self {
socket_path: socket_path.to_path_buf(),
}
}
pub async fn inject(
&self,
secrets: &[SecretEntry],
policy: crate::tee::AttestationPolicy,
allow_simulated: bool,
) -> Result<SecretInjectionResult> {
use a3s_box_core::tee::{AttestRequest, AttestRoute};
if secrets.is_empty() {
return Ok(SecretInjectionResult {
injected: 0,
errors: vec![],
});
}
let mut tls_stream = connect_ratls(&self.socket_path, policy, allow_simulated).await?;
let req = AttestRequest {
route: AttestRoute::Secrets,
payload: serde_json::json!({ "secrets": secrets }),
};
let payload = serde_json::to_vec(&req).map_err(|e| {
BoxError::AttestationError(format!("Failed to serialize secrets request: {}", e))
})?;
write_tls_frame(&mut tls_stream, 0x01, &payload).await?;
let (frame_type, response_data) = read_tls_frame(&mut tls_stream).await?;
if frame_type == 0x04 {
let msg = String::from_utf8_lossy(&response_data);
return Err(BoxError::AttestationError(format!(
"Secret injection failed: {}",
msg,
)));
}
let result: SecretInjectionResult =
serde_json::from_slice(&response_data).map_err(|e| {
BoxError::AttestationError(format!("Failed to parse injection response: {}", e))
})?;
Ok(result)
}
}
#[derive(Debug, Clone, serde::Deserialize)]
pub struct SealResult {
pub blob: String,
pub policy: String,
pub context: String,
}
#[derive(Debug, Clone, serde::Deserialize)]
pub struct UnsealResult {
pub data: String,
}
#[derive(Debug)]
pub struct SealClient {
socket_path: PathBuf,
}
impl SealClient {
pub fn new(socket_path: &Path) -> Self {
Self {
socket_path: socket_path.to_path_buf(),
}
}
pub async fn seal(
&self,
data: &[u8],
context: &str,
policy: &str,
attestation_policy: crate::tee::AttestationPolicy,
allow_simulated: bool,
) -> Result<SealResult> {
use a3s_box_core::tee::{AttestRequest, AttestRoute};
use base64::Engine;
let mut tls_stream =
connect_ratls(&self.socket_path, attestation_policy, allow_simulated).await?;
let req = AttestRequest {
route: AttestRoute::Seal,
payload: serde_json::json!({
"data": base64::engine::general_purpose::STANDARD.encode(data),
"context": context,
"policy": policy,
}),
};
let payload = serde_json::to_vec(&req).map_err(|e| {
BoxError::AttestationError(format!("Failed to serialize seal request: {}", e))
})?;
write_tls_frame(&mut tls_stream, 0x01, &payload).await?;
let (frame_type, response_data) = read_tls_frame(&mut tls_stream).await?;
if frame_type == 0x04 {
let msg = String::from_utf8_lossy(&response_data);
return Err(BoxError::AttestationError(format!(
"Seal request failed: {}",
msg,
)));
}
let result: SealResult = serde_json::from_slice(&response_data).map_err(|e| {
BoxError::AttestationError(format!("Failed to parse seal response: {}", e))
})?;
Ok(result)
}
pub async fn unseal(
&self,
blob: &str,
context: &str,
policy: &str,
attestation_policy: crate::tee::AttestationPolicy,
allow_simulated: bool,
) -> Result<Vec<u8>> {
use a3s_box_core::tee::{AttestRequest, AttestRoute};
use base64::Engine;
let mut tls_stream =
connect_ratls(&self.socket_path, attestation_policy, allow_simulated).await?;
let req = AttestRequest {
route: AttestRoute::Unseal,
payload: serde_json::json!({
"blob": blob,
"context": context,
"policy": policy,
}),
};
let payload = serde_json::to_vec(&req).map_err(|e| {
BoxError::AttestationError(format!("Failed to serialize unseal request: {}", e))
})?;
write_tls_frame(&mut tls_stream, 0x01, &payload).await?;
let (frame_type, response_data) = read_tls_frame(&mut tls_stream).await?;
if frame_type == 0x04 {
let msg = String::from_utf8_lossy(&response_data);
return Err(BoxError::AttestationError(format!(
"Unseal request failed: {}",
msg,
)));
}
let result: UnsealResult = serde_json::from_slice(&response_data).map_err(|e| {
BoxError::AttestationError(format!("Failed to parse unseal response: {}", e))
})?;
let plaintext = base64::engine::general_purpose::STANDARD
.decode(&result.data)
.map_err(|e| {
BoxError::AttestationError(format!("Failed to decode unsealed data: {}", e))
})?;
Ok(plaintext)
}
}
async fn write_tls_frame<S>(stream: &mut S, frame_type: u8, payload: &[u8]) -> Result<()>
where
S: tokio::io::AsyncWriteExt + Unpin,
{
let len = payload.len() as u32;
let mut header = [0u8; 5];
header[0] = frame_type;
header[1..5].copy_from_slice(&len.to_be_bytes());
stream
.write_all(&header)
.await
.map_err(|e| BoxError::AttestationError(format!("TLS frame header write failed: {}", e)))?;
if !payload.is_empty() {
stream.write_all(payload).await.map_err(|e| {
BoxError::AttestationError(format!("TLS frame payload write failed: {}", e))
})?;
}
Ok(())
}
async fn read_tls_frame<S>(stream: &mut S) -> Result<(u8, Vec<u8>)>
where
S: tokio::io::AsyncReadExt + Unpin,
{
let mut header = [0u8; 5];
match stream.read_exact(&mut header).await {
Ok(_) => {}
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
tracing::debug!("RA-TLS peer closed without sending response frame");
return Ok((0x01, Vec::new()));
}
Err(e) => {
return Err(BoxError::AttestationError(format!(
"TLS frame header read failed: {}",
e
)));
}
}
let frame_type = header[0];
let len = u32::from_be_bytes([header[1], header[2], header[3], header[4]]) as usize;
let mut payload = vec![0u8; len];
if len > 0 {
stream.read_exact(&mut payload).await.map_err(|e| {
BoxError::AttestationError(format!("TLS frame payload read failed: {}", e))
})?;
}
Ok((frame_type, payload))
}
#[cfg(test)]
mod tests {
use super::*;
use tokio::net::UnixListener;
fn bind_test_listener(path: &Path) -> Option<UnixListener> {
match UnixListener::bind(path) {
Ok(listener) => Some(listener),
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
eprintln!(
"skipping Unix socket test; sandbox denied bind at {}: {}",
path.display(),
e
);
None
}
Err(e) => panic!("failed to bind test socket {}: {}", path.display(), e),
}
}
#[tokio::test]
async fn test_attestation_connect_nonexistent_socket() {
let result =
AttestationClient::connect(Path::new("/tmp/nonexistent-a3s-attest-test.sock")).await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, BoxError::AttestationError(_)));
}
#[tokio::test]
async fn test_attestation_connect_and_socket_path() {
let tmp = tempfile::TempDir::new().unwrap();
let sock_path = tmp.path().join("attest.sock");
let Some(_listener) = bind_test_listener(&sock_path) else {
return;
};
let client = AttestationClient::connect(&sock_path).await.unwrap();
assert_eq!(client.socket_path(), sock_path);
}
}