Skip to main content

a3s_box_runtime/grpc/
attestation.rs

1//! Attestation, secret injection, and seal/unseal clients over RA-TLS.
2
3use 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/// Client for requesting attestation reports from the guest VM.
12///
13/// Sends HTTP POST /attest requests over the Unix socket to the guest agent,
14/// which calls the SNP_GET_REPORT ioctl and returns the hardware-signed report.
15#[derive(Debug)]
16pub struct AttestationClient {
17    socket_path: PathBuf,
18}
19
20impl AttestationClient {
21    /// Connect to the guest agent for attestation requests.
22    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    /// Get the socket path this client is connected to.
37    pub fn socket_path(&self) -> &Path {
38        &self.socket_path
39    }
40
41    /// Request an attestation report from the guest VM.
42    ///
43    /// The guest agent receives the request, calls `SNP_GET_REPORT` via
44    /// `/dev/sev-guest`, and returns the hardware-signed report with
45    /// the certificate chain.
46    ///
47    /// # Arguments
48    /// * `request` - Attestation request containing the verifier's nonce
49    ///
50    /// # Returns
51    /// * `Ok(AttestationReport)` - Hardware-signed report with cert chain
52    /// * `Err(...)` - If the guest agent is unreachable or SNP is unavailable
53    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        // Read full response (report + certs can be several KB)
80        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            // Safety limit: 1 MiB (report + full cert chain)
91            if response.len() > 1024 * 1024 {
92                break;
93            }
94        }
95
96        let response_str = String::from_utf8_lossy(&response);
97
98        // Find the JSON body after the HTTP headers
99        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        // Check for HTTP error status
109        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
124/// Establish an RA-TLS connection to the guest attestation server.
125///
126/// Creates a TLS connector with the given attestation policy, connects to the
127/// Unix socket, and performs the TLS handshake (which verifies the TEE).
128async 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/// Client for verifying TEE attestation via RA-TLS handshake.
154///
155/// Connects to the guest's RA-TLS attestation server over Unix socket,
156/// performs a TLS handshake with a custom certificate verifier that
157/// extracts and verifies the SNP report from the server's certificate.
158///
159/// Attestation verification happens during the TLS handshake — if the
160/// handshake succeeds, the TEE is verified.
161#[derive(Debug)]
162pub struct RaTlsAttestationClient {
163    socket_path: PathBuf,
164}
165
166/// Report and exact peer certificate proven by one live RA-TLS session.
167#[derive(Debug, Clone)]
168pub(crate) struct RaTlsAttestationEvidence {
169    pub(crate) report: AttestationReport,
170    pub(crate) certificate_der: Vec<u8>,
171}
172
173impl RaTlsAttestationClient {
174    /// Create a new RA-TLS attestation client for the given socket path.
175    pub fn new(socket_path: &Path) -> Self {
176        Self {
177            socket_path: socket_path.to_path_buf(),
178        }
179    }
180
181    /// Get the socket path.
182    pub fn socket_path(&self) -> &Path {
183        &self.socket_path
184    }
185
186    /// Verify TEE attestation via RA-TLS handshake.
187    ///
188    /// Connects to the guest attestation server, performs a TLS handshake
189    /// with a custom verifier that checks the SNP report embedded in the
190    /// server's certificate, and returns the verification result.
191    ///
192    /// # Arguments
193    /// * `policy` - Attestation policy to verify against
194    /// * `allow_simulated` - Whether to accept simulated (non-hardware) reports
195    pub async fn verify(
196        &self,
197        policy: crate::tee::AttestationPolicy,
198        allow_simulated: bool,
199    ) -> Result<crate::tee::VerificationResult> {
200        use a3s_box_core::tee::{AttestRequest, AttestRoute};
201
202        let mut tls_stream = connect_ratls(&self.socket_path, policy, allow_simulated).await?;
203
204        // Send a Frame-based status request
205        let req = AttestRequest {
206            route: AttestRoute::Status,
207            payload: serde_json::Value::Null,
208        };
209        let payload = serde_json::to_vec(&req).map_err(|e| {
210            BoxError::AttestationError(format!("Failed to serialize status request: {}", e))
211        })?;
212        write_tls_frame(&mut tls_stream, 0x01, &payload).await?;
213
214        // Read response frame
215        let _response = read_tls_frame(&mut tls_stream).await?;
216
217        // Extract the peer certificate for detailed report info
218        let (_, tls_conn) = tls_stream.get_ref();
219        let peer_certs = tls_conn.peer_certificates();
220
221        if let Some(certs) = peer_certs {
222            if let Some(cert) = certs.first() {
223                let report = crate::tee::ratls::extract_report_from_cert(cert.as_ref())?;
224                let nonce = if report.report.len() >= 0x90 {
225                    &report.report[0x50..0x90]
226                } else {
227                    &[]
228                };
229                return crate::tee::verify_attestation(
230                    &report,
231                    nonce,
232                    &crate::tee::AttestationPolicy::default(),
233                    allow_simulated,
234                );
235            }
236        }
237
238        // If we got here, TLS handshake succeeded (verifier passed)
239        // but we couldn't extract the cert for detailed results
240        Ok(crate::tee::VerificationResult {
241            verified: true,
242            platform: crate::tee::PlatformInfo::default(),
243            policy_result: crate::tee::PolicyResult {
244                passed: true,
245                violations: vec![],
246            },
247            signature_valid: true,
248            cert_chain_valid: true,
249            nonce_valid: true,
250            report_age_valid: true,
251            failures: vec![],
252        })
253    }
254
255    /// Fetch the raw attestation report over RA-TLS, without applying a
256    /// verification policy.
257    ///
258    /// The guest attestation server speaks RA-TLS + framed messages (not plain
259    /// HTTP); the signed report is carried in the server's TLS certificate and
260    /// is extracted here after the handshake.
261    pub async fn fetch_report(&self, allow_simulated: bool) -> Result<AttestationReport> {
262        self.fetch_report_with_policy(crate::tee::AttestationPolicy::default(), allow_simulated)
263            .await
264    }
265
266    /// Fetch the signed report while enforcing the caller's RA-TLS policy.
267    pub async fn fetch_report_with_policy(
268        &self,
269        policy: crate::tee::AttestationPolicy,
270        allow_simulated: bool,
271    ) -> Result<AttestationReport> {
272        Ok(self
273            .fetch_evidence_with_policy(policy, allow_simulated)
274            .await?
275            .report)
276    }
277
278    /// Fetch the report together with the peer certificate whose private key
279    /// was proven during this live TLS handshake.
280    pub(crate) async fn fetch_evidence_with_policy(
281        &self,
282        policy: crate::tee::AttestationPolicy,
283        allow_simulated: bool,
284    ) -> Result<RaTlsAttestationEvidence> {
285        use a3s_box_core::tee::{AttestRequest, AttestRoute};
286
287        let mut tls_stream = connect_ratls(&self.socket_path, policy, allow_simulated).await?;
288
289        // Exchange a Status frame so the handshake (and report extraction)
290        // completes against a live server.
291        let req = AttestRequest {
292            route: AttestRoute::Status,
293            payload: serde_json::Value::Null,
294        };
295        let payload = serde_json::to_vec(&req).map_err(|e| {
296            BoxError::AttestationError(format!("Failed to serialize status request: {}", e))
297        })?;
298        write_tls_frame(&mut tls_stream, 0x01, &payload).await?;
299        let _ = read_tls_frame(&mut tls_stream).await?;
300
301        let (_, tls_conn) = tls_stream.get_ref();
302        let cert = tls_conn
303            .peer_certificates()
304            .and_then(|certs| certs.first())
305            .ok_or_else(|| {
306                BoxError::AttestationError(
307                    "RA-TLS server presented no certificate to extract a report from".to_string(),
308                )
309            })?;
310        let certificate_der = cert.as_ref().to_vec();
311        let report = crate::tee::ratls::extract_report_from_cert(&certificate_der)?;
312        Ok(RaTlsAttestationEvidence {
313            report,
314            certificate_der,
315        })
316    }
317}
318
319/// A secret to inject into the TEE.
320#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
321pub struct SecretEntry {
322    /// Secret name (used as filename in /run/secrets/ and env var name).
323    pub name: String,
324    /// Secret value.
325    pub value: String,
326    /// Whether to set as environment variable in the guest (default: true).
327    #[serde(default = "default_true")]
328    pub set_env: bool,
329}
330
331fn default_true() -> bool {
332    true
333}
334
335/// Response from the guest after secret injection.
336#[derive(Debug, Clone, serde::Deserialize)]
337pub struct SecretInjectionResult {
338    /// Number of secrets successfully injected.
339    pub injected: usize,
340    /// Any non-fatal errors encountered.
341    #[serde(default)]
342    pub errors: Vec<String>,
343}
344
345/// Client for injecting secrets into the TEE via RA-TLS.
346///
347/// Connects to the guest's RA-TLS attestation server, verifies the TEE
348/// during the TLS handshake, then sends secrets over the encrypted channel.
349/// The guest stores secrets in `/run/secrets/` (tmpfs) and optionally
350/// sets them as environment variables.
351#[derive(Debug)]
352pub struct SecretInjector {
353    socket_path: PathBuf,
354}
355
356impl SecretInjector {
357    /// Create a new secret injector for the given attestation socket.
358    pub fn new(socket_path: &Path) -> Self {
359        Self {
360            socket_path: socket_path.to_path_buf(),
361        }
362    }
363
364    /// Inject secrets into the TEE via RA-TLS.
365    ///
366    /// 1. Connects to the guest attestation server
367    /// 2. TLS handshake verifies the TEE (attestation in cert)
368    /// 3. Sends secrets over the verified encrypted channel (Frame protocol)
369    /// 4. Guest stores secrets in /run/secrets/ and sets env vars
370    ///
371    /// # Arguments
372    /// * `secrets` - List of secrets to inject
373    /// * `policy` - Attestation policy for TEE verification
374    /// * `allow_simulated` - Whether to accept simulated TEE reports
375    pub async fn inject(
376        &self,
377        secrets: &[SecretEntry],
378        policy: crate::tee::AttestationPolicy,
379        allow_simulated: bool,
380    ) -> Result<SecretInjectionResult> {
381        use a3s_box_core::tee::{AttestRequest, AttestRoute};
382
383        if secrets.is_empty() {
384            return Ok(SecretInjectionResult {
385                injected: 0,
386                errors: vec![],
387            });
388        }
389
390        // Build RA-TLS connection (attestation verified during handshake)
391        let mut tls_stream = connect_ratls(&self.socket_path, policy, allow_simulated).await?;
392
393        // Build and send Frame-based secret injection request
394        let req = AttestRequest {
395            route: AttestRoute::Secrets,
396            payload: serde_json::json!({ "secrets": secrets }),
397        };
398        let payload = serde_json::to_vec(&req).map_err(|e| {
399            BoxError::AttestationError(format!("Failed to serialize secrets request: {}", e))
400        })?;
401        write_tls_frame(&mut tls_stream, 0x01, &payload).await?;
402
403        // Read response frame
404        let (frame_type, response_data) = read_tls_frame(&mut tls_stream).await?;
405
406        if frame_type == 0x04 {
407            let msg = String::from_utf8_lossy(&response_data);
408            return Err(BoxError::AttestationError(format!(
409                "Secret injection failed: {}",
410                msg,
411            )));
412        }
413
414        let result: SecretInjectionResult =
415            serde_json::from_slice(&response_data).map_err(|e| {
416                BoxError::AttestationError(format!("Failed to parse injection response: {}", e))
417            })?;
418
419        Ok(result)
420    }
421}
422
423/// Result of a seal operation from the guest.
424#[derive(Debug, Clone, serde::Deserialize)]
425pub struct SealResult {
426    /// Sealed blob (base64-encoded): nonce || ciphertext || tag.
427    pub blob: String,
428    /// Policy used for sealing.
429    pub policy: String,
430    /// Context used for key derivation.
431    pub context: String,
432}
433
434/// Result of an unseal operation from the guest.
435#[derive(Debug, Clone, serde::Deserialize)]
436pub struct UnsealResult {
437    /// Decrypted data (base64-encoded).
438    pub data: String,
439}
440
441/// Client for seal/unseal operations in the TEE via RA-TLS.
442///
443/// Connects to the guest's RA-TLS attestation server, verifies the TEE
444/// during the TLS handshake, then sends seal/unseal requests over the
445/// encrypted channel. The guest performs the actual crypto using keys
446/// derived from its TEE identity (measurement + chip_id).
447#[derive(Debug)]
448pub struct SealClient {
449    socket_path: PathBuf,
450}
451
452impl SealClient {
453    /// Create a new seal client for the given attestation socket.
454    pub fn new(socket_path: &Path) -> Self {
455        Self {
456            socket_path: socket_path.to_path_buf(),
457        }
458    }
459
460    /// Seal data inside the TEE via RA-TLS.
461    ///
462    /// 1. Connects to the guest attestation server
463    /// 2. TLS handshake verifies the TEE
464    /// 3. Sends plaintext (base64) over the encrypted channel (Frame protocol)
465    /// 4. Guest encrypts with AES-256-GCM bound to TEE identity
466    ///
467    /// # Arguments
468    /// * `data` - Raw data to seal
469    /// * `context` - Application-specific context for key derivation
470    /// * `policy` - Sealing policy name ("MeasurementAndChip", "MeasurementOnly", "ChipOnly")
471    /// * `attestation_policy` - Attestation policy for TEE verification
472    /// * `allow_simulated` - Whether to accept simulated TEE reports
473    pub async fn seal(
474        &self,
475        data: &[u8],
476        context: &str,
477        policy: &str,
478        attestation_policy: crate::tee::AttestationPolicy,
479        allow_simulated: bool,
480    ) -> Result<SealResult> {
481        use a3s_box_core::tee::{AttestRequest, AttestRoute};
482        use base64::Engine;
483
484        let mut tls_stream =
485            connect_ratls(&self.socket_path, attestation_policy, allow_simulated).await?;
486
487        let req = AttestRequest {
488            route: AttestRoute::Seal,
489            payload: serde_json::json!({
490                "data": base64::engine::general_purpose::STANDARD.encode(data),
491                "context": context,
492                "policy": policy,
493            }),
494        };
495        let payload = serde_json::to_vec(&req).map_err(|e| {
496            BoxError::AttestationError(format!("Failed to serialize seal request: {}", e))
497        })?;
498        write_tls_frame(&mut tls_stream, 0x01, &payload).await?;
499
500        let (frame_type, response_data) = read_tls_frame(&mut tls_stream).await?;
501
502        if frame_type == 0x04 {
503            let msg = String::from_utf8_lossy(&response_data);
504            return Err(BoxError::AttestationError(format!(
505                "Seal request failed: {}",
506                msg,
507            )));
508        }
509
510        let result: SealResult = serde_json::from_slice(&response_data).map_err(|e| {
511            BoxError::AttestationError(format!("Failed to parse seal response: {}", e))
512        })?;
513
514        Ok(result)
515    }
516
517    /// Unseal data inside the TEE via RA-TLS.
518    ///
519    /// 1. Connects to the guest attestation server
520    /// 2. TLS handshake verifies the TEE
521    /// 3. Sends sealed blob over the encrypted channel (Frame protocol)
522    /// 4. Guest decrypts with the TEE-bound key
523    ///
524    /// # Arguments
525    /// * `blob` - Base64-encoded sealed blob
526    /// * `context` - Context used during sealing
527    /// * `policy` - Sealing policy used during sealing
528    /// * `attestation_policy` - Attestation policy for TEE verification
529    /// * `allow_simulated` - Whether to accept simulated TEE reports
530    pub async fn unseal(
531        &self,
532        blob: &str,
533        context: &str,
534        policy: &str,
535        attestation_policy: crate::tee::AttestationPolicy,
536        allow_simulated: bool,
537    ) -> Result<Vec<u8>> {
538        use a3s_box_core::tee::{AttestRequest, AttestRoute};
539        use base64::Engine;
540
541        let mut tls_stream =
542            connect_ratls(&self.socket_path, attestation_policy, allow_simulated).await?;
543
544        let req = AttestRequest {
545            route: AttestRoute::Unseal,
546            payload: serde_json::json!({
547                "blob": blob,
548                "context": context,
549                "policy": policy,
550            }),
551        };
552        let payload = serde_json::to_vec(&req).map_err(|e| {
553            BoxError::AttestationError(format!("Failed to serialize unseal request: {}", e))
554        })?;
555        write_tls_frame(&mut tls_stream, 0x01, &payload).await?;
556
557        let (frame_type, response_data) = read_tls_frame(&mut tls_stream).await?;
558
559        if frame_type == 0x04 {
560            let msg = String::from_utf8_lossy(&response_data);
561            return Err(BoxError::AttestationError(format!(
562                "Unseal request failed: {}",
563                msg,
564            )));
565        }
566
567        let result: UnsealResult = serde_json::from_slice(&response_data).map_err(|e| {
568            BoxError::AttestationError(format!("Failed to parse unseal response: {}", e))
569        })?;
570
571        let plaintext = base64::engine::general_purpose::STANDARD
572            .decode(&result.data)
573            .map_err(|e| {
574                BoxError::AttestationError(format!("Failed to decode unsealed data: {}", e))
575            })?;
576
577        Ok(plaintext)
578    }
579}
580
581// ============================================================================
582// TLS Frame helpers (used by RA-TLS clients)
583// ============================================================================
584
585/// Write a frame over an async TLS stream.
586/// Wire format: [type:u8][length:u32 BE][payload]
587async fn write_tls_frame<S>(stream: &mut S, frame_type: u8, payload: &[u8]) -> Result<()>
588where
589    S: tokio::io::AsyncWriteExt + Unpin,
590{
591    let len = payload.len() as u32;
592    let mut header = [0u8; 5];
593    header[0] = frame_type;
594    header[1..5].copy_from_slice(&len.to_be_bytes());
595    stream
596        .write_all(&header)
597        .await
598        .map_err(|e| BoxError::AttestationError(format!("TLS frame header write failed: {}", e)))?;
599    if !payload.is_empty() {
600        stream.write_all(payload).await.map_err(|e| {
601            BoxError::AttestationError(format!("TLS frame payload write failed: {}", e))
602        })?;
603    }
604    Ok(())
605}
606
607/// Read a frame from an async TLS stream.
608/// Returns (frame_type, payload). Treats unexpected EOF after handshake as empty response.
609async fn read_tls_frame<S>(stream: &mut S) -> Result<(u8, Vec<u8>)>
610where
611    S: tokio::io::AsyncReadExt + Unpin,
612{
613    let mut header = [0u8; 5];
614    match stream.read_exact(&mut header).await {
615        Ok(_) => {}
616        Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
617            tracing::debug!("RA-TLS peer closed without sending response frame");
618            return Ok((0x01, Vec::new()));
619        }
620        Err(e) => {
621            return Err(BoxError::AttestationError(format!(
622                "TLS frame header read failed: {}",
623                e
624            )));
625        }
626    }
627    let frame_type = header[0];
628    let len = u32::from_be_bytes([header[1], header[2], header[3], header[4]]) as usize;
629    let mut payload = vec![0u8; len];
630    if len > 0 {
631        stream.read_exact(&mut payload).await.map_err(|e| {
632            BoxError::AttestationError(format!("TLS frame payload read failed: {}", e))
633        })?;
634    }
635    Ok((frame_type, payload))
636}
637
638#[cfg(test)]
639mod tests {
640    use super::*;
641    use crate::tee::attestation::{CertificateChain, PlatformInfo};
642    use tokio::io::{AsyncReadExt, AsyncWriteExt};
643    use tokio::net::UnixListener;
644
645    fn bind_test_listener(path: &Path) -> Option<UnixListener> {
646        match UnixListener::bind(path) {
647            Ok(listener) => Some(listener),
648            Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
649                eprintln!(
650                    "skipping Unix socket test; sandbox denied bind at {}: {}",
651                    path.display(),
652                    e
653                );
654                None
655            }
656            Err(e) => panic!("failed to bind test socket {}: {}", path.display(), e),
657        }
658    }
659
660    fn test_report() -> AttestationReport {
661        AttestationReport {
662            report: vec![1, 2, 3, 4],
663            cert_chain: CertificateChain::default(),
664            platform: PlatformInfo::default(),
665        }
666    }
667
668    async fn spawn_attestation_http_server(listener: UnixListener, response: Vec<u8>) -> Vec<u8> {
669        let (mut stream, _) = listener.accept().await.unwrap();
670        let mut request = Vec::new();
671        let mut buf = [0u8; 1024];
672        loop {
673            let n = stream.read(&mut buf).await.unwrap();
674            if n == 0 {
675                break;
676            }
677            request.extend_from_slice(&buf[..n]);
678            if request.windows(4).any(|w| w == b"\r\n\r\n") {
679                break;
680            }
681        }
682        stream.write_all(&response).await.unwrap();
683        request
684    }
685
686    #[tokio::test]
687    async fn test_attestation_connect_nonexistent_socket() {
688        let result =
689            AttestationClient::connect(Path::new("/tmp/nonexistent-a3s-attest-test.sock")).await;
690        assert!(result.is_err());
691        let err = result.unwrap_err();
692        assert!(matches!(err, BoxError::AttestationError(_)));
693    }
694
695    #[tokio::test]
696    async fn test_attestation_connect_and_socket_path() {
697        let tmp = tempfile::TempDir::new().unwrap();
698        let sock_path = tmp.path().join("attest.sock");
699        let Some(_listener) = bind_test_listener(&sock_path) else {
700            return;
701        };
702
703        let client = AttestationClient::connect(&sock_path).await.unwrap();
704        assert_eq!(client.socket_path(), sock_path);
705    }
706
707    #[tokio::test]
708    async fn test_attestation_get_report_parses_success_response() {
709        let tmp = tempfile::TempDir::new().unwrap();
710        let sock_path = tmp.path().join("attest_success.sock");
711        let Some(listener) = bind_test_listener(&sock_path) else {
712            return;
713        };
714
715        let body = serde_json::to_vec(&test_report()).unwrap();
716        let response = format!(
717            "HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n{}",
718            body.len(),
719            String::from_utf8(body).unwrap()
720        )
721        .into_bytes();
722        let server = tokio::spawn(spawn_attestation_http_server(listener, response));
723
724        let client = AttestationClient {
725            socket_path: sock_path,
726        };
727        let report = client
728            .get_report(&AttestationRequest {
729                nonce: vec![9, 8, 7],
730                user_data: None,
731            })
732            .await
733            .unwrap();
734
735        assert_eq!(report.report, vec![1, 2, 3, 4]);
736        let request = server.await.unwrap();
737        let request = String::from_utf8_lossy(&request);
738        assert!(request.starts_with("POST /attest HTTP/1.1\r\n"));
739        assert!(request.contains("Content-Type: application/json\r\n"));
740        assert!(request.contains("Content-Length:"));
741    }
742
743    #[tokio::test]
744    async fn test_attestation_get_report_surfaces_http_error_body() {
745        let tmp = tempfile::TempDir::new().unwrap();
746        let sock_path = tmp.path().join("attest_error.sock");
747        let Some(listener) = bind_test_listener(&sock_path) else {
748            return;
749        };
750
751        let response =
752            b"HTTP/1.1 500 Internal Server Error\r\nContent-Length: 12\r\n\r\nbad hardware"
753                .to_vec();
754        let server = tokio::spawn(spawn_attestation_http_server(listener, response));
755
756        let client = AttestationClient {
757            socket_path: sock_path,
758        };
759        let err = client
760            .get_report(&AttestationRequest {
761                nonce: vec![],
762                user_data: None,
763            })
764            .await
765            .unwrap_err();
766
767        assert!(matches!(err, BoxError::AttestationError(_)));
768        assert!(err.to_string().contains("bad hardware"));
769        let _ = server.await.unwrap();
770    }
771
772    #[tokio::test]
773    async fn test_attestation_get_report_rejects_malformed_and_invalid_json() {
774        for (name, response, expected) in [
775            (
776                "no_body",
777                b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\n".to_vec(),
778                "no HTTP body",
779            ),
780            (
781                "bad_json",
782                b"HTTP/1.1 200 OK\r\nContent-Length: 8\r\n\r\nnot-json".to_vec(),
783                "Failed to parse attestation response",
784            ),
785        ] {
786            let tmp = tempfile::TempDir::new().unwrap();
787            let sock_path = tmp.path().join(format!("{name}.sock"));
788            let Some(listener) = bind_test_listener(&sock_path) else {
789                return;
790            };
791            let server = tokio::spawn(spawn_attestation_http_server(listener, response));
792
793            let client = AttestationClient {
794                socket_path: sock_path,
795            };
796            let err = client
797                .get_report(&AttestationRequest {
798                    nonce: vec![],
799                    user_data: None,
800                })
801                .await
802                .unwrap_err();
803
804            assert!(err.to_string().contains(expected), "{err}");
805            let _ = server.await.unwrap();
806        }
807    }
808
809    #[tokio::test]
810    async fn test_tls_frame_write_and_read_roundtrip() {
811        let (mut client, mut server) = tokio::io::duplex(64);
812
813        let writer = tokio::spawn(async move {
814            write_tls_frame(&mut client, 0x03, b"payload")
815                .await
816                .unwrap();
817            write_tls_frame(&mut client, 0x02, b"").await.unwrap();
818        });
819
820        let (frame_type, payload) = read_tls_frame(&mut server).await.unwrap();
821        assert_eq!(frame_type, 0x03);
822        assert_eq!(payload, b"payload");
823
824        let (frame_type, payload) = read_tls_frame(&mut server).await.unwrap();
825        assert_eq!(frame_type, 0x02);
826        assert!(payload.is_empty());
827
828        writer.await.unwrap();
829    }
830
831    #[tokio::test]
832    async fn test_tls_frame_read_unexpected_eof_is_empty_response() {
833        let (client, mut server) = tokio::io::duplex(8);
834        drop(client);
835
836        let (frame_type, payload) = read_tls_frame(&mut server).await.unwrap();
837        assert_eq!(frame_type, 0x01);
838        assert!(payload.is_empty());
839    }
840
841    #[tokio::test]
842    async fn test_tls_frame_read_truncated_payload_errors() {
843        let (mut client, mut server) = tokio::io::duplex(16);
844        client
845            .write_all(&[0x01, 0, 0, 0, 5, b'a', b'b'])
846            .await
847            .unwrap();
848        drop(client);
849
850        let err = read_tls_frame(&mut server).await.unwrap_err();
851        assert!(matches!(err, BoxError::AttestationError(_)));
852        assert!(err.to_string().contains("payload read failed"));
853    }
854
855    #[tokio::test]
856    async fn test_secret_injector_empty_secrets_returns_without_connecting() {
857        let injector = SecretInjector::new(Path::new("/tmp/nonexistent-empty-secrets.sock"));
858        let result = injector
859            .inject(&[], crate::tee::AttestationPolicy::default(), false)
860            .await
861            .unwrap();
862
863        assert_eq!(result.injected, 0);
864        assert!(result.errors.is_empty());
865    }
866
867    #[test]
868    fn test_attestation_client_constructors_and_serde_defaults() {
869        let path = Path::new("/tmp/a3s-attest.sock");
870        assert_eq!(RaTlsAttestationClient::new(path).socket_path(), path);
871        assert_eq!(SecretInjector::new(path).socket_path, path);
872        assert_eq!(SealClient::new(path).socket_path, path);
873
874        let secret: SecretEntry =
875            serde_json::from_str(r#"{"name":"TOKEN","value":"secret"}"#).unwrap();
876        assert!(secret.set_env);
877
878        let result: SecretInjectionResult = serde_json::from_str(r#"{"injected":2}"#).unwrap();
879        assert_eq!(result.injected, 2);
880        assert!(result.errors.is_empty());
881
882        let seal: SealResult =
883            serde_json::from_str(r#"{"blob":"abc","policy":"ChipOnly","context":"ctx"}"#).unwrap();
884        assert_eq!(seal.blob, "abc");
885        assert_eq!(seal.policy, "ChipOnly");
886        assert_eq!(seal.context, "ctx");
887
888        let unseal: UnsealResult = serde_json::from_str(r#"{"data":"c2VjcmV0"}"#).unwrap();
889        assert_eq!(unseal.data, "c2VjcmV0");
890    }
891}