Development Documentation (main branch) - For stable release docs, see docs.rs/eidetica
Skip to main content

eidetica/auth/
crypto.rs

1//! Cryptographic operations for Eidetica authentication
2//!
3//! This module provides signature generation and verification for authenticating
4//! entries in the database. The `PublicKey` and `PrivateKey` enums enable
5//! crypto-agility by dispatching to algorithm-specific implementations.
6
7use base64ct::{Base64, Encoding};
8use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
9use rand::Rng;
10use rand::rngs::OsRng;
11use serde::{Deserialize, Serialize};
12use zeroize::{ZeroizeOnDrop, Zeroizing};
13
14use super::errors::AuthError;
15use crate::{Entry, Error};
16
17/// Size of Ed25519 public keys in bytes
18pub const ED25519_PUBLIC_KEY_SIZE: usize = 32;
19
20/// Size of Ed25519 private keys in bytes
21pub const ED25519_PRIVATE_KEY_SIZE: usize = 32;
22
23/// Size of Ed25519 signatures in bytes
24pub const ED25519_SIGNATURE_SIZE: usize = 64;
25
26/// Size of authentication challenges in bytes
27pub const CHALLENGE_SIZE: usize = 32;
28
29// ==================== Algorithm-Agnostic Key Types ====================
30
31/// Algorithm-agnostic public key for signature verification.
32///
33/// Wraps algorithm-specific verifying key types in a single enum,
34/// enabling crypto-agility while maintaining zero-cost dispatch for
35/// the common Ed25519 case.
36#[non_exhaustive]
37#[derive(Debug, Clone, PartialEq, Eq, Hash)]
38pub enum PublicKey {
39    /// Ed25519 public key (32 bytes)
40    Ed25519(VerifyingKey),
41}
42
43impl PublicKey {
44    /// Verify a signature over the given data.
45    ///
46    /// Returns `Ok(())` if the signature is valid, or `Err` if verification
47    /// fails for any reason (malformed signature, wrong key, etc.).
48    pub fn verify(&self, data: &[u8], signature: &[u8]) -> Result<(), AuthError> {
49        match self {
50            PublicKey::Ed25519(key) => {
51                let sig_array: [u8; ED25519_SIGNATURE_SIZE] = signature
52                    .try_into()
53                    .map_err(|_| AuthError::InvalidSignature)?;
54                let sig = Signature::from_bytes(&sig_array);
55                key.verify(data, &sig)
56                    .map_err(|_| AuthError::InvalidSignature)
57            }
58        }
59    }
60
61    /// Format the public key as a prefixed string (e.g. `"ed25519:base64..."`).
62    pub fn to_prefixed_string(&self) -> String {
63        match self {
64            PublicKey::Ed25519(key) => {
65                let encoded = Base64::encode_string(&key.to_bytes());
66                format!("ed25519:{encoded}")
67            }
68        }
69    }
70
71    /// Parse a public key from a prefixed string (e.g. `"ed25519:base64..."`).
72    pub fn from_prefixed_string(s: &str) -> Result<Self, AuthError> {
73        let (prefix, key_data) = s
74            .split_once(':')
75            .ok_or_else(|| AuthError::InvalidKeyFormat {
76                reason: "Expected 'algorithm:key' format".to_string(),
77            })?;
78        match prefix {
79            "ed25519" => {
80                let key_bytes =
81                    Base64::decode_vec(key_data).map_err(|e| AuthError::InvalidKeyFormat {
82                        reason: format!("Invalid base64 for key: {e}"),
83                    })?;
84                let key_array: [u8; ED25519_PUBLIC_KEY_SIZE] = key_bytes.try_into().map_err(
85                    |v: Vec<u8>| AuthError::InvalidKeyFormat {
86                        reason: format!(
87                            "Ed25519 public key must be {ED25519_PUBLIC_KEY_SIZE} bytes, got {}",
88                            v.len()
89                        ),
90                    },
91                )?;
92                let verifying_key = VerifyingKey::from_bytes(&key_array).map_err(|e| {
93                    AuthError::KeyParsingFailed {
94                        reason: e.to_string(),
95                    }
96                })?;
97                Ok(PublicKey::Ed25519(verifying_key))
98            }
99            _ => Err(AuthError::InvalidKeyFormat {
100                reason: format!("Unknown key algorithm prefix: '{prefix}'"),
101            }),
102        }
103    }
104
105    /// Generate a random public key.
106    ///
107    /// Creates a fresh Ed25519 keypair and returns only the public key,
108    /// discarding the private key.
109    pub fn random() -> Self {
110        PrivateKey::generate().public_key()
111    }
112
113    /// Get the algorithm name for this key.
114    pub fn algorithm(&self) -> &'static str {
115        match self {
116            PublicKey::Ed25519(_) => "ed25519",
117        }
118    }
119}
120
121impl std::fmt::Display for PublicKey {
122    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123        write!(f, "{}", self.to_prefixed_string())
124    }
125}
126
127/// Serializes as the prefixed string format (e.g. `"ed25519:base64..."`).
128impl Serialize for PublicKey {
129    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
130        serializer.serialize_str(&self.to_prefixed_string())
131    }
132}
133
134/// Deserializes from the prefixed string format (e.g. `"ed25519:base64..."`).
135impl<'de> Deserialize<'de> for PublicKey {
136    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
137        let s = String::deserialize(deserializer)?;
138        PublicKey::from_prefixed_string(&s).map_err(serde::de::Error::custom)
139    }
140}
141
142/// Algorithm-agnostic signing key for creating signatures.
143///
144/// Wraps algorithm-specific signing key types in a single enum.
145/// Secret material is volatile-zeroed on drop via the inner key types'
146/// [`ZeroizeOnDrop`] implementations.
147#[non_exhaustive]
148#[derive(Clone)]
149pub enum PrivateKey {
150    /// Ed25519 signing key (32 bytes)
151    Ed25519(SigningKey),
152}
153
154impl std::fmt::Debug for PrivateKey {
155    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156        match self {
157            PrivateKey::Ed25519(_) => f.write_str("PrivateKey::Ed25519([REDACTED])"),
158        }
159    }
160}
161
162impl PrivateKey {
163    /// Sign the given data and return the raw signature bytes.
164    pub fn sign(&self, data: &[u8]) -> Vec<u8> {
165        match self {
166            PrivateKey::Ed25519(key) => {
167                let signature: Signature = key.sign(data);
168                signature.to_bytes().to_vec()
169            }
170        }
171    }
172
173    /// Derive the corresponding public key.
174    pub fn public_key(&self) -> PublicKey {
175        match self {
176            PrivateKey::Ed25519(key) => PublicKey::Ed25519(key.verifying_key()),
177        }
178    }
179
180    /// Generate a new key using the default algorithm (Ed25519).
181    pub fn generate() -> Self {
182        PrivateKey::Ed25519(SigningKey::generate(&mut OsRng))
183    }
184
185    /// Export the raw key bytes (for encryption/storage).
186    ///
187    /// The returned buffer is wrapped in [`Zeroizing`] so the key material
188    /// is automatically cleared from memory when dropped.
189    pub fn to_bytes(&self) -> Zeroizing<Vec<u8>> {
190        match self {
191            PrivateKey::Ed25519(key) => Zeroizing::new(key.to_bytes().to_vec()),
192        }
193    }
194
195    /// Reconstruct a private key from raw bytes and an algorithm identifier.
196    pub fn from_bytes(algorithm: &str, bytes: &[u8]) -> Result<Self, AuthError> {
197        match algorithm {
198            "ed25519" => {
199                let key_array: [u8; ED25519_PRIVATE_KEY_SIZE] =
200                    bytes.try_into().map_err(|_| AuthError::InvalidKeyFormat {
201                        reason: format!(
202                            "Ed25519 private key must be {ED25519_PRIVATE_KEY_SIZE} bytes, got {}",
203                            bytes.len()
204                        ),
205                    })?;
206                Ok(PrivateKey::Ed25519(SigningKey::from_bytes(&key_array)))
207            }
208            _ => Err(AuthError::InvalidKeyFormat {
209                reason: format!("Unknown key algorithm: {algorithm}"),
210            }),
211        }
212    }
213
214    /// Format the private key as a prefixed string (e.g. `"ed25519:base64..."`).
215    ///
216    /// The returned string is wrapped in [`Zeroizing`] so the key material
217    /// is automatically cleared from memory when dropped.
218    pub fn to_prefixed_string(&self) -> Zeroizing<String> {
219        let bytes = self.to_bytes();
220        let encoded = Base64::encode_string(&bytes);
221        Zeroizing::new(format!("{}:{encoded}", self.algorithm()))
222    }
223
224    /// Parse a private key from a prefixed string (e.g. `"ed25519:base64..."`).
225    pub fn from_prefixed_string(s: &str) -> Result<Self, AuthError> {
226        let (prefix, key_data) = s
227            .split_once(':')
228            .ok_or_else(|| AuthError::InvalidKeyFormat {
229                reason: "Expected 'algorithm:key' format".to_string(),
230            })?;
231        match prefix {
232            "ed25519" => {
233                let key_bytes =
234                    Base64::decode_vec(key_data).map_err(|e| AuthError::InvalidKeyFormat {
235                        reason: format!("Invalid base64 for key: {e}"),
236                    })?;
237                Self::from_bytes("ed25519", &key_bytes)
238            }
239            _ => Err(AuthError::InvalidKeyFormat {
240                reason: format!("Unknown key algorithm prefix: '{prefix}'"),
241            }),
242        }
243    }
244
245    /// Get the algorithm name for this key.
246    pub fn algorithm(&self) -> &'static str {
247        match self {
248            PrivateKey::Ed25519(_) => "ed25519",
249        }
250    }
251}
252
253/// Zeroization is handled by the inner key types' `Drop` impls.
254/// For Ed25519, the `zeroize` feature on `ed25519-dalek` ensures
255/// `SigningKey::Drop` uses volatile writes to clear the secret bytes.
256///
257/// **Invariant:** all inner key types must implement [`ZeroizeOnDrop`].
258impl ZeroizeOnDrop for PrivateKey {}
259
260/// Serializes as the prefixed string format (e.g. `"ed25519:base64..."`).
261impl Serialize for PrivateKey {
262    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
263        serializer.serialize_str(&self.to_prefixed_string())
264    }
265}
266
267/// Deserializes from the prefixed string format (e.g. `"ed25519:base64..."`).
268impl<'de> Deserialize<'de> for PrivateKey {
269    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
270        let s = String::deserialize(deserializer)?;
271        PrivateKey::from_prefixed_string(&s).map_err(serde::de::Error::custom)
272    }
273}
274
275// ==================== Free Functions ====================
276
277/// Generate a new keypair using the default algorithm (Ed25519).
278///
279/// Returns a `(PrivateKey, PublicKey)` tuple. Uses the operating system's
280/// cryptographically secure random number generator.
281pub fn generate_keypair() -> (PrivateKey, PublicKey) {
282    let key = PrivateKey::generate();
283    let pubkey = key.public_key();
284    (key, pubkey)
285}
286
287/// Sign an entry and return a base64-encoded signature string.
288///
289/// Accepts the algorithm-agnostic `PrivateKey` enum.
290pub fn sign_entry(entry: &Entry, signing_key: &PrivateKey) -> Result<String, Error> {
291    Ok(sign_data(entry.signing_bytes()?, signing_key))
292}
293
294/// Verify an entry's signature using an algorithm-agnostic `PublicKey`.
295///
296/// Returns `Ok(())` if the signature is valid, or `Err(AuthError)` if
297/// verification fails (missing signature, malformed data, or wrong key).
298pub fn verify_entry_signature(entry: &Entry, public_key: &PublicKey) -> Result<(), AuthError> {
299    let signature_base64 = entry
300        .auth()
301        .signature
302        .as_ref()
303        .ok_or(AuthError::InvalidSignature)?;
304
305    let signature_bytes =
306        Base64::decode_vec(signature_base64).map_err(|_| AuthError::InvalidSignature)?;
307
308    let signing_bytes = entry
309        .signing_bytes()
310        .map_err(|e| AuthError::InvalidAuthConfiguration {
311            reason: format!("Failed to get signing bytes: {e}"),
312        })?;
313
314    public_key.verify(&signing_bytes, &signature_bytes)
315}
316
317/// Sign data and return a base64-encoded signature.
318pub fn sign_data(data: impl AsRef<[u8]>, signing_key: &PrivateKey) -> String {
319    let sig_bytes = signing_key.sign(data.as_ref());
320    Base64::encode_string(&sig_bytes)
321}
322
323/// Generate random challenge bytes for authentication
324///
325/// Generates 32 bytes of cryptographically secure random data using
326/// `rand::rngs::OsRng` for use in challenge-response authentication protocols.
327/// The challenge serves as a nonce to prevent replay attacks during handshakes.
328///
329/// # Security
330/// Uses `OsRng` which provides the highest quality randomness available on the
331/// platform by interfacing directly with the operating system's random number
332/// generator (e.g., `/dev/urandom` on Unix systems, `CryptGenRandom` on Windows).
333///
334/// # Example
335/// ```rust,ignore
336/// use eidetica::auth::crypto::generate_challenge;
337///
338/// let challenge = generate_challenge();
339/// assert_eq!(challenge.len(), 32);
340/// ```
341pub fn generate_challenge() -> Vec<u8> {
342    let mut challenge = vec![0u8; CHALLENGE_SIZE];
343    OsRng.fill(&mut challenge[..]);
344    challenge
345}
346
347/// Create a challenge response by signing a challenge
348///
349/// Signs the challenge with the given key and returns the raw signature bytes.
350/// This is used in sync handshake protocols where the signature needs to be
351/// transmitted as binary data rather than base64 strings.
352///
353/// # Arguments
354/// * `challenge` - The challenge bytes to sign
355/// * `signing_key` - The private key to sign with
356///
357/// # Returns
358/// Raw signature bytes (not base64 encoded)
359pub fn create_challenge_response(challenge: impl AsRef<[u8]>, signing_key: &PrivateKey) -> Vec<u8> {
360    signing_key.sign(challenge.as_ref())
361}
362
363/// Verify a challenge response
364///
365/// Verifies that the given response bytes are a valid signature of the challenge
366/// using the provided public key.
367///
368/// # Arguments
369/// * `challenge` - The original challenge bytes
370/// * `response` - The signature bytes to verify
371/// * `public_key` - The public key to verify against
372///
373/// # Errors
374/// Returns `AuthError::InvalidSignature` if the signature is malformed or does not match.
375///
376/// # Example
377/// ```rust,ignore
378/// use eidetica::auth::crypto::{generate_challenge, create_challenge_response, verify_challenge_response};
379///
380/// let challenge = generate_challenge();
381/// let response = create_challenge_response(&challenge, &signing_key);
382///
383/// match verify_challenge_response(&challenge, &response, &public_key) {
384///     Ok(()) => println!("Signature verified"),
385///     Err(e) => println!("Verification failed: {}", e),
386/// }
387/// ```
388pub fn verify_challenge_response(
389    challenge: impl AsRef<[u8]>,
390    response: impl AsRef<[u8]>,
391    public_key: &PublicKey,
392) -> Result<(), AuthError> {
393    public_key.verify(challenge.as_ref(), response.as_ref())
394}
395
396#[cfg(test)]
397mod tests {
398    use super::*;
399    use crate::auth::types::{AuthInfo, SigKey};
400
401    #[test]
402    fn test_keypair_generation() {
403        let (signing_key, verifying_key) = generate_keypair();
404
405        // Test signing and verification
406        let test_data = b"hello world";
407        let signature = sign_data(test_data, &signing_key);
408        let sig_bytes = Base64::decode_vec(&signature).unwrap();
409
410        verifying_key.verify(test_data, &sig_bytes).unwrap();
411
412        // Test with wrong data
413        let wrong_data = b"goodbye world";
414        assert!(verifying_key.verify(wrong_data, &sig_bytes).is_err());
415    }
416
417    #[test]
418    fn test_key_formatting() {
419        let (_, verifying_key) = generate_keypair();
420        let formatted = verifying_key.to_string();
421
422        assert!(formatted.starts_with("ed25519:"));
423
424        // Should be able to parse it back
425        let parsed = PublicKey::from_prefixed_string(&formatted);
426        assert!(parsed.is_ok());
427        assert_eq!(parsed.unwrap(), verifying_key);
428    }
429
430    #[test]
431    fn test_entry_signing() {
432        let (signing_key, verifying_key) = generate_keypair();
433
434        // Create a test entry with auth info but no signature
435        let entry = Entry::root_builder()
436            .set_auth(
437                AuthInfo::builder()
438                    .key(SigKey::from_name("KEY_LAPTOP"))
439                    .build(),
440            )
441            .build()
442            .expect("Root entry should build successfully");
443
444        // Sign the entry
445        let signature = sign_entry(&entry, &signing_key).unwrap();
446
447        // Set the signature on the entry
448        let entry = entry.with_auth(|auth| auth.signature = Some(signature));
449
450        // Verify the signature using algorithm-agnostic PublicKey
451        verify_entry_signature(&entry, &verifying_key).unwrap();
452
453        // Test with wrong key
454        let (_, wrong_key) = generate_keypair();
455        assert!(verify_entry_signature(&entry, &wrong_key).is_err());
456    }
457
458    #[test]
459    fn test_challenge_generation() {
460        let challenge1 = generate_challenge();
461        let challenge2 = generate_challenge();
462
463        // Should be CHALLENGE_SIZE bytes
464        assert_eq!(challenge1.len(), CHALLENGE_SIZE);
465        assert_eq!(challenge2.len(), CHALLENGE_SIZE);
466
467        // Should be different each time
468        assert_ne!(challenge1, challenge2);
469    }
470
471    #[test]
472    fn test_challenge_response() {
473        let (signing_key, verifying_key) = generate_keypair();
474        let challenge = generate_challenge();
475
476        // Create and verify challenge response
477        let response = create_challenge_response(&challenge, &signing_key);
478
479        // Response should be ED25519_SIGNATURE_SIZE bytes (Ed25519 signature)
480        assert_eq!(response.len(), ED25519_SIGNATURE_SIZE);
481
482        // Should verify correctly
483        assert!(verify_challenge_response(&challenge, &response, &verifying_key).is_ok());
484
485        // Should fail with wrong challenge
486        let wrong_challenge = generate_challenge();
487        assert!(verify_challenge_response(&wrong_challenge, &response, &verifying_key).is_err());
488
489        // Should fail with wrong key
490        let (_, wrong_pubkey) = generate_keypair();
491        assert!(verify_challenge_response(&challenge, &response, &wrong_pubkey).is_err());
492    }
493
494    // ==================== PublicKey / PrivateKey Enum Tests ====================
495
496    #[test]
497    fn test_private_key_generate_and_sign() {
498        let key = PrivateKey::generate();
499        let pubkey = key.public_key();
500        let data = b"hello world";
501
502        let signature = key.sign(data);
503        pubkey.verify(data, &signature).unwrap();
504
505        // Wrong data should not verify
506        assert!(pubkey.verify(b"wrong data", &signature).is_err());
507    }
508
509    #[test]
510    fn test_public_key_prefixed_string_roundtrip() {
511        let key = PrivateKey::generate();
512        let pubkey = key.public_key();
513
514        let formatted = pubkey.to_prefixed_string();
515        assert!(formatted.starts_with("ed25519:"));
516
517        let parsed = PublicKey::from_prefixed_string(&formatted).unwrap();
518        assert_eq!(parsed, pubkey);
519    }
520
521    #[test]
522    fn test_public_key_from_prefixed_string_invalid() {
523        // Unknown prefix
524        assert!(PublicKey::from_prefixed_string("rsa:abc").is_err());
525
526        // No prefix
527        assert!(PublicKey::from_prefixed_string("abc").is_err());
528
529        // Invalid base64
530        assert!(PublicKey::from_prefixed_string("ed25519:!!!invalid!!!").is_err());
531
532        // Wrong length
533        assert!(PublicKey::from_prefixed_string("ed25519:AAAA").is_err());
534    }
535
536    #[test]
537    fn test_private_key_algorithm() {
538        let key = PrivateKey::generate();
539        assert_eq!(key.algorithm(), "ed25519");
540        assert_eq!(key.public_key().algorithm(), "ed25519");
541    }
542
543    #[test]
544    fn test_private_key_bytes_roundtrip() {
545        let key = PrivateKey::generate();
546        let bytes = key.to_bytes();
547        let algorithm = key.algorithm();
548
549        let restored = PrivateKey::from_bytes(algorithm, &bytes).unwrap();
550        assert_eq!(
551            key.public_key().to_prefixed_string(),
552            restored.public_key().to_prefixed_string()
553        );
554    }
555
556    #[test]
557    fn test_private_key_from_bytes_invalid() {
558        // Unknown algorithm
559        assert!(PrivateKey::from_bytes("rsa", &[0u8; 32]).is_err());
560
561        // Wrong length
562        assert!(PrivateKey::from_bytes("ed25519", &[0u8; 16]).is_err());
563    }
564
565    #[test]
566    fn test_private_key_prefixed_string_roundtrip() {
567        let key = PrivateKey::generate();
568        let formatted = key.to_prefixed_string();
569        assert!(formatted.starts_with("ed25519:"));
570
571        let restored = PrivateKey::from_prefixed_string(&formatted).unwrap();
572        assert_eq!(
573            key.public_key().to_prefixed_string(),
574            restored.public_key().to_prefixed_string()
575        );
576    }
577
578    #[test]
579    fn test_private_key_from_prefixed_string_invalid() {
580        // Unknown prefix
581        assert!(PrivateKey::from_prefixed_string("rsa:abc").is_err());
582
583        // No prefix
584        assert!(PrivateKey::from_prefixed_string("abc").is_err());
585
586        // Invalid base64
587        assert!(PrivateKey::from_prefixed_string("ed25519:!!!invalid!!!").is_err());
588
589        // Wrong length
590        assert!(PrivateKey::from_prefixed_string("ed25519:AAAA").is_err());
591    }
592
593    #[test]
594    fn test_private_key_serde_roundtrip() {
595        let key = PrivateKey::generate();
596        let pubkey_str = key.public_key().to_prefixed_string();
597
598        let serialized = serde_json::to_string(&key).unwrap();
599        // Should serialize as a prefixed string, same format as PublicKey
600        assert!(serialized.starts_with("\"ed25519:"));
601
602        let deserialized: PrivateKey = serde_json::from_str(&serialized).unwrap();
603        assert_eq!(deserialized.public_key().to_prefixed_string(), pubkey_str);
604    }
605
606    #[test]
607    fn test_private_key_debug_redacted() {
608        let key = PrivateKey::generate();
609        let debug_str = format!("{key:?}");
610        assert_eq!(debug_str, "PrivateKey::Ed25519([REDACTED])");
611        assert!(!debug_str.contains(&format!("{:?}", key.to_bytes())));
612    }
613
614    #[test]
615    fn test_public_key_display() {
616        let key = PrivateKey::generate();
617        let pubkey = key.public_key();
618        assert_eq!(format!("{pubkey}"), pubkey.to_prefixed_string());
619    }
620
621    #[test]
622    fn test_public_key_verify_malformed_signature() {
623        let key = PrivateKey::generate();
624        let pubkey = key.public_key();
625
626        // Too short
627        assert!(pubkey.verify(b"data", &[0u8; 10]).is_err());
628
629        // Wrong length
630        assert!(pubkey.verify(b"data", &[0u8; 63]).is_err());
631
632        // Correct length but invalid
633        assert!(pubkey.verify(b"data", &[0u8; 64]).is_err());
634    }
635
636    #[test]
637    fn test_public_key_serde_roundtrip() {
638        let key = PrivateKey::generate();
639        let pubkey = key.public_key();
640
641        let serialized = serde_json::to_string(&pubkey).unwrap();
642        // Should serialize as a plain prefixed string
643        assert_eq!(serialized, format!("\"{}\"", pubkey.to_prefixed_string()));
644
645        let deserialized: PublicKey = serde_json::from_str(&serialized).unwrap();
646        assert_eq!(deserialized, pubkey);
647    }
648
649    #[test]
650    fn test_public_key_hash() {
651        use std::collections::HashSet;
652
653        let key1 = PrivateKey::generate();
654        let key2 = PrivateKey::generate();
655        let pubkey1 = key1.public_key();
656        let pubkey2 = key2.public_key();
657
658        let mut set = HashSet::new();
659        set.insert(pubkey1.clone());
660        set.insert(pubkey2.clone());
661        set.insert(pubkey1.clone()); // duplicate
662
663        assert_eq!(set.len(), 2);
664        assert!(set.contains(&pubkey1));
665        assert!(set.contains(&pubkey2));
666    }
667
668    #[test]
669    fn test_public_key_display_matches_prefixed_string() {
670        // PublicKey Display should produce the same format as to_prefixed_string()
671        let (_, verifying_key) = generate_keypair();
672        let formatted = verifying_key.to_string();
673        assert_eq!(verifying_key.to_prefixed_string(), formatted);
674    }
675
676    #[test]
677    fn test_private_key_sign_and_verify_roundtrip() {
678        let (signing_key, verifying_key) = generate_keypair();
679
680        let data = b"test data for signing";
681        let sig_b64 = sign_data(data, &signing_key);
682        let sig_bytes = signing_key.sign(data);
683
684        // Verify via PublicKey enum
685        verifying_key
686            .verify(data, &Base64::decode_vec(&sig_b64).unwrap())
687            .unwrap();
688        verifying_key.verify(data, &sig_bytes).unwrap();
689    }
690}