1use std::{
4 sync::{Arc, LazyLock},
5 time::Duration,
6};
7
8use tokio::sync::Semaphore;
9
10use attested_request::{
11 base::CanonicalRequest,
12 sign::{SignError, SignedHeaders, Signer},
13 Platform,
14};
15use tokio_tungstenite::tungstenite::handshake::client::Request;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)]
19pub enum RequestIntegrityPlatform {
20 Ios,
22 Android,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error, uniffi::Error)]
28pub enum RequestIntegrityError {
29 #[error("request integrity is unavailable")]
31 Unavailable,
32 #[error("request integrity session is invalid")]
34 InvalidSession,
35 #[error("request integrity signing failed")]
37 SigningFailed,
38 #[error("request integrity callback failed")]
40 CallbackFailed,
41 #[error("request integrity authentication timed out")]
43 TimedOut,
44}
45
46impl From<uniffi::UnexpectedUniFFICallbackError> for RequestIntegrityError {
47 fn from(_: uniffi::UnexpectedUniFFICallbackError) -> Self {
48 Self::CallbackFailed
49 }
50}
51
52#[uniffi::export(with_foreign)]
60pub trait RequestDigestSigner: Send + Sync {
61 fn sign_digest(
70 &self,
71 client_data_hash: Vec<u8>,
72 ) -> Result<Vec<u8>, RequestIntegrityError>;
73}
74
75#[derive(Clone, uniffi::Record)]
81pub struct RequestIntegritySession {
82 pub token: String,
84 pub platform: RequestIntegrityPlatform,
86 pub signer: Arc<dyn RequestDigestSigner>,
88}
89
90#[uniffi::export(with_foreign)]
97#[async_trait::async_trait]
98pub trait RequestIntegrityProvider: Send + Sync {
99 async fn prepare(&self) -> Result<RequestIntegritySession, RequestIntegrityError>;
104}
105
106const AUTHENTICATION_TIMEOUT: Duration = Duration::from_secs(30);
107
108static SIGNING_SLOTS: LazyLock<Arc<Semaphore>> =
110 LazyLock::new(|| Arc::new(Semaphore::new(4)));
111
112pub(super) async fn sign_request(
114 provider: Arc<dyn RequestIntegrityProvider>,
115 request: Request,
116) -> Result<SignedHeaders, RequestIntegrityError> {
117 sign_request_with_limit(provider, request, SIGNING_SLOTS.clone()).await
118}
119
120async fn sign_request_with_limit(
122 provider: Arc<dyn RequestIntegrityProvider>,
123 request: Request,
124 slots: Arc<Semaphore>,
125) -> Result<SignedHeaders, RequestIntegrityError> {
126 tokio::time::timeout(AUTHENTICATION_TIMEOUT, async move {
127 let session = provider.prepare().await?;
128 if session.token.is_empty()
129 || !session
130 .token
131 .bytes()
132 .all(|byte| (0x21..=0x7e).contains(&byte))
133 {
134 return Err(RequestIntegrityError::InvalidSession);
135 }
136
137 let permit = slots
138 .acquire_owned()
139 .await
140 .map_err(|_| RequestIntegrityError::Unavailable)?;
141 tokio::task::spawn_blocking(move || {
142 let _permit = permit;
143 let uri = request.uri();
144 let scheme = match uri.scheme_str() {
145 Some("ws" | "http") => "http",
146 Some("wss" | "https") => "https",
147 _ => return Err(RequestIntegrityError::SigningFailed),
148 };
149 let authority = uri
150 .authority()
151 .ok_or(RequestIntegrityError::SigningFailed)?;
152 let canonical = CanonicalRequest::new(
153 request.method().as_str(),
154 scheme,
155 authority.as_str(),
156 uri.path(),
157 uri.query(),
158 b"",
159 )
160 .map_err(|_| RequestIntegrityError::SigningFailed)?;
161 attested_request::sign::sign_request(&canonical, &session.token, &session)
162 .map_err(|error| match error {
163 SignError::Signer(error) => error,
164 _ => RequestIntegrityError::SigningFailed,
165 })
166 })
167 .await
168 .map_err(|_| RequestIntegrityError::SigningFailed)?
169 })
170 .await
171 .map_err(|_| RequestIntegrityError::TimedOut)?
172}
173
174impl Signer for RequestIntegritySession {
175 type Error = RequestIntegrityError;
176
177 fn platform(&self) -> Platform {
178 match self.platform {
179 RequestIntegrityPlatform::Ios => Platform::Ios,
180 RequestIntegrityPlatform::Android => Platform::Android,
181 }
182 }
183
184 fn sign(&self, client_data_hash: &[u8; 32]) -> Result<Vec<u8>, Self::Error> {
185 let signature = self.signer.sign_digest(client_data_hash.to_vec())?;
186 if signature.is_empty() {
187 return Err(RequestIntegrityError::SigningFailed);
188 }
189 Ok(signature)
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use std::{
196 sync::{
197 atomic::{AtomicU8, Ordering},
198 Condvar, Mutex,
199 },
200 time::SystemTime,
201 };
202
203 use attested_request::{
204 base::CanonicalRequest,
205 sign::SignedHeaders,
206 signature::SignatureParams,
207 test_util::{test_key, SoftwareSigner, TestClaims, TestIssuer},
208 token::TokenVerifier,
209 Platform, RejectReason, Verifier,
210 };
211 use tokio_tungstenite::tungstenite::{
212 client::IntoClientRequest, handshake::client::Request, http,
213 };
214
215 use super::*;
216
217 const AUDIENCE: &str = "flamingo-verifier";
218 const TARGET: &str = "/proxy/a%20b/v1/matches?x=%2F&x=one+two";
219
220 fn request(url: &str) -> Request {
221 url.into_client_request().unwrap()
222 }
223
224 fn received(
225 headers: &SignedHeaders,
226 method: &str,
227 target: &str,
228 ) -> http::request::Parts {
229 let mut request = http::Request::builder().method(method).uri(target);
230 for (name, value) in headers.headers() {
231 request = request.header(name, value);
232 }
233 request.body(()).unwrap().into_parts().0
234 }
235
236 struct SoftwareCallback {
237 signer: SoftwareSigner,
238 runtime_thread: std::thread::ThreadId,
239 digests: Arc<Mutex<Vec<Vec<u8>>>>,
240 }
241
242 impl RequestDigestSigner for SoftwareCallback {
243 fn sign_digest(
244 &self,
245 client_data_hash: Vec<u8>,
246 ) -> Result<Vec<u8>, RequestIntegrityError> {
247 assert_ne!(std::thread::current().id(), self.runtime_thread);
248 let digest = client_data_hash.as_slice().try_into().unwrap();
249 self.digests.lock().unwrap().push(client_data_hash);
250 Ok(self.signer.sign(&digest).unwrap())
251 }
252 }
253
254 struct RotatingProvider {
255 issuer: TestIssuer,
256 platform: Platform,
257 next_key: AtomicU8,
258 runtime_thread: std::thread::ThreadId,
259 digests: Arc<Mutex<Vec<Vec<u8>>>>,
260 }
261
262 impl RotatingProvider {
263 fn new(platform: Platform) -> Self {
264 Self {
265 issuer: TestIssuer::new("https://attestation.example"),
266 platform,
267 next_key: AtomicU8::new(1),
268 runtime_thread: std::thread::current().id(),
269 digests: Arc::new(Mutex::new(Vec::new())),
270 }
271 }
272
273 fn verifier(&self, scheme: &str, authority: &str) -> Verifier {
274 let tokens = TokenVerifier::new(
275 [(
276 self.issuer.issuer.clone(),
277 Arc::new(self.issuer.keys()) as _,
278 )],
279 [AUDIENCE.to_owned()],
280 )
281 .unwrap();
282 Verifier::builder(tokens, authority)
283 .scheme(scheme)
284 .build()
285 .unwrap()
286 }
287 }
288
289 #[async_trait::async_trait]
290 impl RequestIntegrityProvider for RotatingProvider {
291 async fn prepare(
292 &self,
293 ) -> Result<RequestIntegritySession, RequestIntegrityError> {
294 let key = self.next_key.fetch_add(1, Ordering::Relaxed);
295 let signer =
296 SoftwareSigner::new(test_key(&format!("device-{key}")), self.platform);
297 let token = self.issuer.mint(&TestClaims::valid(
298 AUDIENCE,
299 self.platform,
300 signer.verifying_key(),
301 SystemTime::now(),
302 ));
303 Ok(RequestIntegritySession {
304 token,
305 platform: match self.platform {
306 Platform::Ios => RequestIntegrityPlatform::Ios,
307 Platform::Android => RequestIntegrityPlatform::Android,
308 },
309 signer: Arc::new(SoftwareCallback {
310 signer,
311 runtime_thread: self.runtime_thread,
312 digests: self.digests.clone(),
313 }),
314 })
315 }
316 }
317
318 #[tokio::test]
320 #[allow(clippy::result_large_err)] async fn matcher_upgrade_verifies_after_crossing_the_network() {
322 use super::super::{FlamingoError, FlamingoMatcher, MatchClient};
323 use tokio::net::TcpListener;
324 use tokio_tungstenite::accept_hdr_async;
325
326 for platform in [Platform::Ios, Platform::Android] {
327 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
328 let authority = listener.local_addr().unwrap().to_string();
329 let (sent, received) = tokio::sync::oneshot::channel();
330 let server = tokio::spawn(async move {
331 let (stream, _) = listener.accept().await.unwrap();
332 let socket =
333 accept_hdr_async(stream, move |upgrade: &Request, response| {
334 sent.send(upgrade.clone()).unwrap();
335 Ok(response)
336 })
337 .await
338 .unwrap();
339 drop(socket);
340 });
341 let provider = Arc::new(RotatingProvider::new(platform));
342 let mut matcher = FlamingoMatcher::new(&format!(
343 "http://{authority}/proxy/a%20b/?x=%2F&x=one+two"
344 ))
345 .unwrap();
346 matcher.integrity_provider = Some(provider.clone());
347 let matcher = matcher.dangerously_skip_measurements().unwrap();
348 let result = tokio::time::timeout(Duration::from_secs(5), async {
349 matcher.client().await.unwrap().connect().await
350 })
351 .await
352 .unwrap();
353 assert!(matches!(result, Err(FlamingoError::Transport { .. })));
354 let upgrade = received.await.unwrap();
355 server.await.unwrap();
356 let (mut parts, ()) = upgrade.into_parts();
357 assert_eq!(parts.uri.path_and_query().unwrap().as_str(), TARGET);
358 let verifier = provider.verifier("http", &authority);
359 verifier.verify(&parts, b"").await.unwrap();
360 parts.uri = "/tampered/v1/matches?x=%2F&x=one+two".parse().unwrap();
361 assert_eq!(
362 verifier.verify(&parts, b"").await.unwrap_err().reason,
363 RejectReason::SignatureInvalid
364 );
365 }
366 }
367
368 #[tokio::test]
369 async fn software_callbacks_verify_for_the_final_websocket_request() {
370 for platform in [Platform::Ios, Platform::Android] {
371 for (url, scheme, authority) in [
372 (
373 format!("wss://Verifier.Example:443{TARGET}"),
374 "https",
375 "verifier.example",
376 ),
377 (
378 format!("ws://Verifier.Example:80{TARGET}"),
379 "http",
380 "verifier.example",
381 ),
382 (
383 format!("wss://Verifier.Example:8443{TARGET}"),
384 "https",
385 "verifier.example:8443",
386 ),
387 ] {
388 let provider = Arc::new(RotatingProvider::new(platform));
389 let signed =
390 sign_request(provider.clone(), request(&url)).await.unwrap();
391 let context = provider
392 .verifier(scheme, authority)
393 .verify(&received(&signed, "GET", TARGET), b"")
394 .await
395 .unwrap();
396 assert_eq!(context.device.platform, platform);
397 assert_eq!(context.device.audience, AUDIENCE);
398 assert_eq!(context.request_binding, signed.request_binding);
399 assert_eq!(
400 context.device.key.verifying_key(),
401 test_key("device-1").verifying_key()
402 );
403
404 let params = SignatureParams::parse(&signed.signature_input).unwrap();
405 let canonical = CanonicalRequest::new(
406 "GET",
407 scheme,
408 authority,
409 "/proxy/a%20b/v1/matches",
410 Some("x=%2F&x=one+two"),
411 b"",
412 )
413 .unwrap();
414 let digest = canonical
415 .signature_base(¶ms, &signed.integrity_token)
416 .client_data_hash();
417 assert_eq!(*provider.digests.lock().unwrap(), [digest.to_vec()]);
418 }
419 }
420 }
421
422 #[tokio::test]
423 async fn reconnects_prepare_new_key_bound_sessions_and_fresh_signatures() {
424 let provider = Arc::new(RotatingProvider::new(Platform::Android));
425 let url = "wss://verifier.example/v1/matches";
426 let first = sign_request(provider.clone(), request(url)).await.unwrap();
427 let second = sign_request(provider.clone(), request(url)).await.unwrap();
428 assert_eq!(provider.next_key.load(Ordering::Relaxed), 3);
429 assert_ne!(first.integrity_token, second.integrity_token);
430 assert_ne!(first.signature_input, second.signature_input);
431 assert_ne!(first.request_binding, second.request_binding);
432 let verifier = provider.verifier("https", "verifier.example");
433 for headers in [&first, &second] {
434 verifier
435 .verify(&received(headers, "GET", "/v1/matches"), b"")
436 .await
437 .unwrap();
438 }
439 let mut mixed = received(&second, "GET", "/v1/matches");
440 mixed
441 .headers
442 .insert("integrity-token", first.integrity_token.parse().unwrap());
443 assert_eq!(
444 verifier.verify(&mixed, b"").await.unwrap_err().reason,
445 RejectReason::SignatureInvalid
446 );
447 }
448
449 #[tokio::test]
450 async fn signatures_reject_changes_to_covered_request_components() {
451 let provider = Arc::new(RotatingProvider::new(Platform::Android));
452 let signed = sign_request(
453 provider.clone(),
454 request(&format!("wss://verifier.example{TARGET}")),
455 )
456 .await
457 .unwrap();
458 let verifier = provider.verifier("https", "verifier.example");
459 for (method, target, body) in [
460 ("POST", TARGET, b"".as_slice()),
461 (
462 "GET",
463 "/proxy/a%20b/v1/other?x=%2F&x=one+two",
464 b"".as_slice(),
465 ),
466 (
467 "GET",
468 "/proxy/a%20b/v1/matches?x=%2f&x=one+two",
469 b"".as_slice(),
470 ),
471 ("GET", TARGET, b"changed".as_slice()),
472 ] {
473 assert_eq!(
474 verifier
475 .verify(&received(&signed, method, target), body)
476 .await
477 .unwrap_err()
478 .reason,
479 RejectReason::SignatureInvalid
480 );
481 }
482 for (scheme, authority) in
483 [("wss", "verifier.example"), ("https", "other.example")]
484 {
485 assert_eq!(
486 provider
487 .verifier(scheme, authority)
488 .verify(&received(&signed, "GET", TARGET), b"")
489 .await
490 .unwrap_err()
491 .reason,
492 RejectReason::SignatureInvalid
493 );
494 }
495 }
496
497 struct FailingProvider(RequestIntegrityError);
499
500 #[async_trait::async_trait]
501 impl RequestIntegrityProvider for FailingProvider {
502 async fn prepare(
503 &self,
504 ) -> Result<RequestIntegritySession, RequestIntegrityError> {
505 Err(self.0)
506 }
507 }
508
509 #[tokio::test]
510 async fn preserves_provider_failure() {
511 assert!(matches!(
512 sign_request(
513 Arc::new(FailingProvider(RequestIntegrityError::Unavailable)),
514 request("wss://verifier.example/v1/matches")
515 )
516 .await,
517 Err(RequestIntegrityError::Unavailable)
518 ));
519 }
520
521 #[tokio::test]
523 async fn unexpected_callback_failures_are_neutral_and_do_not_expose_details() {
524 let error = RequestIntegrityError::from(
525 uniffi::UnexpectedUniFFICallbackError::new("private native diagnostic"),
526 );
527 assert_eq!(error, RequestIntegrityError::CallbackFailed);
528 assert_eq!(error.to_string(), "request integrity callback failed");
529 for provider in [
530 Arc::new(FailingProvider(error)) as Arc<dyn RequestIntegrityProvider>,
531 fixed_provider("opaque-token", Err(error)),
532 ] {
533 assert_eq!(
534 sign_request(provider, request("wss://verifier.example/v1/matches"))
535 .await,
536 Err(RequestIntegrityError::CallbackFailed),
537 );
538 }
539 }
540
541 struct PendingProvider;
542
543 #[async_trait::async_trait]
544 impl RequestIntegrityProvider for PendingProvider {
545 async fn prepare(
546 &self,
547 ) -> Result<RequestIntegritySession, RequestIntegrityError> {
548 std::future::pending().await
549 }
550 }
551
552 #[tokio::test(start_paused = true)]
553 async fn times_out_a_provider_that_never_returns() {
554 let started = tokio::time::Instant::now();
555 assert_eq!(
556 sign_request(
557 Arc::new(PendingProvider),
558 request("wss://verifier.example/v1/matches")
559 )
560 .await,
561 Err(RequestIntegrityError::TimedOut)
562 );
563 assert_eq!(started.elapsed(), AUTHENTICATION_TIMEOUT);
564 }
565
566 struct FixedProvider(RequestIntegritySession);
567
568 #[async_trait::async_trait]
569 impl RequestIntegrityProvider for FixedProvider {
570 async fn prepare(
571 &self,
572 ) -> Result<RequestIntegritySession, RequestIntegrityError> {
573 Ok(self.0.clone())
574 }
575 }
576
577 struct FixedSigner(Result<Vec<u8>, RequestIntegrityError>);
578
579 impl RequestDigestSigner for FixedSigner {
580 fn sign_digest(&self, _: Vec<u8>) -> Result<Vec<u8>, RequestIntegrityError> {
581 self.0.clone()
582 }
583 }
584
585 fn fixed_provider(
586 token: &str,
587 signature: Result<Vec<u8>, RequestIntegrityError>,
588 ) -> Arc<dyn RequestIntegrityProvider> {
589 Arc::new(FixedProvider(RequestIntegritySession {
590 token: token.to_string(),
591 platform: RequestIntegrityPlatform::Android,
592 signer: Arc::new(FixedSigner(signature)),
593 }))
594 }
595
596 #[tokio::test]
597 async fn rejects_unusable_tokens_before_signing() {
598 for token in ["", " ", "token\r\ninjected: value", "non-ascii-\u{00e9}"] {
599 assert!(matches!(
600 sign_request(
601 fixed_provider(token, Err(RequestIntegrityError::SigningFailed),),
602 request("wss://verifier.example/v1/matches")
603 )
604 .await,
605 Err(RequestIntegrityError::InvalidSession)
606 ));
607 }
608 }
609
610 #[tokio::test]
611 async fn preserves_signer_errors_and_rejects_empty_signatures() {
612 for (signature, expected) in [
613 (
614 Err(RequestIntegrityError::SigningFailed),
615 RequestIntegrityError::SigningFailed,
616 ),
617 (
618 Err(RequestIntegrityError::Unavailable),
619 RequestIntegrityError::Unavailable,
620 ),
621 (Ok(Vec::new()), RequestIntegrityError::SigningFailed),
622 ] {
623 assert!(matches!(
624 sign_request(fixed_provider("opaque-token", signature),
625 request("wss://verifier.example/v1/matches")).await,
626 Err(actual) if actual == expected
627 ));
628 }
629 }
630
631 struct BlockingSigner {
632 release: Arc<(Mutex<bool>, Condvar)>,
633 entered: Mutex<Option<tokio::sync::oneshot::Sender<()>>>,
634 finished: Mutex<Option<tokio::sync::oneshot::Sender<()>>>,
635 }
636
637 impl RequestDigestSigner for BlockingSigner {
638 fn sign_digest(&self, _: Vec<u8>) -> Result<Vec<u8>, RequestIntegrityError> {
639 self.entered
640 .lock()
641 .unwrap()
642 .take()
643 .unwrap()
644 .send(())
645 .unwrap();
646 let (released, condition) = &*self.release;
647 let _guard = condition
648 .wait_while(released.lock().unwrap(), |ready| !*ready)
649 .unwrap();
650 self.finished
651 .lock()
652 .unwrap()
653 .take()
654 .unwrap()
655 .send(())
656 .unwrap();
657 Ok(vec![1])
658 }
659 }
660
661 #[tokio::test(start_paused = true)]
662 async fn discards_a_native_signature_that_finishes_after_the_deadline() {
663 let release = Arc::new((Mutex::new(false), Condvar::new()));
664 let (entered_tx, entered_rx) = tokio::sync::oneshot::channel();
665 let (finished_tx, finished_rx) = tokio::sync::oneshot::channel();
666 let provider = Arc::new(FixedProvider(RequestIntegritySession {
667 token: "opaque-token".to_owned(),
668 platform: RequestIntegrityPlatform::Android,
669 signer: Arc::new(BlockingSigner {
670 release: release.clone(),
671 entered: Mutex::new(Some(entered_tx)),
672 finished: Mutex::new(Some(finished_tx)),
673 }),
674 }));
675 let slots = Arc::new(Semaphore::new(1));
676 let signing = tokio::spawn(sign_request_with_limit(
677 provider,
678 request("wss://verifier.example/v1/matches"),
679 slots.clone(),
680 ));
681 entered_rx.await.unwrap();
682 tokio::time::advance(AUTHENTICATION_TIMEOUT).await;
683 let result = signing.await.unwrap();
684 let queued = tokio::spawn(sign_request_with_limit(
685 fixed_provider("opaque-token", Ok(vec![1])),
686 request("wss://verifier.example/v1/matches"),
687 slots.clone(),
688 ));
689 tokio::task::yield_now().await;
690 tokio::time::advance(AUTHENTICATION_TIMEOUT).await;
691 let queued_result = queued.await.unwrap();
692 *release.0.lock().unwrap() = true;
693 release.1.notify_one();
694 finished_rx.await.unwrap();
695 assert_eq!(result, Err(RequestIntegrityError::TimedOut));
696 assert_eq!(queued_result, Err(RequestIntegrityError::TimedOut));
697 tokio::time::resume();
699 assert!(sign_request_with_limit(
700 fixed_provider("opaque-token", Ok(vec![1])),
701 request("wss://verifier.example/v1/matches"),
702 slots,
703 )
704 .await
705 .is_ok());
706 }
707}