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}