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
166impl RaTlsAttestationClient {
167    /// Create a new RA-TLS attestation client for the given socket path.
168    pub fn new(socket_path: &Path) -> Self {
169        Self {
170            socket_path: socket_path.to_path_buf(),
171        }
172    }
173
174    /// Get the socket path.
175    pub fn socket_path(&self) -> &Path {
176        &self.socket_path
177    }
178
179    /// Verify TEE attestation via RA-TLS handshake.
180    ///
181    /// Connects to the guest attestation server, performs a TLS handshake
182    /// with a custom verifier that checks the SNP report embedded in the
183    /// server's certificate, and returns the verification result.
184    ///
185    /// # Arguments
186    /// * `policy` - Attestation policy to verify against
187    /// * `allow_simulated` - Whether to accept simulated (non-hardware) reports
188    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        // Send a Frame-based status request
198        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        // Read response frame
208        let _response = read_tls_frame(&mut tls_stream).await?;
209
210        // Extract the peer certificate for detailed report info
211        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        // If we got here, TLS handshake succeeded (verifier passed)
232        // but we couldn't extract the cert for detailed results
233        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    /// Fetch the raw attestation report over RA-TLS, without applying a
249    /// verification policy.
250    ///
251    /// The guest attestation server speaks RA-TLS + framed messages (not plain
252    /// HTTP); the signed report is carried in the server's TLS certificate and
253    /// is extracted here after the handshake.
254    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        // Exchange a Status frame so the handshake (and report extraction)
265        // completes against a live server.
266        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/// A secret to inject into the TEE.
290#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
291pub struct SecretEntry {
292    /// Secret name (used as filename in /run/secrets/ and env var name).
293    pub name: String,
294    /// Secret value.
295    pub value: String,
296    /// Whether to set as environment variable in the guest (default: true).
297    #[serde(default = "default_true")]
298    pub set_env: bool,
299}
300
301fn default_true() -> bool {
302    true
303}
304
305/// Response from the guest after secret injection.
306#[derive(Debug, Clone, serde::Deserialize)]
307pub struct SecretInjectionResult {
308    /// Number of secrets successfully injected.
309    pub injected: usize,
310    /// Any non-fatal errors encountered.
311    #[serde(default)]
312    pub errors: Vec<String>,
313}
314
315/// Client for injecting secrets into the TEE via RA-TLS.
316///
317/// Connects to the guest's RA-TLS attestation server, verifies the TEE
318/// during the TLS handshake, then sends secrets over the encrypted channel.
319/// The guest stores secrets in `/run/secrets/` (tmpfs) and optionally
320/// sets them as environment variables.
321#[derive(Debug)]
322pub struct SecretInjector {
323    socket_path: PathBuf,
324}
325
326impl SecretInjector {
327    /// Create a new secret injector for the given attestation socket.
328    pub fn new(socket_path: &Path) -> Self {
329        Self {
330            socket_path: socket_path.to_path_buf(),
331        }
332    }
333
334    /// Inject secrets into the TEE via RA-TLS.
335    ///
336    /// 1. Connects to the guest attestation server
337    /// 2. TLS handshake verifies the TEE (attestation in cert)
338    /// 3. Sends secrets over the verified encrypted channel (Frame protocol)
339    /// 4. Guest stores secrets in /run/secrets/ and sets env vars
340    ///
341    /// # Arguments
342    /// * `secrets` - List of secrets to inject
343    /// * `policy` - Attestation policy for TEE verification
344    /// * `allow_simulated` - Whether to accept simulated TEE reports
345    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        // Build RA-TLS connection (attestation verified during handshake)
361        let mut tls_stream = connect_ratls(&self.socket_path, policy, allow_simulated).await?;
362
363        // Build and send Frame-based secret injection request
364        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        // Read response frame
374        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/// Result of a seal operation from the guest.
394#[derive(Debug, Clone, serde::Deserialize)]
395pub struct SealResult {
396    /// Sealed blob (base64-encoded): nonce || ciphertext || tag.
397    pub blob: String,
398    /// Policy used for sealing.
399    pub policy: String,
400    /// Context used for key derivation.
401    pub context: String,
402}
403
404/// Result of an unseal operation from the guest.
405#[derive(Debug, Clone, serde::Deserialize)]
406pub struct UnsealResult {
407    /// Decrypted data (base64-encoded).
408    pub data: String,
409}
410
411/// Client for seal/unseal operations in the TEE via RA-TLS.
412///
413/// Connects to the guest's RA-TLS attestation server, verifies the TEE
414/// during the TLS handshake, then sends seal/unseal requests over the
415/// encrypted channel. The guest performs the actual crypto using keys
416/// derived from its TEE identity (measurement + chip_id).
417#[derive(Debug)]
418pub struct SealClient {
419    socket_path: PathBuf,
420}
421
422impl SealClient {
423    /// Create a new seal client for the given attestation socket.
424    pub fn new(socket_path: &Path) -> Self {
425        Self {
426            socket_path: socket_path.to_path_buf(),
427        }
428    }
429
430    /// Seal data inside the TEE via RA-TLS.
431    ///
432    /// 1. Connects to the guest attestation server
433    /// 2. TLS handshake verifies the TEE
434    /// 3. Sends plaintext (base64) over the encrypted channel (Frame protocol)
435    /// 4. Guest encrypts with AES-256-GCM bound to TEE identity
436    ///
437    /// # Arguments
438    /// * `data` - Raw data to seal
439    /// * `context` - Application-specific context for key derivation
440    /// * `policy` - Sealing policy name ("MeasurementAndChip", "MeasurementOnly", "ChipOnly")
441    /// * `attestation_policy` - Attestation policy for TEE verification
442    /// * `allow_simulated` - Whether to accept simulated TEE reports
443    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    /// Unseal data inside the TEE via RA-TLS.
488    ///
489    /// 1. Connects to the guest attestation server
490    /// 2. TLS handshake verifies the TEE
491    /// 3. Sends sealed blob over the encrypted channel (Frame protocol)
492    /// 4. Guest decrypts with the TEE-bound key
493    ///
494    /// # Arguments
495    /// * `blob` - Base64-encoded sealed blob
496    /// * `context` - Context used during sealing
497    /// * `policy` - Sealing policy used during sealing
498    /// * `attestation_policy` - Attestation policy for TEE verification
499    /// * `allow_simulated` - Whether to accept simulated TEE reports
500    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
551// ============================================================================
552// TLS Frame helpers (used by RA-TLS clients)
553// ============================================================================
554
555/// Write a frame over an async TLS stream.
556/// Wire format: [type:u8][length:u32 BE][payload]
557async 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
577/// Read a frame from an async TLS stream.
578/// Returns (frame_type, payload). Treats unexpected EOF after handshake as empty response.
579async 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}