1use std::path::{Path, PathBuf};
4
5use a3s_box_core::error::{BoxError, Result};
6use tokio::io::{AsyncReadExt, AsyncWriteExt};
7use tokio::net::UnixStream;
8
9use crate::tee::attestation::{AttestationReport, AttestationRequest};
10
11#[derive(Debug)]
16pub struct AttestationClient {
17 socket_path: PathBuf,
18}
19
20impl AttestationClient {
21 pub async fn connect(socket_path: &Path) -> Result<Self> {
23 let _stream = UnixStream::connect(socket_path).await.map_err(|e| {
24 BoxError::AttestationError(format!(
25 "Failed to connect to agent at {}: {}",
26 socket_path.display(),
27 e,
28 ))
29 })?;
30
31 Ok(Self {
32 socket_path: socket_path.to_path_buf(),
33 })
34 }
35
36 pub fn socket_path(&self) -> &Path {
38 &self.socket_path
39 }
40
41 pub async fn get_report(&self, request: &AttestationRequest) -> Result<AttestationReport> {
54 let body = serde_json::to_string(request).map_err(|e| {
55 BoxError::AttestationError(format!("Failed to serialize attestation request: {}", e))
56 })?;
57
58 let http_request = format!(
59 "POST /attest HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
60 body.len(),
61 body,
62 );
63
64 let mut stream = UnixStream::connect(&self.socket_path).await.map_err(|e| {
65 BoxError::AttestationError(format!(
66 "Attestation connection failed to {}: {}",
67 self.socket_path.display(),
68 e,
69 ))
70 })?;
71
72 stream
73 .write_all(http_request.as_bytes())
74 .await
75 .map_err(|e| {
76 BoxError::AttestationError(format!("Attestation request write failed: {}", e))
77 })?;
78
79 let mut response = Vec::with_capacity(8192);
81 let mut buf = vec![0u8; 8192];
82 loop {
83 let n = stream.read(&mut buf).await.map_err(|e| {
84 BoxError::AttestationError(format!("Attestation response read failed: {}", e))
85 })?;
86 if n == 0 {
87 break;
88 }
89 response.extend_from_slice(&buf[..n]);
90 if response.len() > 1024 * 1024 {
92 break;
93 }
94 }
95
96 let response_str = String::from_utf8_lossy(&response);
97
98 let body_str = response_str
100 .find("\r\n\r\n")
101 .map(|pos| &response_str[pos + 4..])
102 .ok_or_else(|| {
103 BoxError::AttestationError(
104 "Malformed attestation response: no HTTP body".to_string(),
105 )
106 })?;
107
108 if !response_str.starts_with("HTTP/1.1 200") && !response_str.starts_with("HTTP/1.0 200") {
110 return Err(BoxError::AttestationError(format!(
111 "Attestation request failed: {}",
112 body_str.chars().take(200).collect::<String>(),
113 )));
114 }
115
116 let report: AttestationReport = serde_json::from_str(body_str).map_err(|e| {
117 BoxError::AttestationError(format!("Failed to parse attestation response: {}", e))
118 })?;
119
120 Ok(report)
121 }
122}
123
124async fn connect_ratls(
129 socket_path: &Path,
130 policy: crate::tee::AttestationPolicy,
131 allow_simulated: bool,
132) -> Result<tokio_rustls::client::TlsStream<UnixStream>> {
133 let client_config = crate::tee::ratls::create_client_config(policy, allow_simulated)?;
134 let connector = tokio_rustls::TlsConnector::from(std::sync::Arc::new(client_config));
135
136 let stream = UnixStream::connect(socket_path).await.map_err(|e| {
137 BoxError::AttestationError(format!(
138 "Failed to connect to RA-TLS server at {}: {}",
139 socket_path.display(),
140 e,
141 ))
142 })?;
143
144 let server_name = rustls::pki_types::ServerName::try_from("localhost")
145 .map_err(|e| BoxError::AttestationError(format!("Invalid server name: {}", e)))?;
146
147 connector
148 .connect(server_name, stream)
149 .await
150 .map_err(|e| BoxError::AttestationError(format!("RA-TLS handshake failed: {}", e)))
151}
152
153#[derive(Debug)]
162pub struct RaTlsAttestationClient {
163 socket_path: PathBuf,
164}
165
166#[derive(Debug, Clone)]
168pub(crate) struct RaTlsAttestationEvidence {
169 pub(crate) report: AttestationReport,
170 pub(crate) certificate_der: Vec<u8>,
171}
172
173impl RaTlsAttestationClient {
174 pub fn new(socket_path: &Path) -> Self {
176 Self {
177 socket_path: socket_path.to_path_buf(),
178 }
179 }
180
181 pub fn socket_path(&self) -> &Path {
183 &self.socket_path
184 }
185
186 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 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 let _response = read_tls_frame(&mut tls_stream).await?;
216
217 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 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 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 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 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 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#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
321pub struct SecretEntry {
322 pub name: String,
324 pub value: String,
326 #[serde(default = "default_true")]
328 pub set_env: bool,
329}
330
331fn default_true() -> bool {
332 true
333}
334
335#[derive(Debug, Clone, serde::Deserialize)]
337pub struct SecretInjectionResult {
338 pub injected: usize,
340 #[serde(default)]
342 pub errors: Vec<String>,
343}
344
345#[derive(Debug)]
352pub struct SecretInjector {
353 socket_path: PathBuf,
354}
355
356impl SecretInjector {
357 pub fn new(socket_path: &Path) -> Self {
359 Self {
360 socket_path: socket_path.to_path_buf(),
361 }
362 }
363
364 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 let mut tls_stream = connect_ratls(&self.socket_path, policy, allow_simulated).await?;
392
393 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 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#[derive(Debug, Clone, serde::Deserialize)]
425pub struct SealResult {
426 pub blob: String,
428 pub policy: String,
430 pub context: String,
432}
433
434#[derive(Debug, Clone, serde::Deserialize)]
436pub struct UnsealResult {
437 pub data: String,
439}
440
441#[derive(Debug)]
448pub struct SealClient {
449 socket_path: PathBuf,
450}
451
452impl SealClient {
453 pub fn new(socket_path: &Path) -> Self {
455 Self {
456 socket_path: socket_path.to_path_buf(),
457 }
458 }
459
460 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 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
581async 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
607async 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}