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

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.sig.sig.as_ref().ok_or(AuthError::InvalidSignature)?;
300
301    let signature_bytes =
302        Base64::decode_vec(signature_base64).map_err(|_| AuthError::InvalidSignature)?;
303
304    let signing_bytes = entry
305        .signing_bytes()
306        .map_err(|e| AuthError::InvalidAuthConfiguration {
307            reason: format!("Failed to get signing bytes: {e}"),
308        })?;
309
310    public_key.verify(&signing_bytes, &signature_bytes)
311}
312
313/// Sign data and return a base64-encoded signature.
314pub fn sign_data(data: impl AsRef<[u8]>, signing_key: &PrivateKey) -> String {
315    let sig_bytes = signing_key.sign(data.as_ref());
316    Base64::encode_string(&sig_bytes)
317}
318
319/// Generate random challenge bytes for authentication
320///
321/// Generates 32 bytes of cryptographically secure random data using
322/// `rand::rngs::OsRng` for use in challenge-response authentication protocols.
323/// The challenge serves as a nonce to prevent replay attacks during handshakes.
324///
325/// # Security
326/// Uses `OsRng` which provides the highest quality randomness available on the
327/// platform by interfacing directly with the operating system's random number
328/// generator (e.g., `/dev/urandom` on Unix systems, `CryptGenRandom` on Windows).
329///
330/// # Example
331/// ```rust,ignore
332/// use eidetica::auth::crypto::generate_challenge;
333///
334/// let challenge = generate_challenge();
335/// assert_eq!(challenge.len(), 32);
336/// ```
337pub fn generate_challenge() -> Vec<u8> {
338    let mut challenge = vec![0u8; CHALLENGE_SIZE];
339    OsRng.fill(&mut challenge[..]);
340    challenge
341}
342
343/// Create a challenge response by signing a challenge
344///
345/// Signs the challenge with the given key and returns the raw signature bytes.
346/// This is used in sync handshake protocols where the signature needs to be
347/// transmitted as binary data rather than base64 strings.
348///
349/// # Arguments
350/// * `challenge` - The challenge bytes to sign
351/// * `signing_key` - The private key to sign with
352///
353/// # Returns
354/// Raw signature bytes (not base64 encoded)
355pub fn create_challenge_response(challenge: impl AsRef<[u8]>, signing_key: &PrivateKey) -> Vec<u8> {
356    signing_key.sign(challenge.as_ref())
357}
358
359/// Verify a challenge response
360///
361/// Verifies that the given response bytes are a valid signature of the challenge
362/// using the provided public key.
363///
364/// # Arguments
365/// * `challenge` - The original challenge bytes
366/// * `response` - The signature bytes to verify
367/// * `public_key` - The public key to verify against
368///
369/// # Errors
370/// Returns `AuthError::InvalidSignature` if the signature is malformed or does not match.
371///
372/// # Example
373/// ```rust,ignore
374/// use eidetica::auth::crypto::{generate_challenge, create_challenge_response, verify_challenge_response};
375///
376/// let challenge = generate_challenge();
377/// let response = create_challenge_response(&challenge, &signing_key);
378///
379/// match verify_challenge_response(&challenge, &response, &public_key) {
380///     Ok(()) => println!("Signature verified"),
381///     Err(e) => println!("Verification failed: {}", e),
382/// }
383/// ```
384pub fn verify_challenge_response(
385    challenge: impl AsRef<[u8]>,
386    response: impl AsRef<[u8]>,
387    public_key: &PublicKey,
388) -> Result<(), AuthError> {
389    public_key.verify(challenge.as_ref(), response.as_ref())
390}
391
392#[cfg(test)]
393mod tests {
394    use super::*;
395    use crate::auth::types::{SigInfo, SigKey};
396
397    #[test]
398    fn test_keypair_generation() {
399        let (signing_key, verifying_key) = generate_keypair();
400
401        // Test signing and verification
402        let test_data = b"hello world";
403        let signature = sign_data(test_data, &signing_key);
404        let sig_bytes = Base64::decode_vec(&signature).unwrap();
405
406        verifying_key.verify(test_data, &sig_bytes).unwrap();
407
408        // Test with wrong data
409        let wrong_data = b"goodbye world";
410        assert!(verifying_key.verify(wrong_data, &sig_bytes).is_err());
411    }
412
413    #[test]
414    fn test_key_formatting() {
415        let (_, verifying_key) = generate_keypair();
416        let formatted = verifying_key.to_string();
417
418        assert!(formatted.starts_with("ed25519:"));
419
420        // Should be able to parse it back
421        let parsed = PublicKey::from_prefixed_string(&formatted);
422        assert!(parsed.is_ok());
423        assert_eq!(parsed.unwrap(), verifying_key);
424    }
425
426    #[test]
427    fn test_entry_signing() {
428        let (signing_key, verifying_key) = generate_keypair();
429
430        // Create a test entry with auth info but no signature
431        let mut entry = Entry::root_builder()
432            .build()
433            .expect("Root entry should build successfully");
434
435        // Set auth ID without signature
436        entry.sig = SigInfo::builder()
437            .key(SigKey::from_name("KEY_LAPTOP"))
438            .build();
439
440        // Sign the entry
441        let signature = sign_entry(&entry, &signing_key).unwrap();
442
443        // Set the signature on the entry
444        entry.sig.sig = Some(signature);
445
446        // Verify the signature using algorithm-agnostic PublicKey
447        verify_entry_signature(&entry, &verifying_key).unwrap();
448
449        // Test with wrong key
450        let (_, wrong_key) = generate_keypair();
451        assert!(verify_entry_signature(&entry, &wrong_key).is_err());
452    }
453
454    #[test]
455    fn test_challenge_generation() {
456        let challenge1 = generate_challenge();
457        let challenge2 = generate_challenge();
458
459        // Should be CHALLENGE_SIZE bytes
460        assert_eq!(challenge1.len(), CHALLENGE_SIZE);
461        assert_eq!(challenge2.len(), CHALLENGE_SIZE);
462
463        // Should be different each time
464        assert_ne!(challenge1, challenge2);
465    }
466
467    #[test]
468    fn test_challenge_response() {
469        let (signing_key, verifying_key) = generate_keypair();
470        let challenge = generate_challenge();
471
472        // Create and verify challenge response
473        let response = create_challenge_response(&challenge, &signing_key);
474
475        // Response should be ED25519_SIGNATURE_SIZE bytes (Ed25519 signature)
476        assert_eq!(response.len(), ED25519_SIGNATURE_SIZE);
477
478        // Should verify correctly
479        assert!(verify_challenge_response(&challenge, &response, &verifying_key).is_ok());
480
481        // Should fail with wrong challenge
482        let wrong_challenge = generate_challenge();
483        assert!(verify_challenge_response(&wrong_challenge, &response, &verifying_key).is_err());
484
485        // Should fail with wrong key
486        let (_, wrong_pubkey) = generate_keypair();
487        assert!(verify_challenge_response(&challenge, &response, &wrong_pubkey).is_err());
488    }
489
490    // ==================== PublicKey / PrivateKey Enum Tests ====================
491
492    #[test]
493    fn test_private_key_generate_and_sign() {
494        let key = PrivateKey::generate();
495        let pubkey = key.public_key();
496        let data = b"hello world";
497
498        let signature = key.sign(data);
499        pubkey.verify(data, &signature).unwrap();
500
501        // Wrong data should not verify
502        assert!(pubkey.verify(b"wrong data", &signature).is_err());
503    }
504
505    #[test]
506    fn test_public_key_prefixed_string_roundtrip() {
507        let key = PrivateKey::generate();
508        let pubkey = key.public_key();
509
510        let formatted = pubkey.to_prefixed_string();
511        assert!(formatted.starts_with("ed25519:"));
512
513        let parsed = PublicKey::from_prefixed_string(&formatted).unwrap();
514        assert_eq!(parsed, pubkey);
515    }
516
517    #[test]
518    fn test_public_key_from_prefixed_string_invalid() {
519        // Unknown prefix
520        assert!(PublicKey::from_prefixed_string("rsa:abc").is_err());
521
522        // No prefix
523        assert!(PublicKey::from_prefixed_string("abc").is_err());
524
525        // Invalid base64
526        assert!(PublicKey::from_prefixed_string("ed25519:!!!invalid!!!").is_err());
527
528        // Wrong length
529        assert!(PublicKey::from_prefixed_string("ed25519:AAAA").is_err());
530    }
531
532    #[test]
533    fn test_private_key_algorithm() {
534        let key = PrivateKey::generate();
535        assert_eq!(key.algorithm(), "ed25519");
536        assert_eq!(key.public_key().algorithm(), "ed25519");
537    }
538
539    #[test]
540    fn test_private_key_bytes_roundtrip() {
541        let key = PrivateKey::generate();
542        let bytes = key.to_bytes();
543        let algorithm = key.algorithm();
544
545        let restored = PrivateKey::from_bytes(algorithm, &bytes).unwrap();
546        assert_eq!(
547            key.public_key().to_prefixed_string(),
548            restored.public_key().to_prefixed_string()
549        );
550    }
551
552    #[test]
553    fn test_private_key_from_bytes_invalid() {
554        // Unknown algorithm
555        assert!(PrivateKey::from_bytes("rsa", &[0u8; 32]).is_err());
556
557        // Wrong length
558        assert!(PrivateKey::from_bytes("ed25519", &[0u8; 16]).is_err());
559    }
560
561    #[test]
562    fn test_private_key_prefixed_string_roundtrip() {
563        let key = PrivateKey::generate();
564        let formatted = key.to_prefixed_string();
565        assert!(formatted.starts_with("ed25519:"));
566
567        let restored = PrivateKey::from_prefixed_string(&formatted).unwrap();
568        assert_eq!(
569            key.public_key().to_prefixed_string(),
570            restored.public_key().to_prefixed_string()
571        );
572    }
573
574    #[test]
575    fn test_private_key_from_prefixed_string_invalid() {
576        // Unknown prefix
577        assert!(PrivateKey::from_prefixed_string("rsa:abc").is_err());
578
579        // No prefix
580        assert!(PrivateKey::from_prefixed_string("abc").is_err());
581
582        // Invalid base64
583        assert!(PrivateKey::from_prefixed_string("ed25519:!!!invalid!!!").is_err());
584
585        // Wrong length
586        assert!(PrivateKey::from_prefixed_string("ed25519:AAAA").is_err());
587    }
588
589    #[test]
590    fn test_private_key_serde_roundtrip() {
591        let key = PrivateKey::generate();
592        let pubkey_str = key.public_key().to_prefixed_string();
593
594        let serialized = serde_json::to_string(&key).unwrap();
595        // Should serialize as a prefixed string, same format as PublicKey
596        assert!(serialized.starts_with("\"ed25519:"));
597
598        let deserialized: PrivateKey = serde_json::from_str(&serialized).unwrap();
599        assert_eq!(deserialized.public_key().to_prefixed_string(), pubkey_str);
600    }
601
602    #[test]
603    fn test_private_key_debug_redacted() {
604        let key = PrivateKey::generate();
605        let debug_str = format!("{key:?}");
606        assert_eq!(debug_str, "PrivateKey::Ed25519([REDACTED])");
607        assert!(!debug_str.contains(&format!("{:?}", key.to_bytes())));
608    }
609
610    #[test]
611    fn test_public_key_display() {
612        let key = PrivateKey::generate();
613        let pubkey = key.public_key();
614        assert_eq!(format!("{pubkey}"), pubkey.to_prefixed_string());
615    }
616
617    #[test]
618    fn test_public_key_verify_malformed_signature() {
619        let key = PrivateKey::generate();
620        let pubkey = key.public_key();
621
622        // Too short
623        assert!(pubkey.verify(b"data", &[0u8; 10]).is_err());
624
625        // Wrong length
626        assert!(pubkey.verify(b"data", &[0u8; 63]).is_err());
627
628        // Correct length but invalid
629        assert!(pubkey.verify(b"data", &[0u8; 64]).is_err());
630    }
631
632    #[test]
633    fn test_public_key_serde_roundtrip() {
634        let key = PrivateKey::generate();
635        let pubkey = key.public_key();
636
637        let serialized = serde_json::to_string(&pubkey).unwrap();
638        // Should serialize as a plain prefixed string
639        assert_eq!(serialized, format!("\"{}\"", pubkey.to_prefixed_string()));
640
641        let deserialized: PublicKey = serde_json::from_str(&serialized).unwrap();
642        assert_eq!(deserialized, pubkey);
643    }
644
645    #[test]
646    fn test_public_key_hash() {
647        use std::collections::HashSet;
648
649        let key1 = PrivateKey::generate();
650        let key2 = PrivateKey::generate();
651        let pubkey1 = key1.public_key();
652        let pubkey2 = key2.public_key();
653
654        let mut set = HashSet::new();
655        set.insert(pubkey1.clone());
656        set.insert(pubkey2.clone());
657        set.insert(pubkey1.clone()); // duplicate
658
659        assert_eq!(set.len(), 2);
660        assert!(set.contains(&pubkey1));
661        assert!(set.contains(&pubkey2));
662    }
663
664    #[test]
665    fn test_public_key_display_matches_prefixed_string() {
666        // PublicKey Display should produce the same format as to_prefixed_string()
667        let (_, verifying_key) = generate_keypair();
668        let formatted = verifying_key.to_string();
669        assert_eq!(verifying_key.to_prefixed_string(), formatted);
670    }
671
672    #[test]
673    fn test_private_key_sign_and_verify_roundtrip() {
674        let (signing_key, verifying_key) = generate_keypair();
675
676        let data = b"test data for signing";
677        let sig_b64 = sign_data(data, &signing_key);
678        let sig_bytes = signing_key.sign(data);
679
680        // Verify via PublicKey enum
681        verifying_key
682            .verify(data, &Base64::decode_vec(&sig_b64).unwrap())
683            .unwrap();
684        verifying_key.verify(data, &sig_bytes).unwrap();
685    }
686}