1use std::path::{Path, PathBuf};
4
5use a3s_box_core::error::{BoxError, Result};
6use tokio::io::{AsyncReadExt, AsyncWriteExt};
7use tokio::net::UnixStream;
8
9use crate::tee::attestation::{AttestationReport, AttestationRequest};
10
11#[derive(Debug)]
16pub struct AttestationClient {
17 socket_path: PathBuf,
18}
19
20impl AttestationClient {
21 pub async fn connect(socket_path: &Path) -> Result<Self> {
23 let _stream = UnixStream::connect(socket_path).await.map_err(|e| {
24 BoxError::AttestationError(format!(
25 "Failed to connect to agent at {}: {}",
26 socket_path.display(),
27 e,
28 ))
29 })?;
30
31 Ok(Self {
32 socket_path: socket_path.to_path_buf(),
33 })
34 }
35
36 pub fn socket_path(&self) -> &Path {
38 &self.socket_path
39 }
40
41 pub async fn get_report(&self, request: &AttestationRequest) -> Result<AttestationReport> {
54 let body = serde_json::to_string(request).map_err(|e| {
55 BoxError::AttestationError(format!("Failed to serialize attestation request: {}", e))
56 })?;
57
58 let http_request = format!(
59 "POST /attest HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
60 body.len(),
61 body,
62 );
63
64 let mut stream = UnixStream::connect(&self.socket_path).await.map_err(|e| {
65 BoxError::AttestationError(format!(
66 "Attestation connection failed to {}: {}",
67 self.socket_path.display(),
68 e,
69 ))
70 })?;
71
72 stream
73 .write_all(http_request.as_bytes())
74 .await
75 .map_err(|e| {
76 BoxError::AttestationError(format!("Attestation request write failed: {}", e))
77 })?;
78
79 let mut response = Vec::with_capacity(8192);
81 let mut buf = vec![0u8; 8192];
82 loop {
83 let n = stream.read(&mut buf).await.map_err(|e| {
84 BoxError::AttestationError(format!("Attestation response read failed: {}", e))
85 })?;
86 if n == 0 {
87 break;
88 }
89 response.extend_from_slice(&buf[..n]);
90 if response.len() > 1024 * 1024 {
92 break;
93 }
94 }
95
96 let response_str = String::from_utf8_lossy(&response);
97
98 let body_str = response_str
100 .find("\r\n\r\n")
101 .map(|pos| &response_str[pos + 4..])
102 .ok_or_else(|| {
103 BoxError::AttestationError(
104 "Malformed attestation response: no HTTP body".to_string(),
105 )
106 })?;
107
108 if !response_str.starts_with("HTTP/1.1 200") && !response_str.starts_with("HTTP/1.0 200") {
110 return Err(BoxError::AttestationError(format!(
111 "Attestation request failed: {}",
112 body_str.chars().take(200).collect::<String>(),
113 )));
114 }
115
116 let report: AttestationReport = serde_json::from_str(body_str).map_err(|e| {
117 BoxError::AttestationError(format!("Failed to parse attestation response: {}", e))
118 })?;
119
120 Ok(report)
121 }
122}
123
124async fn connect_ratls(
129 socket_path: &Path,
130 policy: crate::tee::AttestationPolicy,
131 allow_simulated: bool,
132) -> Result<tokio_rustls::client::TlsStream<UnixStream>> {
133 let client_config = crate::tee::ratls::create_client_config(policy, allow_simulated)?;
134 let connector = tokio_rustls::TlsConnector::from(std::sync::Arc::new(client_config));
135
136 let stream = UnixStream::connect(socket_path).await.map_err(|e| {
137 BoxError::AttestationError(format!(
138 "Failed to connect to RA-TLS server at {}: {}",
139 socket_path.display(),
140 e,
141 ))
142 })?;
143
144 let server_name = rustls::pki_types::ServerName::try_from("localhost")
145 .map_err(|e| BoxError::AttestationError(format!("Invalid server name: {}", e)))?;
146
147 connector
148 .connect(server_name, stream)
149 .await
150 .map_err(|e| BoxError::AttestationError(format!("RA-TLS handshake failed: {}", e)))
151}
152
153#[derive(Debug)]
162pub struct RaTlsAttestationClient {
163 socket_path: PathBuf,
164}
165
166impl RaTlsAttestationClient {
167 pub fn new(socket_path: &Path) -> Self {
169 Self {
170 socket_path: socket_path.to_path_buf(),
171 }
172 }
173
174 pub fn socket_path(&self) -> &Path {
176 &self.socket_path
177 }
178
179 pub async fn verify(
189 &self,
190 policy: crate::tee::AttestationPolicy,
191 allow_simulated: bool,
192 ) -> Result<crate::tee::VerificationResult> {
193 use a3s_box_core::tee::{AttestRequest, AttestRoute};
194
195 let mut tls_stream = connect_ratls(&self.socket_path, policy, allow_simulated).await?;
196
197 let req = AttestRequest {
199 route: AttestRoute::Status,
200 payload: serde_json::Value::Null,
201 };
202 let payload = serde_json::to_vec(&req).map_err(|e| {
203 BoxError::AttestationError(format!("Failed to serialize status request: {}", e))
204 })?;
205 write_tls_frame(&mut tls_stream, 0x01, &payload).await?;
206
207 let _response = read_tls_frame(&mut tls_stream).await?;
209
210 let (_, tls_conn) = tls_stream.get_ref();
212 let peer_certs = tls_conn.peer_certificates();
213
214 if let Some(certs) = peer_certs {
215 if let Some(cert) = certs.first() {
216 let report = crate::tee::ratls::extract_report_from_cert(cert.as_ref())?;
217 let nonce = if report.report.len() >= 0x90 {
218 &report.report[0x50..0x90]
219 } else {
220 &[]
221 };
222 return crate::tee::verify_attestation(
223 &report,
224 nonce,
225 &crate::tee::AttestationPolicy::default(),
226 allow_simulated,
227 );
228 }
229 }
230
231 Ok(crate::tee::VerificationResult {
234 verified: true,
235 platform: crate::tee::PlatformInfo::default(),
236 policy_result: crate::tee::PolicyResult {
237 passed: true,
238 violations: vec![],
239 },
240 signature_valid: true,
241 cert_chain_valid: true,
242 nonce_valid: true,
243 report_age_valid: true,
244 failures: vec![],
245 })
246 }
247
248 pub async fn fetch_report(&self, allow_simulated: bool) -> Result<AttestationReport> {
255 use a3s_box_core::tee::{AttestRequest, AttestRoute};
256
257 let mut tls_stream = connect_ratls(
258 &self.socket_path,
259 crate::tee::AttestationPolicy::default(),
260 allow_simulated,
261 )
262 .await?;
263
264 let req = AttestRequest {
267 route: AttestRoute::Status,
268 payload: serde_json::Value::Null,
269 };
270 let payload = serde_json::to_vec(&req).map_err(|e| {
271 BoxError::AttestationError(format!("Failed to serialize status request: {}", e))
272 })?;
273 write_tls_frame(&mut tls_stream, 0x01, &payload).await?;
274 let _ = read_tls_frame(&mut tls_stream).await?;
275
276 let (_, tls_conn) = tls_stream.get_ref();
277 let cert = tls_conn
278 .peer_certificates()
279 .and_then(|certs| certs.first())
280 .ok_or_else(|| {
281 BoxError::AttestationError(
282 "RA-TLS server presented no certificate to extract a report from".to_string(),
283 )
284 })?;
285 crate::tee::ratls::extract_report_from_cert(cert.as_ref())
286 }
287}
288
289#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
291pub struct SecretEntry {
292 pub name: String,
294 pub value: String,
296 #[serde(default = "default_true")]
298 pub set_env: bool,
299}
300
301fn default_true() -> bool {
302 true
303}
304
305#[derive(Debug, Clone, serde::Deserialize)]
307pub struct SecretInjectionResult {
308 pub injected: usize,
310 #[serde(default)]
312 pub errors: Vec<String>,
313}
314
315#[derive(Debug)]
322pub struct SecretInjector {
323 socket_path: PathBuf,
324}
325
326impl SecretInjector {
327 pub fn new(socket_path: &Path) -> Self {
329 Self {
330 socket_path: socket_path.to_path_buf(),
331 }
332 }
333
334 pub async fn inject(
346 &self,
347 secrets: &[SecretEntry],
348 policy: crate::tee::AttestationPolicy,
349 allow_simulated: bool,
350 ) -> Result<SecretInjectionResult> {
351 use a3s_box_core::tee::{AttestRequest, AttestRoute};
352
353 if secrets.is_empty() {
354 return Ok(SecretInjectionResult {
355 injected: 0,
356 errors: vec![],
357 });
358 }
359
360 let mut tls_stream = connect_ratls(&self.socket_path, policy, allow_simulated).await?;
362
363 let req = AttestRequest {
365 route: AttestRoute::Secrets,
366 payload: serde_json::json!({ "secrets": secrets }),
367 };
368 let payload = serde_json::to_vec(&req).map_err(|e| {
369 BoxError::AttestationError(format!("Failed to serialize secrets request: {}", e))
370 })?;
371 write_tls_frame(&mut tls_stream, 0x01, &payload).await?;
372
373 let (frame_type, response_data) = read_tls_frame(&mut tls_stream).await?;
375
376 if frame_type == 0x04 {
377 let msg = String::from_utf8_lossy(&response_data);
378 return Err(BoxError::AttestationError(format!(
379 "Secret injection failed: {}",
380 msg,
381 )));
382 }
383
384 let result: SecretInjectionResult =
385 serde_json::from_slice(&response_data).map_err(|e| {
386 BoxError::AttestationError(format!("Failed to parse injection response: {}", e))
387 })?;
388
389 Ok(result)
390 }
391}
392
393#[derive(Debug, Clone, serde::Deserialize)]
395pub struct SealResult {
396 pub blob: String,
398 pub policy: String,
400 pub context: String,
402}
403
404#[derive(Debug, Clone, serde::Deserialize)]
406pub struct UnsealResult {
407 pub data: String,
409}
410
411#[derive(Debug)]
418pub struct SealClient {
419 socket_path: PathBuf,
420}
421
422impl SealClient {
423 pub fn new(socket_path: &Path) -> Self {
425 Self {
426 socket_path: socket_path.to_path_buf(),
427 }
428 }
429
430 pub async fn seal(
444 &self,
445 data: &[u8],
446 context: &str,
447 policy: &str,
448 attestation_policy: crate::tee::AttestationPolicy,
449 allow_simulated: bool,
450 ) -> Result<SealResult> {
451 use a3s_box_core::tee::{AttestRequest, AttestRoute};
452 use base64::Engine;
453
454 let mut tls_stream =
455 connect_ratls(&self.socket_path, attestation_policy, allow_simulated).await?;
456
457 let req = AttestRequest {
458 route: AttestRoute::Seal,
459 payload: serde_json::json!({
460 "data": base64::engine::general_purpose::STANDARD.encode(data),
461 "context": context,
462 "policy": policy,
463 }),
464 };
465 let payload = serde_json::to_vec(&req).map_err(|e| {
466 BoxError::AttestationError(format!("Failed to serialize seal request: {}", e))
467 })?;
468 write_tls_frame(&mut tls_stream, 0x01, &payload).await?;
469
470 let (frame_type, response_data) = read_tls_frame(&mut tls_stream).await?;
471
472 if frame_type == 0x04 {
473 let msg = String::from_utf8_lossy(&response_data);
474 return Err(BoxError::AttestationError(format!(
475 "Seal request failed: {}",
476 msg,
477 )));
478 }
479
480 let result: SealResult = serde_json::from_slice(&response_data).map_err(|e| {
481 BoxError::AttestationError(format!("Failed to parse seal response: {}", e))
482 })?;
483
484 Ok(result)
485 }
486
487 pub async fn unseal(
501 &self,
502 blob: &str,
503 context: &str,
504 policy: &str,
505 attestation_policy: crate::tee::AttestationPolicy,
506 allow_simulated: bool,
507 ) -> Result<Vec<u8>> {
508 use a3s_box_core::tee::{AttestRequest, AttestRoute};
509 use base64::Engine;
510
511 let mut tls_stream =
512 connect_ratls(&self.socket_path, attestation_policy, allow_simulated).await?;
513
514 let req = AttestRequest {
515 route: AttestRoute::Unseal,
516 payload: serde_json::json!({
517 "blob": blob,
518 "context": context,
519 "policy": policy,
520 }),
521 };
522 let payload = serde_json::to_vec(&req).map_err(|e| {
523 BoxError::AttestationError(format!("Failed to serialize unseal request: {}", e))
524 })?;
525 write_tls_frame(&mut tls_stream, 0x01, &payload).await?;
526
527 let (frame_type, response_data) = read_tls_frame(&mut tls_stream).await?;
528
529 if frame_type == 0x04 {
530 let msg = String::from_utf8_lossy(&response_data);
531 return Err(BoxError::AttestationError(format!(
532 "Unseal request failed: {}",
533 msg,
534 )));
535 }
536
537 let result: UnsealResult = serde_json::from_slice(&response_data).map_err(|e| {
538 BoxError::AttestationError(format!("Failed to parse unseal response: {}", e))
539 })?;
540
541 let plaintext = base64::engine::general_purpose::STANDARD
542 .decode(&result.data)
543 .map_err(|e| {
544 BoxError::AttestationError(format!("Failed to decode unsealed data: {}", e))
545 })?;
546
547 Ok(plaintext)
548 }
549}
550
551async fn write_tls_frame<S>(stream: &mut S, frame_type: u8, payload: &[u8]) -> Result<()>
558where
559 S: tokio::io::AsyncWriteExt + Unpin,
560{
561 let len = payload.len() as u32;
562 let mut header = [0u8; 5];
563 header[0] = frame_type;
564 header[1..5].copy_from_slice(&len.to_be_bytes());
565 stream
566 .write_all(&header)
567 .await
568 .map_err(|e| BoxError::AttestationError(format!("TLS frame header write failed: {}", e)))?;
569 if !payload.is_empty() {
570 stream.write_all(payload).await.map_err(|e| {
571 BoxError::AttestationError(format!("TLS frame payload write failed: {}", e))
572 })?;
573 }
574 Ok(())
575}
576
577async fn read_tls_frame<S>(stream: &mut S) -> Result<(u8, Vec<u8>)>
580where
581 S: tokio::io::AsyncReadExt + Unpin,
582{
583 let mut header = [0u8; 5];
584 match stream.read_exact(&mut header).await {
585 Ok(_) => {}
586 Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
587 tracing::debug!("RA-TLS peer closed without sending response frame");
588 return Ok((0x01, Vec::new()));
589 }
590 Err(e) => {
591 return Err(BoxError::AttestationError(format!(
592 "TLS frame header read failed: {}",
593 e
594 )));
595 }
596 }
597 let frame_type = header[0];
598 let len = u32::from_be_bytes([header[1], header[2], header[3], header[4]]) as usize;
599 let mut payload = vec![0u8; len];
600 if len > 0 {
601 stream.read_exact(&mut payload).await.map_err(|e| {
602 BoxError::AttestationError(format!("TLS frame payload read failed: {}", e))
603 })?;
604 }
605 Ok((frame_type, payload))
606}
607
608#[cfg(test)]
609mod tests {
610 use super::*;
611 use crate::tee::attestation::{CertificateChain, PlatformInfo};
612 use tokio::io::{AsyncReadExt, AsyncWriteExt};
613 use tokio::net::UnixListener;
614
615 fn bind_test_listener(path: &Path) -> Option<UnixListener> {
616 match UnixListener::bind(path) {
617 Ok(listener) => Some(listener),
618 Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
619 eprintln!(
620 "skipping Unix socket test; sandbox denied bind at {}: {}",
621 path.display(),
622 e
623 );
624 None
625 }
626 Err(e) => panic!("failed to bind test socket {}: {}", path.display(), e),
627 }
628 }
629
630 fn test_report() -> AttestationReport {
631 AttestationReport {
632 report: vec![1, 2, 3, 4],
633 cert_chain: CertificateChain::default(),
634 platform: PlatformInfo::default(),
635 }
636 }
637
638 async fn spawn_attestation_http_server(listener: UnixListener, response: Vec<u8>) -> Vec<u8> {
639 let (mut stream, _) = listener.accept().await.unwrap();
640 let mut request = Vec::new();
641 let mut buf = [0u8; 1024];
642 loop {
643 let n = stream.read(&mut buf).await.unwrap();
644 if n == 0 {
645 break;
646 }
647 request.extend_from_slice(&buf[..n]);
648 if request.windows(4).any(|w| w == b"\r\n\r\n") {
649 break;
650 }
651 }
652 stream.write_all(&response).await.unwrap();
653 request
654 }
655
656 #[tokio::test]
657 async fn test_attestation_connect_nonexistent_socket() {
658 let result =
659 AttestationClient::connect(Path::new("/tmp/nonexistent-a3s-attest-test.sock")).await;
660 assert!(result.is_err());
661 let err = result.unwrap_err();
662 assert!(matches!(err, BoxError::AttestationError(_)));
663 }
664
665 #[tokio::test]
666 async fn test_attestation_connect_and_socket_path() {
667 let tmp = tempfile::TempDir::new().unwrap();
668 let sock_path = tmp.path().join("attest.sock");
669 let Some(_listener) = bind_test_listener(&sock_path) else {
670 return;
671 };
672
673 let client = AttestationClient::connect(&sock_path).await.unwrap();
674 assert_eq!(client.socket_path(), sock_path);
675 }
676
677 #[tokio::test]
678 async fn test_attestation_get_report_parses_success_response() {
679 let tmp = tempfile::TempDir::new().unwrap();
680 let sock_path = tmp.path().join("attest_success.sock");
681 let Some(listener) = bind_test_listener(&sock_path) else {
682 return;
683 };
684
685 let body = serde_json::to_vec(&test_report()).unwrap();
686 let response = format!(
687 "HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n{}",
688 body.len(),
689 String::from_utf8(body).unwrap()
690 )
691 .into_bytes();
692 let server = tokio::spawn(spawn_attestation_http_server(listener, response));
693
694 let client = AttestationClient {
695 socket_path: sock_path,
696 };
697 let report = client
698 .get_report(&AttestationRequest {
699 nonce: vec![9, 8, 7],
700 user_data: None,
701 })
702 .await
703 .unwrap();
704
705 assert_eq!(report.report, vec![1, 2, 3, 4]);
706 let request = server.await.unwrap();
707 let request = String::from_utf8_lossy(&request);
708 assert!(request.starts_with("POST /attest HTTP/1.1\r\n"));
709 assert!(request.contains("Content-Type: application/json\r\n"));
710 assert!(request.contains("Content-Length:"));
711 }
712
713 #[tokio::test]
714 async fn test_attestation_get_report_surfaces_http_error_body() {
715 let tmp = tempfile::TempDir::new().unwrap();
716 let sock_path = tmp.path().join("attest_error.sock");
717 let Some(listener) = bind_test_listener(&sock_path) else {
718 return;
719 };
720
721 let response =
722 b"HTTP/1.1 500 Internal Server Error\r\nContent-Length: 12\r\n\r\nbad hardware"
723 .to_vec();
724 let server = tokio::spawn(spawn_attestation_http_server(listener, response));
725
726 let client = AttestationClient {
727 socket_path: sock_path,
728 };
729 let err = client
730 .get_report(&AttestationRequest {
731 nonce: vec![],
732 user_data: None,
733 })
734 .await
735 .unwrap_err();
736
737 assert!(matches!(err, BoxError::AttestationError(_)));
738 assert!(err.to_string().contains("bad hardware"));
739 let _ = server.await.unwrap();
740 }
741
742 #[tokio::test]
743 async fn test_attestation_get_report_rejects_malformed_and_invalid_json() {
744 for (name, response, expected) in [
745 (
746 "no_body",
747 b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n".to_vec(),
748 "no HTTP body",
749 ),
750 (
751 "bad_json",
752 b"HTTP/1.1 200 OK\r\nContent-Length: 8\r\n\r\nnot-json".to_vec(),
753 "Failed to parse attestation response",
754 ),
755 ] {
756 let tmp = tempfile::TempDir::new().unwrap();
757 let sock_path = tmp.path().join(format!("{name}.sock"));
758 let Some(listener) = bind_test_listener(&sock_path) else {
759 return;
760 };
761 let server = tokio::spawn(spawn_attestation_http_server(listener, response));
762
763 let client = AttestationClient {
764 socket_path: sock_path,
765 };
766 let err = client
767 .get_report(&AttestationRequest {
768 nonce: vec![],
769 user_data: None,
770 })
771 .await
772 .unwrap_err();
773
774 assert!(err.to_string().contains(expected), "{err}");
775 let _ = server.await.unwrap();
776 }
777 }
778
779 #[tokio::test]
780 async fn test_tls_frame_write_and_read_roundtrip() {
781 let (mut client, mut server) = tokio::io::duplex(64);
782
783 let writer = tokio::spawn(async move {
784 write_tls_frame(&mut client, 0x03, b"payload")
785 .await
786 .unwrap();
787 write_tls_frame(&mut client, 0x02, b"").await.unwrap();
788 });
789
790 let (frame_type, payload) = read_tls_frame(&mut server).await.unwrap();
791 assert_eq!(frame_type, 0x03);
792 assert_eq!(payload, b"payload");
793
794 let (frame_type, payload) = read_tls_frame(&mut server).await.unwrap();
795 assert_eq!(frame_type, 0x02);
796 assert!(payload.is_empty());
797
798 writer.await.unwrap();
799 }
800
801 #[tokio::test]
802 async fn test_tls_frame_read_unexpected_eof_is_empty_response() {
803 let (client, mut server) = tokio::io::duplex(8);
804 drop(client);
805
806 let (frame_type, payload) = read_tls_frame(&mut server).await.unwrap();
807 assert_eq!(frame_type, 0x01);
808 assert!(payload.is_empty());
809 }
810
811 #[tokio::test]
812 async fn test_tls_frame_read_truncated_payload_errors() {
813 let (mut client, mut server) = tokio::io::duplex(16);
814 client
815 .write_all(&[0x01, 0, 0, 0, 5, b'a', b'b'])
816 .await
817 .unwrap();
818 drop(client);
819
820 let err = read_tls_frame(&mut server).await.unwrap_err();
821 assert!(matches!(err, BoxError::AttestationError(_)));
822 assert!(err.to_string().contains("payload read failed"));
823 }
824
825 #[tokio::test]
826 async fn test_secret_injector_empty_secrets_returns_without_connecting() {
827 let injector = SecretInjector::new(Path::new("/tmp/nonexistent-empty-secrets.sock"));
828 let result = injector
829 .inject(&[], crate::tee::AttestationPolicy::default(), false)
830 .await
831 .unwrap();
832
833 assert_eq!(result.injected, 0);
834 assert!(result.errors.is_empty());
835 }
836
837 #[test]
838 fn test_attestation_client_constructors_and_serde_defaults() {
839 let path = Path::new("/tmp/a3s-attest.sock");
840 assert_eq!(RaTlsAttestationClient::new(path).socket_path(), path);
841 assert_eq!(SecretInjector::new(path).socket_path, path);
842 assert_eq!(SealClient::new(path).socket_path, path);
843
844 let secret: SecretEntry =
845 serde_json::from_str(r#"{"name":"TOKEN","value":"secret"}"#).unwrap();
846 assert!(secret.set_env);
847
848 let result: SecretInjectionResult = serde_json::from_str(r#"{"injected":2}"#).unwrap();
849 assert_eq!(result.injected, 2);
850 assert!(result.errors.is_empty());
851
852 let seal: SealResult =
853 serde_json::from_str(r#"{"blob":"abc","policy":"ChipOnly","context":"ctx"}"#).unwrap();
854 assert_eq!(seal.blob, "abc");
855 assert_eq!(seal.policy, "ChipOnly");
856 assert_eq!(seal.context, "ctx");
857
858 let unseal: UnsealResult = serde_json::from_str(r#"{"data":"c2VjcmV0"}"#).unwrap();
859 assert_eq!(unseal.data, "c2VjcmV0");
860 }
861}