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