privy_rs/
keys.rs

1use std::{
2    future,
3    pin::Pin,
4    sync::{Arc, Mutex},
5};
6
7use futures::{Stream, StreamExt};
8use p256::{
9    ecdsa::{Signature, SigningKey, signature::hazmat::PrehashSigner},
10    elliptic_curve::SecretKey,
11};
12
13use crate::{KeyError, SigningError};
14
15const SIGNATURE_RESOLUTION_CONCURRENCY: usize = 10;
16
17/// A context for signing messages. Any keys added to the context will be
18/// automatically added to the list of signatories for requests to the Privy API
19/// that require authorization.
20///
21/// The context accepts anything that implements `IntoSignature`, which by
22/// extension includes anything that implements `IntoKey`. This allows you to
23/// create a context that includes keys from a variety of sources, such as
24/// files, JWTs, or KMS services.
25///
26/// For usage information, see the `AuthorizationContext::sign` and
27/// `AuthorizationContext::push` methods.
28///
29/// This struct is thread-safe, and can be cloned. It synchronizes access to the
30/// underlying store internally.
31#[derive(Clone)]
32pub struct AuthorizationContext {
33    signers: Arc<Mutex<Vec<Arc<dyn IntoSignatureBoxed + Send + Sync>>>>,
34    resolution_concurrency: usize,
35}
36
37impl std::fmt::Debug for AuthorizationContext {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        f.debug_struct("AuthorizationContext").finish()
40    }
41}
42
43impl Default for AuthorizationContext {
44    fn default() -> Self {
45        Self::new()
46    }
47}
48
49impl AuthorizationContext {
50    /// Create a new `AuthorizationContext` with the default concurrency.
51    #[must_use]
52    pub fn new() -> Self {
53        Self {
54            signers: Default::default(),
55            resolution_concurrency: SIGNATURE_RESOLUTION_CONCURRENCY,
56        }
57    }
58
59    /// Push a new credential source into the context. This supports
60    /// anything that implements `IntoSignature`, which includes
61    /// anything that implements `IntoKey`.
62    ///
63    /// In the following example, we create a `JwtUser` source which
64    /// will transparently perform authorization with privy to get
65    /// a key, and then sign the message with that key. We also
66    /// add a `PrivateKey` source which you can set yourself.
67    ///
68    /// ```rust
69    /// # use privy_rs::{AuthorizationContext, JwtUser, IntoSignature, PrivateKey, PrivyClient};
70    /// # use p256::ecdsa::signature::SignerMut;
71    /// # use p256::ecdsa::Signature;
72    /// # use p256::elliptic_curve::SecretKey;
73    /// # use std::time::Duration;
74    /// # use std::sync::Arc;
75    /// # async fn foo() {
76    /// let privy = PrivyClient::new("app_id".to_string(), "app_secret".to_string()).unwrap();
77    /// let jwt = JwtUser(privy, "test".to_string());
78    /// let key = PrivateKey("test".to_string());
79    /// let context = AuthorizationContext::new().push(jwt).push(key);
80    /// # }
81    /// ```
82    pub fn push<T: IntoSignature + 'static + Send + Sync>(self, key: T) -> Self {
83        self.signers
84            .lock()
85            .expect("lock poisoned")
86            .push(Arc::new(key));
87        self
88    }
89
90    /// Sign a message with all the keys in the context.
91    /// This produces a stream which yields values as they
92    /// become available. You can collect it into a vec.
93    /// This function will resolve all signatures concurrently,
94    /// according to the policy set in `AuthorizationContext`.
95    ///
96    /// ```rust
97    /// # use privy_rs::{AuthorizationContext, JwtUser, IntoSignature, PrivyClient};
98    /// # use p256::ecdsa::signature::SignerMut;
99    /// # use p256::ecdsa::Signature;
100    /// # use p256::elliptic_curve::SecretKey;
101    /// # use std::time::Duration;
102    /// # use std::sync::Arc;
103    /// # use futures::stream::StreamExt;
104    /// # async fn foo() {
105    /// let privy = PrivyClient::new("app_id".to_string(), "app_secret".to_string()).unwrap();
106    /// let jwt = JwtUser(privy, "test".to_string());
107    /// let context = AuthorizationContext::new().push(jwt);
108    /// let key = context.sign(&[0, 1, 2, 3]).collect::<Vec<_>>().await;
109    /// assert_eq!(key.len(), 1);
110    /// # }
111    /// ```
112    ///
113    /// You can also use `try_collect` to get a `Result<Vec<_>, Error>`,
114    /// or any other combinators on the `StreamExt` and `TryStreamExt` traits.
115    ///
116    /// ```rust
117    /// # use privy_rs::{AuthorizationContext, JwtUser, IntoSignature, PrivyClient};
118    /// # use p256::ecdsa::signature::SignerMut;
119    /// # use p256::ecdsa::Signature;
120    /// # use p256::elliptic_curve::SecretKey;
121    /// # use std::time::Duration;
122    /// # use std::sync::Arc;
123    /// # use futures::stream::TryStreamExt;
124    /// # async fn foo() {
125    /// let privy = PrivyClient::new("app_id".to_string(), "app_secret".to_string()).unwrap();
126    /// let jwt = JwtUser(privy, "test".to_string());
127    /// let context = AuthorizationContext::new().push(jwt);
128    /// let key = context.sign(&[0, 1, 2, 3]).try_collect::<Vec<_>>().await;
129    /// assert!(key.map(|v| v.len() == 1).unwrap_or(false));
130    /// # }
131    /// ```
132    pub fn sign<'a>(
133        &'a self,
134        message: &'a [u8],
135    ) -> impl Stream<Item = Result<Signature, SigningError>> + 'a {
136        // we clone the inner vector before signing so we don't need to hold the lock.
137        // cloning this vector will also clone the inner items, which are reference counted
138        let keys = self.signers.lock().expect("lock poisoned").clone();
139
140        futures::stream::iter(keys)
141            .map(move |key| {
142                let key = key.clone();
143                // this is some awkwardness in rust's type system.
144                // we need communicate to the type system we want to
145                // move the key, clone it, then move both the key and
146                // message into an async closure. later versions of
147                // rust may allow us to be less explicit here
148                async move { key.sign_boxed(message).await }
149            })
150            // await multiple `sign_boxed` futures concurrently,
151            // returning them in order of completion
152            .buffer_unordered(self.resolution_concurrency)
153    }
154
155    /// Exercise the signing mechanism to validate that all keys
156    /// are valid and can produce signatures. Returns a vector
157    /// of errors. An empty vector indicates that all keys are
158    /// valid.
159    ///
160    /// ```
161    /// # use privy_rs::{AuthorizationContext, JwtUser, IntoSignature, PrivyClient};
162    /// # use p256::ecdsa::signature::SignerMut;
163    /// # use p256::ecdsa::Signature;
164    /// # use p256::elliptic_curve::SecretKey;
165    /// # use std::time::Duration;
166    /// # use std::sync::Arc;
167    /// # async fn foo() {
168    /// let privy = PrivyClient::new("app_id".to_string(), "app_secret".to_string()).unwrap();
169    /// let jwt = JwtUser(privy, "test".to_string());
170    /// let key = SecretKey::<p256::NistP256>::from_sec1_pem(&"test".to_string()).unwrap();
171    /// let context = AuthorizationContext::new().push(jwt).push(key);
172    /// let errors = context.validate().await;
173    /// assert!(errors.is_empty());
174    /// # }
175    /// ```
176    pub async fn validate(&self) -> Vec<SigningError> {
177        self.sign(&[])
178            .filter_map(|r| future::ready(r.err())) // filter_map expects a future
179            .collect::<Vec<_>>()
180            .await
181    }
182}
183
184type Key = SecretKey<p256::NistP256>;
185
186/// A trait for getting a key from a source. See `IntoKey::get_key` for more details.
187pub trait IntoKey {
188    /// Get a key from the `IntoKey` source.
189    fn get_key(&self) -> impl Future<Output = Result<Key, KeyError>> + Send;
190}
191
192/// A trait for signing messages. See `IntoSignature::sign` for more details.
193pub trait IntoSignature {
194    /// Sign a message using deterministic ECDSA.
195    ///
196    /// This method implements a two-step signing process that ensures compatibility
197    /// with Privy's API signature verification:
198    ///
199    /// ## Process Overview
200    ///
201    /// 1. **Message Hashing**: The input message is hashed using SHA-256 to produce
202    ///    a 32-byte digest. This follows the standard practice of hashing messages
203    ///    before signing to ensure security and compatibility.
204    ///
205    /// 2. **Deterministic ECDSA Signing**: The hash is signed using ECDSA P-256
206    ///    with RFC 6979 deterministic k-value generation. This ensures that the
207    ///    same message will always produce the same signature when signed with
208    ///    the same private key.
209    ///
210    /// ## Why Deterministic Signing?
211    ///
212    /// Traditional ECDSA uses random k-values during signature generation, which
213    /// means the same message signed with the same key can produce different valid
214    /// signatures. However, Privy's API expects deterministic signatures.
215    ///
216    /// By using RFC 6979 deterministic k-value generation, we ensure:
217    /// - **Reproducibility**: Same input always produces same signature
218    /// - **Security**: RFC 6979 provides cryptographically secure k-values
219    /// - **Consistency**: Matches the behavior of Privy's other SDKs
220    ///
221    /// ## Example Usage
222    ///
223    /// ```rust
224    /// use std::path::PathBuf;
225    ///
226    /// use privy_rs::{IntoSignature, PrivateKey};
227    ///
228    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
229    /// # let my_key = include_str!("../tests/test_private_key.pem").to_string();
230    /// let key_source = PrivateKey(my_key);
231    /// let message = b"canonical request data";
232    /// let signature = key_source.sign(message).await?;
233    ///
234    /// // The signature is deterministic - signing the same message again
235    /// // with the same key will produce identical results
236    /// let signature2 = key_source.sign(message).await?;
237    /// assert_eq!(signature, signature2);
238    /// # Ok(())
239    /// # }
240    /// ```
241    fn sign(&self, message: &[u8]) -> impl Future<Output = Result<Signature, SigningError>> + Send;
242}
243
244// this is a blanket implementation for all types that implement IntoKey.
245// we can simply call get_key on T (since it implements IntoKey) and
246// then call sign on that to get the signature. having this means
247// that AuthorizationContext can be used with any type that implements
248// IntoKey, including things like JwtUser, PrivateKey, and PrivateKeyFromFile.
249impl<T> IntoSignature for T
250where
251    T: IntoKey + Sync,
252{
253    async fn sign(&self, message: &[u8]) -> Result<Signature, SigningError> {
254        let key = self.get_key().await?;
255        key.sign(message).await
256    }
257}
258
259/// Rust has a concept of 'object safety' and `IntoSignature` is not object safe,
260/// meaning it can not be used in `AuthorizationContext` directly. This is because
261/// IntoSignature's return type can differ in size depending on the type of the
262/// implementor.
263///
264/// What we can do, however, is provide a trait that _is_ object safe, and to blanket
265/// impl `IntoSignatureBoxed` for all types that implement `IntoSignature`.
266/// IntoSignatureBoxed returns a boxed future instead, which is object safe. If you
267/// are familiar with `async_trait`, this is how it works under the hood, and how all
268/// rust traits worked until GAT / RPITIT landed.
269///
270/// NOTE: this is a private implementation detail and will never leak to the public API.
271trait IntoSignatureBoxed {
272    fn sign_boxed<'a>(
273        &'a self,
274        message: &'a [u8],
275    ) -> Pin<Box<dyn Future<Output = Result<Signature, SigningError>> + Send + 'a>>;
276}
277
278// the blanket impl referenced above
279impl<T: IntoSignature + 'static> IntoSignatureBoxed for T {
280    fn sign_boxed<'a>(
281        &'a self,
282        message: &'a [u8],
283    ) -> Pin<Box<dyn Future<Output = Result<Signature, SigningError>> + Send + 'a>> {
284        Box::pin(self.sign(message))
285    }
286}
287
288/// A wrapper for a closure that implements `IntoSignature`.
289/// This uses the newtype pattern to avoid conflicting blanket impls.
290pub struct FnSigner<F>(pub F);
291
292/// A wrapper for a closure that implements `IntoKey`.
293/// This uses the newtype pattern to avoid conflicting blanket impls.
294pub struct FnKey<F>(pub F);
295
296/// Blanket implementation for the FnSigner wrapper.
297impl<F, Fut> IntoSignature for FnSigner<F>
298where
299    F: Fn(&[u8]) -> Fut,
300    Fut: Future<Output = Result<Signature, SigningError>> + Send,
301{
302    fn sign(&self, message: &[u8]) -> impl Future<Output = Result<Signature, SigningError>> + Send {
303        (self.0)(message)
304    }
305}
306
307/// Blanket implementation for the FnKey wrapper.
308impl<F, Fut> IntoKey for FnKey<F>
309where
310    F: Fn() -> Fut,
311    Fut: Future<Output = Result<Key, KeyError>> + Send,
312{
313    fn get_key(&self) -> impl Future<Output = Result<Key, KeyError>> + Send {
314        (self.0)()
315    }
316}
317
318/// A key that is sourced from the user identified by the provided JWT.
319///
320/// This is used in JWT-based authentication. When attempting to sign,
321/// the JWT is used to retrieve the user's key from the Privy API.
322///
323/// # Errors
324/// This provider can fail if the JWT is invalid, does not match a user,
325/// or if the API returns an error.
326pub struct JwtUser(pub crate::PrivyClient, pub String);
327
328impl IntoKey for JwtUser {
329    async fn get_key(&self) -> Result<Key, KeyError> {
330        self.0
331            .jwt_exchange
332            .exchange_jwt_for_authorization_key(self)
333            .await
334    }
335}
336
337impl IntoSignature for Key {
338    async fn sign(&self, message: &[u8]) -> Result<Signature, SigningError> {
339        use sha2::{Digest, Sha256};
340
341        tracing::debug!(
342            "Starting ECDSA signing process for {} byte message",
343            message.len()
344        );
345
346        // First hash the message with SHA256
347        let hashed = {
348            let mut sha256 = Sha256::new();
349            sha256.update(message);
350            sha256.finalize()
351        };
352
353        tracing::debug!("SHA256 hash computed: {}", hex::encode(hashed));
354
355        // Sign the hash using deterministic signing (RFC 6979)
356        let signing_key = SigningKey::from(self.clone());
357
358        // Use deterministic prehash signing to ensure consistent signatures
359        let signature: Signature = signing_key.sign_prehash(&hashed)?;
360
361        tracing::debug!("ECDSA signature generated using deterministic RFC 6979");
362
363        Ok(signature)
364    }
365}
366
367impl IntoSignature for Signature {
368    async fn sign(&self, _message: &[u8]) -> Result<Signature, SigningError> {
369        Ok(*self)
370    }
371}
372
373/// A raw private key in SEC1 PEM format.
374///
375/// # Errors
376/// This provider can fail if the key is not in the expected format.
377pub struct PrivateKey(pub String);
378
379impl IntoKey for PrivateKey {
380    async fn get_key(&self) -> Result<Key, KeyError> {
381        SecretKey::<p256::NistP256>::from_sec1_pem(&self.0).map_err(|e| {
382            tracing::error!("Failed to parse SEC1 PEM: {:?}", e);
383            KeyError::InvalidFormat(self.0.clone())
384        })
385    }
386}
387
388#[cfg(test)]
389mod tests {
390    use base64::{Engine, engine::general_purpose::STANDARD};
391    use futures::TryStreamExt;
392    use p256::{
393        ecdsa::Signature,
394        elliptic_curve::{SecretKey, generic_array::GenericArray},
395    };
396    use test_case::test_case;
397    use tracing_test::traced_test;
398
399    use super::*;
400    use crate::{AuthorizationContext, FnKey, FnSigner, KeyError};
401
402    // generated using `mise gen-p256-key`
403    const TEST_PRIVATE_KEY_PEM: &str = include_str!("../tests/test_private_key.pem");
404
405    // PrivateKey tests
406    #[tokio::test]
407    async fn test_private_key_creation() {
408        let key = PrivateKey(TEST_PRIVATE_KEY_PEM.to_string());
409        let result = key.get_key().await;
410        assert!(result.is_ok(), "Should successfully parse valid PEM key");
411    }
412
413    #[tokio::test]
414    async fn test_private_key_invalid_format() {
415        let key = PrivateKey("invalid_pem_data".to_string());
416        let result = key.get_key().await;
417        assert!(result.is_err(), "Should fail with invalid PEM data");
418
419        if let Err(KeyError::InvalidFormat(_)) = result {
420            // Expected error type
421        } else {
422            panic!("Expected InvalidFormat error");
423        }
424    }
425
426    #[tokio::test]
427    async fn test_private_key_signing() {
428        let key = PrivateKey(TEST_PRIVATE_KEY_PEM.to_string());
429        let test_key = key.get_key().await.unwrap();
430
431        let message1 = b"test message for signing";
432        let message2 = b"different message";
433
434        // Test deterministic signing - same message should produce same signature
435        let signature1a = test_key.sign(message1).await.unwrap();
436        let signature1b = test_key.sign(message1).await.unwrap();
437        assert_eq!(
438            signature1a, signature1b,
439            "Deterministic signing should produce identical signatures"
440        );
441
442        // Test different messages produce different signatures
443        let signature2 = test_key.sign(message2).await.unwrap();
444        assert_ne!(
445            signature1a, signature2,
446            "Different messages should produce different signatures"
447        );
448    }
449
450    // Message signing tests with various inputs
451    #[test_case(b"" ; "empty message")]
452    #[test_case(b"short" ; "short message")]
453    #[test_case(&[0u8; 1000] ; "long message")]
454    #[test_case(b"special chars: \x00\xff\n\r\t" ; "special characters")]
455    #[tokio::test]
456    async fn test_signing_various_messages(message: &[u8]) {
457        let key = PrivateKey(TEST_PRIVATE_KEY_PEM.to_string());
458        let test_key = key.get_key().await.unwrap();
459
460        let signature = test_key.sign(message).await;
461        assert!(
462            signature.is_ok(),
463            "Should successfully sign message of length {}",
464            message.len()
465        );
466    }
467
468    #[tokio::test]
469    async fn test_signature_into_signature() {
470        // Create a known signature
471        let key = PrivateKey(TEST_PRIVATE_KEY_PEM.to_string());
472        let test_key = key.get_key().await.unwrap();
473        let original_signature = test_key.sign(b"test").await.unwrap();
474
475        // Use the signature as an IntoSignature source
476        let result = original_signature.sign(b"ignored_message").await.unwrap();
477        assert_eq!(
478            result, original_signature,
479            "Signature should return itself regardless of message"
480        );
481    }
482
483    // AuthorizationContext tests
484    #[tokio::test]
485    #[traced_test]
486    async fn test_authorization_context_empty() {
487        let ctx = AuthorizationContext::new();
488        let signatures: Vec<_> = ctx.sign(b"test").try_collect().await.unwrap();
489        assert!(
490            signatures.is_empty(),
491            "Empty context should produce no signatures"
492        );
493    }
494
495    #[tokio::test]
496    #[traced_test]
497    async fn test_authorization_context_single_key() {
498        let key = PrivateKey(TEST_PRIVATE_KEY_PEM.to_string());
499        let ctx = AuthorizationContext::new().push(key);
500
501        let signatures: Vec<_> = ctx.sign(b"test").try_collect().await.unwrap();
502        assert_eq!(
503            signatures.len(),
504            1,
505            "Context with one key should produce one signature"
506        );
507    }
508
509    #[tokio::test]
510    #[traced_test]
511    async fn test_authorization_context_multiple_keys() {
512        // Create another deterministic key for testing
513        let key_bytes = [2u8; 32]; // Different from test key
514        let second_key = SecretKey::<p256::NistP256>::from_bytes(&key_bytes.into()).unwrap();
515
516        // Add multiple keys
517        let ctx = AuthorizationContext::new()
518            .push(PrivateKey(TEST_PRIVATE_KEY_PEM.to_string()))
519            .push(second_key);
520
521        let signatures: Vec<_> = ctx.sign(b"test").try_collect().await.unwrap();
522        assert_eq!(
523            signatures.len(),
524            2,
525            "Context with two keys should produce two signatures"
526        );
527
528        // Signatures should be different (different keys)
529        assert_ne!(
530            signatures[0], signatures[1],
531            "Different keys should produce different signatures"
532        );
533    }
534
535    #[tokio::test]
536    #[traced_test]
537    async fn test_authorization_context_validation() {
538        // Test successful validation
539        let ctx = AuthorizationContext::new().push(PrivateKey(TEST_PRIVATE_KEY_PEM.to_string()));
540        let errors = ctx.validate().await;
541        assert!(
542            errors.is_empty(),
543            "Valid context should have no validation errors"
544        );
545
546        // Test validation failure
547        let ctx2 = AuthorizationContext::new().push(PrivateKey("invalid_key_data".to_string()));
548        let errors2 = ctx2.validate().await;
549        assert!(
550            !errors2.is_empty(),
551            "Invalid key should produce validation errors"
552        );
553    }
554
555    // Function wrapper tests
556    #[tokio::test]
557    async fn test_fn_signer_wrapper() {
558        use crate::SigningError;
559
560        #[derive(Debug)]
561        struct DummyError;
562        impl std::error::Error for DummyError {}
563        impl std::fmt::Display for DummyError {
564            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
565                write!(f, "dummy error")
566            }
567        }
568
569        // Test that FnSigner struct exists and can be constructed
570        let _signer = FnSigner(|_message: &[u8]| async move {
571            // Mock function - return error for simplicity
572            let result: Result<Signature, SigningError> =
573                Err(SigningError::Other(Box::new(DummyError)));
574            result
575        });
576
577        assert!(matches!(
578            _signer.sign(&[0]).await,
579            Err(SigningError::Other(_))
580        ));
581    }
582
583    #[tokio::test]
584    async fn test_fn_key_wrapper() {
585        let key_fn =
586            FnKey(|| async { PrivateKey(TEST_PRIVATE_KEY_PEM.to_string()).get_key().await });
587
588        let key1 = key_fn.get_key().await.unwrap();
589        let key2 = key_fn.get_key().await.unwrap();
590
591        // Keys should be the same (same source)
592        assert_eq!(key1.to_bytes(), key2.to_bytes());
593    }
594
595    #[tokio::test]
596    #[traced_test]
597    async fn test_authorization_context_concurrent_signing() {
598        let mut ctx = AuthorizationContext::new();
599
600        // Add multiple keys for concurrent testing
601        for i in 0..5 {
602            // Create deterministic keys for testing
603            let mut key_bytes = [1u8; 32];
604            key_bytes[0] = i as u8 + 1; // Make each key different
605            let key = SecretKey::<p256::NistP256>::from_bytes(&key_bytes.into()).unwrap();
606            ctx = ctx.push(key);
607        }
608
609        let message = b"concurrent test message";
610        let signatures: Vec<_> = ctx.sign(message).try_collect().await.unwrap();
611
612        assert_eq!(
613            signatures.len(),
614            5,
615            "Should produce 5 signatures concurrently"
616        );
617
618        // All signatures should be different (different keys)
619        for i in 0..signatures.len() {
620            for j in (i + 1)..signatures.len() {
621                assert_ne!(
622                    signatures[i], signatures[j],
623                    "Signatures from different keys should be different"
624                );
625            }
626        }
627    }
628
629    #[tokio::test]
630    async fn test_key_public_key_derivation() {
631        let private_key = PrivateKey(TEST_PRIVATE_KEY_PEM.to_string());
632        let key = private_key.get_key().await.unwrap();
633        let public_key = key.public_key();
634
635        // Should be able to derive public key without error
636        assert!(
637            !public_key.to_string().is_empty(),
638            "Public key string should not be empty"
639        );
640
641        // Public key should be consistent
642        let public_key2 = key.public_key();
643        assert_eq!(
644            public_key.to_string(),
645            public_key2.to_string(),
646            "Public key derivation should be consistent"
647        );
648    }
649
650    // Legacy compatibility test
651    #[tokio::test]
652    #[traced_test]
653    async fn test_authorization_context_mixed_sources() {
654        // Add path-based key and pre-computed signature
655        let ctx = AuthorizationContext::new()
656            .push(PrivateKey(
657                include_str!("../tests/test_private_key.pem").to_string(),
658            ))
659            .push(Signature::from_bytes(GenericArray::from_slice(&STANDARD.decode("J7GLk/CIqvCNCOSJ8sUZb0rCsqWF9l1H1VgYfsAd1ew2uBJHE5hoY+kV7CSzdKkgOhtdvzj22gXA7gcn5gSqvQ==").unwrap())).expect("right size"));
660
661        let sigs = ctx
662            .sign(&[0, 1, 2, 3])
663            .try_collect::<Vec<_>>()
664            .await
665            .expect("passes");
666
667        assert!(
668            !sigs.is_empty(),
669            "Context with mixed sources should produce signatures"
670        );
671    }
672
673    // Error handling tests
674    #[tokio::test]
675    async fn test_signing_error_propagation() {
676        struct FailingKey;
677
678        #[derive(Debug)]
679        struct DummyError;
680        impl std::error::Error for DummyError {}
681        impl std::fmt::Display for DummyError {
682            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
683                write!(f, "dummy error")
684            }
685        }
686
687        impl IntoKey for FailingKey {
688            async fn get_key(&self) -> Result<SecretKey<p256::NistP256>, KeyError> {
689                Err(KeyError::Other(Box::new(DummyError)))
690            }
691        }
692
693        let failing_key = FailingKey;
694        let result = failing_key.sign(b"test").await;
695        assert!(matches!(result, Err(SigningError::Key(KeyError::Other(_)))));
696    }
697
698    #[tokio::test]
699    async fn test_authorization_context_clone_and_debug() {
700        let ctx1 = AuthorizationContext::new().push(PrivateKey(TEST_PRIVATE_KEY_PEM.to_string()));
701
702        // Test clone functionality
703        let ctx2 = ctx1.clone();
704        let sigs1: Vec<_> = ctx1.sign(b"test").try_collect().await.unwrap();
705        let sigs2: Vec<_> = ctx2.sign(b"test").try_collect().await.unwrap();
706        assert_eq!(sigs1.len(), 1);
707        assert_eq!(sigs2.len(), 1);
708        assert_eq!(
709            sigs1[0], sigs2[0],
710            "Cloned context should produce same signatures"
711        );
712
713        // Test debug output
714        let debug_str = format!("{ctx1:?}");
715        assert!(
716            debug_str.contains("AuthorizationContext"),
717            "Debug output should contain struct name"
718        );
719    }
720}