Skip to main content

walletkit_core/flamingo/
integrity.rs

1//! Native request-integrity callbacks used to authenticate Flamingo connections.
2
3use 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/// The platform determines the native signature encoding.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, uniffi::Enum)]
19pub enum RequestIntegrityPlatform {
20    /// An App Attest assertion encoded as CBOR.
21    Ios,
22    /// An Android Keystore ECDSA signature encoded as DER.
23    Android,
24}
25
26/// Request-integrity failures without token, key, or native diagnostic contents.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error, uniffi::Error)]
28pub enum RequestIntegrityError {
29    /// The host could not prepare an integrity session.
30    #[error("request integrity is unavailable")]
31    Unavailable,
32    /// The prepared session contains an unusable token.
33    #[error("request integrity session is invalid")]
34    InvalidSession,
35    /// The native signer failed or returned an empty signature.
36    #[error("request integrity signing failed")]
37    SigningFailed,
38    /// A host callback threw an unexpected exception.
39    #[error("request integrity callback failed")]
40    CallbackFailed,
41    /// Preparing or signing exceeded the authentication deadline.
42    #[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/// Signs a SHA-256 client-data digest with the key certified by the session token.
53///
54/// The host captures the key identifier and audience in this object. `WalletKit` never
55/// receives private-key material. The callback is invoked on a blocking worker.
56/// A running native signing operation cannot be cancelled when authentication times
57/// out; its late result is discarded. At most four native signing calls run process-wide;
58/// a timed-out call retains its slot until the native callback returns.
59#[uniffi::export(with_foreign)]
60pub trait RequestDigestSigner: Send + Sync {
61    /// Signs exactly 32 bytes and returns the platform's native signature encoding.
62    ///
63    /// iOS returns an App Attest assertion; Android returns a DER ECDSA signature.
64    /// The digest must be forwarded unchanged. Android must sign this precomputed hash,
65    /// without hashing it again; iOS passes it directly as App Attest's `clientDataHash`.
66    ///
67    /// # Errors
68    /// Returns [`RequestIntegrityError`] if native signing cannot complete.
69    fn sign_digest(
70        &self,
71        client_data_hash: Vec<u8>,
72    ) -> Result<Vec<u8>, RequestIntegrityError>;
73}
74
75/// A token and the signer pinned to the exact key certified by that token.
76///
77/// The provider must create the pair atomically so a later key rotation cannot mix them.
78/// An already-returned signer must keep using its captured key. The host adapts its native
79/// session to these callbacks without passing private-key material or other SDK callback types.
80#[derive(Clone, uniffi::Record)]
81pub struct RequestIntegritySession {
82    /// A valid integrity token for the host-selected Flamingo audience.
83    pub token: String,
84    /// Encoding expected from the native signer.
85    pub platform: RequestIntegrityPlatform,
86    /// A signer capturing the matching hardware key and native signing context.
87    pub signer: Arc<dyn RequestDigestSigner>,
88}
89
90/// Supplies authentication material before each WebSocket connection or reassignment.
91///
92/// The host owns token acquisition, refresh, audience policy, and hardware-key lifecycle.
93/// It selects the audience when configuring the provider; `WalletKit` does not decode the
94/// token or receive the audience or key identifier. Preparing and signing share a 30-second
95/// deadline in the matcher.
96#[uniffi::export(with_foreign)]
97#[async_trait::async_trait]
98pub trait RequestIntegrityProvider: Send + Sync {
99    /// Returns a usable token and its matching signer, reusing a cached token when valid.
100    ///
101    /// # Errors
102    /// Returns [`RequestIntegrityError`] if an integrity session is unavailable.
103    async fn prepare(&self) -> Result<RequestIntegritySession, RequestIntegrityError>;
104}
105
106const AUTHENTICATION_TIMEOUT: Duration = Duration::from_secs(30);
107
108/// Bounds native callbacks even when timed-out callers abandon their results.
109static SIGNING_SLOTS: LazyLock<Arc<Semaphore>> =
110    LazyLock::new(|| Arc::new(Semaphore::new(4)));
111
112/// Signs within a shared deadline and the process-wide native callback limit.
113pub(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
120/// Accepts an isolated limit so timeout regression tests do not share live signing slots.
121async 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    /// Verifies the exact upgrade emitted by the matcher, including encoded path and query values.
319    #[tokio::test]
320    #[allow(clippy::result_large_err)] // Tungstenite fixes the handshake callback's error type.
321    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(&params, &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    /// Returns a selected typed error from the provider callback.
498    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    /// Unexpected native diagnostics are hidden and preserve the same classification at either callback.
522    #[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        // The native worker releases its slot after returning, which can follow the signal.
698        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}