use axum::{
body::Bytes,
extract::State,
http::{HeaderMap, StatusCode},
response::IntoResponse,
routing::post,
Router,
};
use sha2::{Digest, Sha256};
use std::net::SocketAddr;
use std::sync::Arc;
use tracing::{error, info};
use vex_core::segment::{
AuthorityData, Capsule, CryptoData, IdentityData, IntentData, SchemaValue, WitnessData,
};
use vex_core::VEP_MAGIC;
use vex_hardware::api::AgentIdentity;
#[derive(Clone)]
struct AppState {
identity: Arc<AgentIdentity>,
target_url: String,
}
#[tokio::main]
async fn main() {
tracing_subscriber::fmt::init();
let identity = Arc::new(AgentIdentity::new());
info!(
"VEX Sidecar initialized with Agent ID: {}",
identity.agent_id
);
let state = AppState {
identity,
target_url: std::env::var("TARGET_URL")
.unwrap_or_else(|_| "http://localhost:8080".to_string()),
};
let app = Router::new()
.route("/*path", post(proxy_handler))
.with_state(state);
let addr = SocketAddr::from(([0, 0, 0, 0], 3000));
info!("VEX Sidecar proxy listening on {}", addr);
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn proxy_handler(
State(state): State<AppState>,
headers: HeaderMap,
body_bytes: Bytes,
) -> impl IntoResponse {
let is_vep = body_bytes.starts_with(&VEP_MAGIC);
if is_vep {
info!("Request is already VEP enveloped. Forwarding to target...");
return forward_request(&state.target_url, headers, body_bytes).await;
}
info!(
"Simulating VEX-API pre-routing check for body of size {}...",
body_bytes.len()
);
info!("Encapsulating payload into VEP Binary format...");
let payload_hash = hex::encode(Sha256::digest(&body_bytes));
let intent = IntentData::Transparent {
request_sha256: payload_hash.clone(),
confidence: 1.0,
capabilities: vec!["proxy-forwarding".to_string()],
magpie_source: None,
continuation_token: None,
metadata: SchemaValue(serde_json::Value::Null),
};
let identity = IdentityData {
aid: state.identity.agent_id.clone(),
identity_type: "hardware-rooted".to_string(),
pcrs: None,
metadata: SchemaValue(serde_json::Value::Null),
};
let authority = AuthorityData {
capsule_id: format!("sidecar-{}", uuid::Uuid::new_v4()),
outcome: "ALLOW".to_string(),
reason_code: "PASSED_SIDECAR".to_string(),
trace_root: payload_hash.clone(),
nonce: "1".to_string(),
escalation_id: None,
binding_status: None,
continuation_token: None,
authority_class: Some("ALLOW_PATH".to_string()),
gate_sensors: SchemaValue(serde_json::Value::Null),
metadata: SchemaValue(serde_json::Value::Null),
};
let witness = WitnessData {
chora_node_id: "sidecar-local".to_string(),
receipt_hash: payload_hash.clone(), timestamp: chrono::Utc::now().timestamp() as u64,
metadata: SchemaValue(serde_json::Value::Null),
};
let intent_hash = intent.to_jcs_hash().unwrap().to_hex();
fn hash_seg<T: serde::Serialize>(seg: &T) -> String {
let jcs = serde_jcs::to_vec(seg).unwrap();
hex::encode(Sha256::digest(&jcs))
}
let authority_hash = hash_seg(&authority);
let identity_hash = hash_seg(&identity);
let witness_hash = witness.to_commitment_hash().unwrap();
let mut capsule = Capsule {
capsule_id: authority.capsule_id.clone(),
intent,
authority,
identity,
witness,
intent_hash,
authority_hash,
identity_hash,
witness_hash: witness_hash.to_hex(),
capsule_root: String::new(),
crypto: CryptoData {
algo: "ed25519".to_string(),
public_key_endpoint: "/public_key".to_string(),
signature_scope: "capsule_root".to_string(),
signature_b64: String::new(), },
request_commitment: None,
};
let root = capsule.to_composite_hash().unwrap();
capsule.capsule_root = root.to_hex();
let sig_bytes = state.identity.sign(&root.0);
capsule.crypto.signature_b64 =
base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &sig_bytes);
info!(
"Capsule hardened and signed. Root={}",
&capsule.capsule_root[..8]
);
let capsule_json = serde_json::to_vec(&capsule).unwrap();
forward_request(&state.target_url, headers, Bytes::from(capsule_json)).await
}
async fn forward_request(target_url: &str, headers: HeaderMap, body: Bytes) -> impl IntoResponse {
let client = reqwest::Client::new();
let mut req = client.post(target_url).body(body);
for (name, value) in headers.iter() {
if name != "host" {
req = req.header(name, value);
}
}
match req.send().await {
Ok(res) => {
let status = res.status();
let body = res.bytes().await.unwrap_or_default();
(status, body).into_response()
}
Err(e) => {
error!("Forwarding failed: {}", e);
(StatusCode::BAD_GATEWAY, "Target unreachable").into_response()
}
}
}