txgate_crypto/keypair.rs
1//! Cryptographic key pair traits and implementations.
2//!
3//! This module provides the [`KeyPair`] trait for abstracting over different
4//! elliptic curve key pairs, and implementations for specific curves.
5//!
6//! # Supported Curves
7//!
8//! - [`Secp256k1KeyPair`] - For Ethereum, Bitcoin, Tron, and Ripple
9//! - [`Ed25519KeyPair`] - For Solana and other ed25519-based chains
10//!
11//! # Example
12//!
13//! ```rust
14//! use txgate_crypto::keypair::{KeyPair, Secp256k1KeyPair};
15//!
16//! // Generate a new random key pair
17//! let keypair = Secp256k1KeyPair::generate();
18//!
19//! // Get the public key
20//! let pubkey = keypair.public_key();
21//! println!("Compressed: {} bytes", pubkey.compressed().len());
22//!
23//! // Get Ethereum address
24//! let eth_address = pubkey.ethereum_address();
25//! println!("Ethereum address: 0x{}", hex::encode(eth_address));
26//!
27//! // Sign a message hash
28//! let hash = [0u8; 32]; // In practice, this would be a real hash
29//! let signature = keypair.sign(&hash).expect("signing failed");
30//! println!("Signature: {} bytes", signature.as_ref().len());
31//! ```
32
33use k256::ecdsa::{RecoveryId, Signature as K256Signature, SigningKey, VerifyingKey};
34use sha3::{Digest, Keccak256};
35
36use crate::keys::SecretKey;
37use txgate_core::error::SignError;
38
39// ============================================================================
40// KeyPair Trait
41// ============================================================================
42
43/// Trait for cryptographic key pairs.
44///
45/// This trait abstracts over different elliptic curve key pairs,
46/// allowing the signing service to work with multiple curves.
47///
48/// # Thread Safety
49///
50/// All implementations must be `Send + Sync` to support multi-threaded
51/// signing operations.
52///
53/// # Example
54///
55/// ```rust
56/// use txgate_crypto::keypair::{KeyPair, Secp256k1KeyPair};
57///
58/// fn sign_message<K: KeyPair>(keypair: &K, hash: &[u8; 32]) -> Vec<u8> {
59/// keypair.sign(hash)
60/// .expect("signing failed")
61/// .as_ref()
62/// .to_vec()
63/// }
64///
65/// let keypair = Secp256k1KeyPair::generate();
66/// let hash = [0u8; 32];
67/// let sig = sign_message(&keypair, &hash);
68/// ```
69pub trait KeyPair: Send + Sync {
70 /// The signature type produced by this key pair.
71 type Signature: AsRef<[u8]>;
72
73 /// The public key type for this key pair.
74 type PublicKey: AsRef<[u8]>;
75
76 /// Generate a new random key pair.
77 ///
78 /// Uses a cryptographically secure random number generator.
79 fn generate() -> Self
80 where
81 Self: Sized;
82
83 /// Create a key pair from raw secret key bytes.
84 ///
85 /// # Arguments
86 /// * `bytes` - The 32-byte secret key material
87 ///
88 /// # Errors
89 /// Returns an error if the bytes don't represent a valid secret key
90 /// for this curve.
91 fn from_bytes(bytes: [u8; 32]) -> Result<Self, SignError>
92 where
93 Self: Sized;
94
95 /// Get the public key.
96 fn public_key(&self) -> &Self::PublicKey;
97
98 /// Sign a 32-byte message hash.
99 ///
100 /// # Arguments
101 /// * `hash` - The 32-byte hash to sign (NOT the raw message)
102 ///
103 /// # Returns
104 /// The signature bytes.
105 ///
106 /// # Errors
107 /// Returns an error if signing fails.
108 ///
109 /// # Important
110 /// The `hash` parameter should be a cryptographic hash of the message
111 /// (e.g., SHA-256 or Keccak-256), NOT the raw message itself.
112 fn sign(&self, hash: &[u8; 32]) -> Result<Self::Signature, SignError>;
113}
114
115// ============================================================================
116// Secp256k1 Public Key
117// ============================================================================
118
119/// Wrapper for secp256k1 public keys.
120///
121/// Stores both compressed (33 bytes) and uncompressed (65 bytes) formats
122/// to avoid recomputation.
123///
124/// # Formats
125///
126/// - **Compressed**: 33 bytes, prefix `0x02` or `0x03` + 32-byte X coordinate
127/// - **Uncompressed**: 65 bytes, prefix `0x04` + 32-byte X + 32-byte Y
128///
129/// # Example
130///
131/// ```rust
132/// use txgate_crypto::keypair::{KeyPair, Secp256k1KeyPair};
133///
134/// let keypair = Secp256k1KeyPair::generate();
135/// let pubkey = keypair.public_key();
136///
137/// // Get compressed format (33 bytes)
138/// assert_eq!(pubkey.compressed().len(), 33);
139///
140/// // Get uncompressed format (65 bytes)
141/// assert_eq!(pubkey.uncompressed().len(), 65);
142///
143/// // Get Ethereum address (20 bytes)
144/// let address = pubkey.ethereum_address();
145/// assert_eq!(address.len(), 20);
146/// ```
147#[derive(Clone)]
148pub struct Secp256k1PublicKey {
149 /// Compressed public key (33 bytes: prefix + X coordinate)
150 compressed: [u8; 33],
151 /// Uncompressed public key (65 bytes: prefix + X + Y coordinates)
152 uncompressed: [u8; 65],
153}
154
155impl Secp256k1PublicKey {
156 /// Create a new public key from a k256 `VerifyingKey`.
157 fn from_verifying_key(verifying: &VerifyingKey) -> Self {
158 let point = verifying.to_encoded_point(false);
159 let uncompressed_bytes = point.as_bytes();
160
161 let mut uncompressed = [0u8; 65];
162 uncompressed.copy_from_slice(uncompressed_bytes);
163
164 let point_compressed = verifying.to_encoded_point(true);
165 let compressed_bytes = point_compressed.as_bytes();
166
167 let mut compressed = [0u8; 33];
168 compressed.copy_from_slice(compressed_bytes);
169
170 Self {
171 compressed,
172 uncompressed,
173 }
174 }
175
176 /// Get the compressed public key (33 bytes).
177 ///
178 /// Format: `prefix (1 byte) || X (32 bytes)`
179 /// - Prefix is `0x02` if Y is even, `0x03` if Y is odd
180 #[must_use]
181 pub const fn compressed(&self) -> &[u8; 33] {
182 &self.compressed
183 }
184
185 /// Get the uncompressed public key (65 bytes).
186 ///
187 /// Format: `0x04 || X (32 bytes) || Y (32 bytes)`
188 #[must_use]
189 pub const fn uncompressed(&self) -> &[u8; 65] {
190 &self.uncompressed
191 }
192
193 /// Derive the Ethereum address from this public key.
194 ///
195 /// The Ethereum address is the last 20 bytes of the Keccak-256 hash
196 /// of the uncompressed public key (without the `0x04` prefix).
197 ///
198 /// # Example
199 ///
200 /// ```rust
201 /// use txgate_crypto::keypair::{KeyPair, Secp256k1KeyPair};
202 ///
203 /// let keypair = Secp256k1KeyPair::generate();
204 /// let address = keypair.public_key().ethereum_address();
205 /// assert_eq!(address.len(), 20);
206 /// ```
207 #[must_use]
208 pub fn ethereum_address(&self) -> [u8; 20] {
209 // Hash the uncompressed public key without the 0x04 prefix
210 // The uncompressed key is always 65 bytes, so [1..] is 64 bytes
211 let hash = Keccak256::digest(&self.uncompressed[1..]);
212
213 // Take the last 20 bytes of the hash
214 // Keccak256 always produces 32 bytes
215 let mut address = [0u8; 20];
216
217 // Use get() to safely extract the slice, though this should never fail
218 // since Keccak256 always produces exactly 32 bytes
219 if let Some(hash_tail) = hash.get(12..32) {
220 address.copy_from_slice(hash_tail);
221 }
222
223 address
224 }
225}
226
227impl AsRef<[u8]> for Secp256k1PublicKey {
228 fn as_ref(&self) -> &[u8] {
229 &self.compressed
230 }
231}
232
233impl std::fmt::Debug for Secp256k1PublicKey {
234 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
235 write!(f, "Secp256k1PublicKey({})", hex::encode(self.compressed))
236 }
237}
238
239// ============================================================================
240// Secp256k1 Signature
241// ============================================================================
242
243/// Wrapper for secp256k1 ECDSA signatures.
244///
245/// Contains the 64-byte signature (r || s) and the recovery ID for
246/// Ethereum compatibility.
247///
248/// # Signature Formats
249///
250/// - **Standard**: 64 bytes (r: 32 bytes || s: 32 bytes)
251/// - **Recoverable**: 65 bytes (r || s || v) where v is the recovery ID
252///
253/// # Security
254///
255/// The S value is normalized to the lower half of the curve order to
256/// prevent signature malleability attacks.
257///
258/// # Example
259///
260/// ```rust
261/// use txgate_crypto::keypair::{KeyPair, Secp256k1KeyPair};
262///
263/// let keypair = Secp256k1KeyPair::generate();
264/// let hash = [0u8; 32];
265/// let signature = keypair.sign(&hash).expect("signing failed");
266///
267/// // Get standard 64-byte signature
268/// assert_eq!(signature.as_ref().len(), 64);
269///
270/// // Get recoverable signature with recovery ID
271/// let recoverable = signature.to_recoverable_bytes();
272/// assert_eq!(recoverable.len(), 65);
273///
274/// // Get recovery ID for ecrecover
275/// let v = signature.recovery_id();
276/// assert!(v == 0 || v == 1);
277/// ```
278#[derive(Clone)]
279pub struct Secp256k1Signature {
280 /// The 64-byte signature (r || s)
281 bytes: [u8; 64],
282 /// Recovery ID for Ethereum compatibility (0 or 1)
283 recovery_id: u8,
284}
285
286impl Secp256k1Signature {
287 /// Create a signature from raw bytes and a recovery ID.
288 ///
289 /// This is useful for reconstructing a signature from its components.
290 ///
291 /// # Arguments
292 /// * `bytes` - The 64-byte signature (r || s)
293 /// * `recovery_id` - The recovery ID (0 or 1)
294 ///
295 /// # Example
296 ///
297 /// ```rust
298 /// use txgate_crypto::keypair::{KeyPair, Secp256k1KeyPair, Secp256k1Signature};
299 ///
300 /// let keypair = Secp256k1KeyPair::generate();
301 /// let hash = [0u8; 32];
302 /// let signature = keypair.sign(&hash).expect("signing failed");
303 ///
304 /// // Get the recoverable bytes
305 /// let recoverable = signature.to_recoverable_bytes();
306 ///
307 /// // Reconstruct the signature
308 /// let bytes: [u8; 64] = recoverable[..64].try_into().unwrap();
309 /// let recovery_id = recoverable[64];
310 /// let reconstructed = Secp256k1Signature::from_bytes_and_recovery_id(bytes, recovery_id);
311 ///
312 /// assert_eq!(signature.as_ref(), reconstructed.as_ref());
313 /// ```
314 #[must_use]
315 pub const fn from_bytes_and_recovery_id(bytes: [u8; 64], recovery_id: u8) -> Self {
316 Self { bytes, recovery_id }
317 }
318
319 /// Get the recovery ID.
320 ///
321 /// This is 0 or 1, which can be used with Ethereum's `ecrecover`:
322 /// - For EIP-155 transactions: `v = recovery_id + 27`
323 /// - For EIP-2930/EIP-1559: `v = recovery_id`
324 #[must_use]
325 pub const fn recovery_id(&self) -> u8 {
326 self.recovery_id
327 }
328
329 /// Return signature as 65 bytes: r (32) || s (32) || v (1).
330 ///
331 /// The `v` byte is the raw recovery ID (0 or 1). For Ethereum
332 /// transactions, you may need to add 27 or use chain-specific
333 /// calculations for EIP-155.
334 #[must_use]
335 pub fn to_recoverable_bytes(&self) -> [u8; 65] {
336 let mut result = [0u8; 65];
337 result[..64].copy_from_slice(&self.bytes);
338 result[64] = self.recovery_id;
339 result
340 }
341
342 /// Get the r component of the signature (first 32 bytes).
343 ///
344 /// This always succeeds because the signature is always exactly 64 bytes.
345 ///
346 /// # Panics
347 ///
348 /// This method cannot panic in practice because the internal storage
349 /// is always exactly 64 bytes, but the conversion is technically fallible.
350 #[must_use]
351 #[allow(clippy::missing_panics_doc)]
352 pub fn r(&self) -> &[u8; 32] {
353 // Split the array at index 32 to get the first half
354 // This is safe because bytes is always [u8; 64]
355 let (r_part, _) = self.bytes.split_at(32);
356 // Convert to fixed-size array reference
357 // SAFETY: split_at(32) on a [u8; 64] always returns exactly 32 bytes in first half
358 #[allow(clippy::expect_used)]
359 r_part
360 .try_into()
361 .expect("split_at(32) always produces 32 bytes")
362 }
363
364 /// Get the s component of the signature (last 32 bytes).
365 ///
366 /// This always succeeds because the signature is always exactly 64 bytes.
367 ///
368 /// # Panics
369 ///
370 /// This method cannot panic in practice because the internal storage
371 /// is always exactly 64 bytes, but the conversion is technically fallible.
372 #[must_use]
373 #[allow(clippy::missing_panics_doc)]
374 pub fn s(&self) -> &[u8; 32] {
375 // Split the array at index 32 to get the second half
376 // This is safe because bytes is always [u8; 64]
377 let (_, s_part) = self.bytes.split_at(32);
378 // Convert to fixed-size array reference
379 // SAFETY: split_at(32) on a [u8; 64] always returns exactly 32 bytes in second half
380 #[allow(clippy::expect_used)]
381 s_part
382 .try_into()
383 .expect("split_at(32) always produces 32 bytes")
384 }
385}
386
387impl AsRef<[u8]> for Secp256k1Signature {
388 fn as_ref(&self) -> &[u8] {
389 &self.bytes
390 }
391}
392
393impl std::fmt::Debug for Secp256k1Signature {
394 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
395 write!(
396 f,
397 "Secp256k1Signature(r={}, s={}, v={})",
398 hex::encode(self.r()),
399 hex::encode(self.s()),
400 self.recovery_id
401 )
402 }
403}
404
405// ============================================================================
406// Secp256k1 Key Pair
407// ============================================================================
408
409/// secp256k1 key pair for Ethereum, Bitcoin, Tron, and Ripple.
410///
411/// This key pair uses the secp256k1 elliptic curve, which is the standard
412/// for most major blockchain networks.
413///
414/// # Security
415///
416/// - The signing key is stored securely and is `ZeroizeOnDrop`
417/// - Signatures are normalized to prevent malleability
418/// - Recovery IDs are computed for Ethereum compatibility
419///
420/// # Example
421///
422/// ```rust
423/// use txgate_crypto::keypair::{KeyPair, Secp256k1KeyPair};
424///
425/// // Generate a new key pair
426/// let keypair = Secp256k1KeyPair::generate();
427///
428/// // Or create from existing bytes
429/// let secret_bytes = [0x42u8; 32]; // Use real secret in production!
430/// let keypair = Secp256k1KeyPair::from_bytes(secret_bytes)
431/// .expect("valid secret key");
432///
433/// // Sign a message hash
434/// let hash = [0u8; 32]; // Use real hash in production!
435/// let signature = keypair.sign(&hash).expect("signing succeeded");
436///
437/// // Get Ethereum address
438/// let address = keypair.public_key().ethereum_address();
439/// println!("Address: 0x{}", hex::encode(address));
440/// ```
441#[allow(clippy::struct_field_names)]
442pub struct Secp256k1KeyPair {
443 /// The signing key (private key)
444 signing_key: SigningKey,
445 /// Cached verifying key (public key) for verification
446 verifying_key: VerifyingKey,
447 /// Cached public key wrapper
448 public_key: Secp256k1PublicKey,
449}
450
451impl Secp256k1KeyPair {
452 /// Create a key pair from a [`SecretKey`].
453 ///
454 /// This consumes the `SecretKey` to ensure the key material exists
455 /// in only one place.
456 ///
457 /// # Errors
458 /// Returns an error if the secret key bytes are not a valid secp256k1
459 /// scalar (e.g., zero or greater than the curve order).
460 ///
461 /// # Example
462 ///
463 /// ```rust
464 /// use txgate_crypto::keys::SecretKey;
465 /// use txgate_crypto::keypair::{KeyPair, Secp256k1KeyPair};
466 ///
467 /// let secret = SecretKey::generate();
468 /// let keypair = Secp256k1KeyPair::from_secret_key(&secret)
469 /// .expect("valid secret key");
470 /// ```
471 pub fn from_secret_key(secret: &SecretKey) -> Result<Self, SignError> {
472 Self::from_bytes(*secret.as_bytes())
473 }
474
475 /// Verify a signature against a hash using this key pair's public key.
476 ///
477 /// This is primarily useful for testing. In production, verification
478 /// is typically done using the public key alone.
479 ///
480 /// # Arguments
481 /// * `hash` - The 32-byte hash that was signed
482 /// * `signature` - The signature to verify
483 ///
484 /// # Returns
485 /// `true` if the signature is valid, `false` otherwise.
486 #[must_use]
487 pub fn verify(&self, hash: &[u8; 32], signature: &Secp256k1Signature) -> bool {
488 use k256::ecdsa::signature::hazmat::PrehashVerifier;
489
490 let Ok(k256_sig) = K256Signature::from_slice(signature.as_ref()) else {
491 return false;
492 };
493
494 self.verifying_key.verify_prehash(hash, &k256_sig).is_ok()
495 }
496}
497
498impl KeyPair for Secp256k1KeyPair {
499 type Signature = Secp256k1Signature;
500 type PublicKey = Secp256k1PublicKey;
501
502 fn generate() -> Self {
503 let secret = SecretKey::generate();
504 // Generated random keys from OsRng should always be valid secp256k1 scalars
505 // (non-zero and less than the curve order). If this fails, there's a
506 // fundamental issue with the RNG or the k256 library.
507 Self::from_bytes(*secret.as_bytes())
508 .unwrap_or_else(|_| unreachable!("OsRng generated an invalid secp256k1 scalar"))
509 }
510
511 fn from_bytes(bytes: [u8; 32]) -> Result<Self, SignError> {
512 let signing_key =
513 SigningKey::from_bytes((&bytes).into()).map_err(|_| SignError::InvalidKey)?;
514
515 let verifying_key = *signing_key.verifying_key();
516 let public_key = Secp256k1PublicKey::from_verifying_key(&verifying_key);
517
518 Ok(Self {
519 signing_key,
520 verifying_key,
521 public_key,
522 })
523 }
524
525 fn public_key(&self) -> &Self::PublicKey {
526 &self.public_key
527 }
528
529 fn sign(&self, hash: &[u8; 32]) -> Result<Self::Signature, SignError> {
530 // Sign the hash using the prehash signer (for pre-hashed messages)
531 let (signature, recovery_id): (K256Signature, RecoveryId) = self
532 .signing_key
533 .sign_prehash_recoverable(hash)
534 .map_err(|_| SignError::signature_failed("secp256k1 signing failed"))?;
535
536 // Normalize the signature to prevent malleability
537 // k256 already normalizes signatures when using sign_prehash_recoverable
538 let normalized = signature.normalize_s();
539
540 // Get the signature bytes
541 let sig_bytes = normalized.unwrap_or(signature).to_bytes();
542 let mut bytes = [0u8; 64];
543 bytes.copy_from_slice(&sig_bytes);
544
545 // Adjust recovery ID if S was normalized
546 let final_recovery_id = if normalized.is_some() {
547 // If S was normalized, flip the recovery ID
548 recovery_id.to_byte() ^ 1
549 } else {
550 recovery_id.to_byte()
551 };
552
553 Ok(Secp256k1Signature {
554 bytes,
555 recovery_id: final_recovery_id,
556 })
557 }
558}
559
560// Implement Debug for Secp256k1KeyPair without exposing the private key
561impl std::fmt::Debug for Secp256k1KeyPair {
562 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
563 f.debug_struct("Secp256k1KeyPair")
564 .field("public_key", &self.public_key)
565 .finish_non_exhaustive()
566 }
567}
568
569// ============================================================================
570// Ed25519 Public Key
571// ============================================================================
572
573/// Wrapper for ed25519 public keys.
574///
575/// Stores the 32-byte public key for ed25519 operations.
576/// Used for Solana and other ed25519-based chains.
577///
578/// # Example
579///
580/// ```rust
581/// use txgate_crypto::keypair::{KeyPair, Ed25519KeyPair};
582///
583/// let keypair = Ed25519KeyPair::generate();
584/// let pubkey = keypair.public_key();
585///
586/// // Get raw bytes (32 bytes)
587/// assert_eq!(pubkey.as_bytes().len(), 32);
588///
589/// // Get Solana address (base58 encoded)
590/// let address = pubkey.solana_address();
591/// assert!(!address.is_empty());
592/// ```
593#[derive(Clone, PartialEq, Eq)]
594pub struct Ed25519PublicKey {
595 /// The 32-byte public key
596 bytes: [u8; 32],
597}
598
599impl Ed25519PublicKey {
600 /// Create a new public key from raw bytes.
601 #[must_use]
602 pub const fn from_bytes(bytes: [u8; 32]) -> Self {
603 Self { bytes }
604 }
605
606 /// Get the raw public key bytes (32 bytes).
607 #[must_use]
608 pub const fn as_bytes(&self) -> &[u8; 32] {
609 &self.bytes
610 }
611
612 /// Derive the Solana address from this public key.
613 ///
614 /// The Solana address is simply the base58-encoded 32-byte public key.
615 ///
616 /// # Example
617 ///
618 /// ```rust
619 /// use txgate_crypto::keypair::{KeyPair, Ed25519KeyPair};
620 ///
621 /// let keypair = Ed25519KeyPair::generate();
622 /// let address = keypair.public_key().solana_address();
623 /// // Solana addresses are base58 encoded, typically 32-44 characters
624 /// assert!(address.len() >= 32 && address.len() <= 44);
625 /// ```
626 #[must_use]
627 pub fn solana_address(&self) -> String {
628 bs58::encode(&self.bytes).into_string()
629 }
630}
631
632impl AsRef<[u8]> for Ed25519PublicKey {
633 fn as_ref(&self) -> &[u8] {
634 &self.bytes
635 }
636}
637
638impl std::fmt::Debug for Ed25519PublicKey {
639 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
640 write!(f, "Ed25519PublicKey({})", hex::encode(self.bytes))
641 }
642}
643
644// ============================================================================
645// Ed25519 Signature
646// ============================================================================
647
648/// Wrapper for ed25519 signatures.
649///
650/// Contains the 64-byte signature.
651///
652/// # Example
653///
654/// ```rust
655/// use txgate_crypto::keypair::{KeyPair, Ed25519KeyPair};
656///
657/// let keypair = Ed25519KeyPair::generate();
658/// let hash = [0u8; 32];
659/// let signature = keypair.sign(&hash).expect("signing failed");
660///
661/// // Get signature bytes (64 bytes)
662/// assert_eq!(signature.as_ref().len(), 64);
663/// ```
664#[derive(Clone, PartialEq, Eq)]
665pub struct Ed25519Signature {
666 /// The 64-byte signature
667 bytes: [u8; 64],
668}
669
670impl Ed25519Signature {
671 /// Create a signature from raw bytes.
672 #[must_use]
673 pub const fn from_bytes(bytes: [u8; 64]) -> Self {
674 Self { bytes }
675 }
676}
677
678impl AsRef<[u8]> for Ed25519Signature {
679 fn as_ref(&self) -> &[u8] {
680 &self.bytes
681 }
682}
683
684impl std::fmt::Debug for Ed25519Signature {
685 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
686 write!(f, "Ed25519Signature({})", hex::encode(self.bytes))
687 }
688}
689
690// ============================================================================
691// Ed25519 Key Pair
692// ============================================================================
693
694/// Ed25519 key pair for Solana and other ed25519-based chains.
695///
696/// This key pair uses the ed25519 elliptic curve, which is the standard
697/// for Solana and some other blockchain networks.
698///
699/// # Security
700///
701/// - The signing key is stored securely using [`ed25519_dalek::SigningKey`]
702/// - The signing key implements `Zeroize` and `ZeroizeOnDrop`, ensuring secret
703/// material is automatically zeroed when dropped (the "zeroize" feature is
704/// explicitly enabled in the workspace Cargo.toml)
705/// - `Debug` output does not expose the private key
706/// - Uses ed25519-dalek for cryptographic operations
707///
708/// # Example
709///
710/// ```rust
711/// use txgate_crypto::keypair::{KeyPair, Ed25519KeyPair};
712///
713/// // Generate a new key pair
714/// let keypair = Ed25519KeyPair::generate();
715///
716/// // Or create from existing bytes
717/// let secret_bytes = [0x42u8; 32]; // Use real secret in production!
718/// let keypair = Ed25519KeyPair::from_bytes(secret_bytes)
719/// .expect("valid secret key");
720///
721/// // Sign a message hash
722/// let hash = [0u8; 32]; // Use real hash in production!
723/// let signature = keypair.sign(&hash).expect("signing succeeded");
724///
725/// // Get Solana address
726/// let address = keypair.public_key().solana_address();
727/// println!("Solana address: {address}");
728/// ```
729pub struct Ed25519KeyPair {
730 /// The signing key (private key)
731 signing_key: ed25519_dalek::SigningKey,
732 /// Cached public key wrapper
733 public_key: Ed25519PublicKey,
734}
735
736impl Ed25519KeyPair {
737 /// Create a key pair from a [`SecretKey`].
738 ///
739 /// # Errors
740 /// Returns an error if the secret key bytes are not valid.
741 ///
742 /// # Example
743 ///
744 /// ```rust
745 /// use txgate_crypto::keys::SecretKey;
746 /// use txgate_crypto::keypair::{KeyPair, Ed25519KeyPair};
747 ///
748 /// let secret = SecretKey::generate();
749 /// let keypair = Ed25519KeyPair::from_secret_key(&secret)
750 /// .expect("valid secret key");
751 /// ```
752 pub fn from_secret_key(secret: &SecretKey) -> Result<Self, SignError> {
753 Self::from_bytes(*secret.as_bytes())
754 }
755
756 /// Verify a signature against a hash using this key pair's public key.
757 ///
758 /// # Arguments
759 /// * `hash` - The 32-byte hash that was signed
760 /// * `signature` - The signature to verify
761 ///
762 /// # Returns
763 /// `true` if the signature is valid, `false` otherwise.
764 #[must_use]
765 pub fn verify(&self, hash: &[u8; 32], signature: &Ed25519Signature) -> bool {
766 use ed25519_dalek::Verifier;
767
768 let Ok(sig) = ed25519_dalek::Signature::from_slice(signature.as_ref()) else {
769 return false;
770 };
771
772 self.signing_key.verifying_key().verify(hash, &sig).is_ok()
773 }
774}
775
776impl KeyPair for Ed25519KeyPair {
777 type Signature = Ed25519Signature;
778 type PublicKey = Ed25519PublicKey;
779
780 fn generate() -> Self {
781 let secret = SecretKey::generate();
782 // Generated random keys from OsRng should always be valid ed25519 keys
783 Self::from_bytes(*secret.as_bytes())
784 .unwrap_or_else(|_| unreachable!("OsRng generated an invalid ed25519 key"))
785 }
786
787 fn from_bytes(bytes: [u8; 32]) -> Result<Self, SignError> {
788 let signing_key = ed25519_dalek::SigningKey::from_bytes(&bytes);
789 let verifying_key = signing_key.verifying_key();
790 let public_key = Ed25519PublicKey::from_bytes(verifying_key.to_bytes());
791
792 Ok(Self {
793 signing_key,
794 public_key,
795 })
796 }
797
798 fn public_key(&self) -> &Self::PublicKey {
799 &self.public_key
800 }
801
802 fn sign(&self, hash: &[u8; 32]) -> Result<Self::Signature, SignError> {
803 use ed25519_dalek::Signer;
804
805 let signature = self.signing_key.sign(hash);
806 let bytes = signature.to_bytes();
807
808 Ok(Ed25519Signature { bytes })
809 }
810}
811
812// Implement Debug for Ed25519KeyPair without exposing the private key
813impl std::fmt::Debug for Ed25519KeyPair {
814 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
815 f.debug_struct("Ed25519KeyPair")
816 .field("public_key", &self.public_key)
817 .finish_non_exhaustive()
818 }
819}
820
821// ============================================================================
822// Tests
823// ============================================================================
824
825#[cfg(test)]
826mod tests {
827 #![allow(clippy::expect_used)]
828 #![allow(clippy::uninlined_format_args)]
829 #![allow(clippy::useless_vec)]
830 #![allow(clippy::panic)]
831
832 use super::*;
833
834 // ------------------------------------------------------------------------
835 // Secp256k1KeyPair Tests
836 // ------------------------------------------------------------------------
837
838 #[test]
839 fn test_generate_produces_valid_keypair() {
840 let keypair = Secp256k1KeyPair::generate();
841
842 // Public key should have correct lengths
843 assert_eq!(keypair.public_key().compressed().len(), 33);
844 assert_eq!(keypair.public_key().uncompressed().len(), 65);
845
846 // Compressed key should start with 0x02 or 0x03
847 let prefix = keypair.public_key().compressed()[0];
848 assert!(prefix == 0x02 || prefix == 0x03);
849
850 // Uncompressed key should start with 0x04
851 assert_eq!(keypair.public_key().uncompressed()[0], 0x04);
852 }
853
854 #[test]
855 fn test_generate_produces_unique_keys() {
856 let keypair1 = Secp256k1KeyPair::generate();
857 let keypair2 = Secp256k1KeyPair::generate();
858
859 // Should generate different public keys
860 assert_ne!(
861 keypair1.public_key().compressed(),
862 keypair2.public_key().compressed()
863 );
864 }
865
866 #[test]
867 fn test_from_bytes_success() {
868 // A known valid secp256k1 private key
869 let bytes = [
870 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
871 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c,
872 0x1d, 0x1e, 0x1f, 0x20,
873 ];
874
875 let result = Secp256k1KeyPair::from_bytes(bytes);
876 assert!(result.is_ok());
877 }
878
879 #[test]
880 fn test_from_bytes_invalid_zero() {
881 // Zero is not a valid secp256k1 scalar
882 let bytes = [0u8; 32];
883 let result = Secp256k1KeyPair::from_bytes(bytes);
884 assert!(matches!(result, Err(SignError::InvalidKey)));
885 }
886
887 #[test]
888 fn test_from_secret_key() {
889 let secret = SecretKey::generate();
890 let result = Secp256k1KeyPair::from_secret_key(&secret);
891 assert!(result.is_ok());
892 }
893
894 #[test]
895 fn test_deterministic_public_key() {
896 // Same private key should always produce the same public key
897 let bytes = [0x42u8; 32];
898
899 let keypair1 = Secp256k1KeyPair::from_bytes(bytes).expect("valid key");
900 let keypair2 = Secp256k1KeyPair::from_bytes(bytes).expect("valid key");
901
902 assert_eq!(
903 keypair1.public_key().compressed(),
904 keypair2.public_key().compressed()
905 );
906 assert_eq!(
907 keypair1.public_key().uncompressed(),
908 keypair2.public_key().uncompressed()
909 );
910 }
911
912 // ------------------------------------------------------------------------
913 // Signing Tests
914 // ------------------------------------------------------------------------
915
916 #[test]
917 fn test_sign_produces_valid_signature() {
918 let keypair = Secp256k1KeyPair::generate();
919 let hash = [0x42u8; 32];
920
921 let signature = keypair.sign(&hash).expect("signing should succeed");
922
923 // Signature should be 64 bytes
924 assert_eq!(signature.as_ref().len(), 64);
925
926 // Recovery ID should be 0 or 1
927 assert!(signature.recovery_id() == 0 || signature.recovery_id() == 1);
928 }
929
930 #[test]
931 fn test_signature_is_verifiable() {
932 let keypair = Secp256k1KeyPair::generate();
933 let hash = [0x42u8; 32];
934
935 let signature = keypair.sign(&hash).expect("signing should succeed");
936
937 // Verify using keypair's verify method
938 assert!(
939 keypair.verify(&hash, &signature),
940 "signature should be valid"
941 );
942 }
943
944 #[test]
945 fn test_different_messages_produce_different_signatures() {
946 let keypair = Secp256k1KeyPair::generate();
947 let hash1 = [0x42u8; 32];
948 let hash2 = [0x43u8; 32];
949
950 let sig1 = keypair.sign(&hash1).expect("signing should succeed");
951 let sig2 = keypair.sign(&hash2).expect("signing should succeed");
952
953 // Different messages should produce different signatures
954 assert_ne!(sig1.as_ref(), sig2.as_ref());
955 }
956
957 #[test]
958 fn test_recoverable_signature_format() {
959 let keypair = Secp256k1KeyPair::generate();
960 let hash = [0x42u8; 32];
961
962 let signature = keypair.sign(&hash).expect("signing should succeed");
963 let recoverable = signature.to_recoverable_bytes();
964
965 assert_eq!(recoverable.len(), 65);
966 assert_eq!(&recoverable[..64], signature.as_ref());
967 assert_eq!(recoverable[64], signature.recovery_id());
968 }
969
970 #[test]
971 fn test_signature_r_and_s_components() {
972 let keypair = Secp256k1KeyPair::generate();
973 let hash = [0x42u8; 32];
974
975 let signature = keypair.sign(&hash).expect("signing should succeed");
976
977 // R and S should each be 32 bytes
978 assert_eq!(signature.r().len(), 32);
979 assert_eq!(signature.s().len(), 32);
980
981 // Concatenated R and S should equal the full signature
982 let mut combined = [0u8; 64];
983 combined[..32].copy_from_slice(signature.r());
984 combined[32..].copy_from_slice(signature.s());
985 assert_eq!(&combined, signature.as_ref());
986 }
987
988 // ------------------------------------------------------------------------
989 // Ethereum Address Tests
990 // ------------------------------------------------------------------------
991
992 #[test]
993 fn test_ethereum_address_length() {
994 let keypair = Secp256k1KeyPair::generate();
995 let address = keypair.public_key().ethereum_address();
996 assert_eq!(address.len(), 20);
997 }
998
999 #[test]
1000 fn test_ethereum_address_deterministic() {
1001 let bytes = [0x42u8; 32];
1002 let keypair = Secp256k1KeyPair::from_bytes(bytes).expect("valid key");
1003
1004 let address1 = keypair.public_key().ethereum_address();
1005 let address2 = keypair.public_key().ethereum_address();
1006
1007 assert_eq!(address1, address2);
1008 }
1009
1010 /// Test vector from Ethereum yellow paper / well-known test cases
1011 #[test]
1012 fn test_ethereum_address_known_vector() {
1013 // This is a well-known test private key
1014 // Private key: 0xfad9c8855b740a0b7ed4c221dbad0f33a83a49cad6b3fe8d5817ac83d38b6a19
1015 let private_key_hex = "fad9c8855b740a0b7ed4c221dbad0f33a83a49cad6b3fe8d5817ac83d38b6a19";
1016 let expected_address_hex = "96216849c49358b10257cb55b28ea603c874b05e";
1017
1018 let private_key_bytes = hex::decode(private_key_hex).expect("valid hex");
1019 let mut bytes = [0u8; 32];
1020 bytes.copy_from_slice(&private_key_bytes);
1021
1022 let keypair = Secp256k1KeyPair::from_bytes(bytes).expect("valid key");
1023 let address = keypair.public_key().ethereum_address();
1024
1025 assert_eq!(hex::encode(address), expected_address_hex);
1026 }
1027
1028 // ------------------------------------------------------------------------
1029 // Public Key Format Tests
1030 // ------------------------------------------------------------------------
1031
1032 #[test]
1033 fn test_public_key_as_ref_returns_compressed() {
1034 let keypair = Secp256k1KeyPair::generate();
1035 let pubkey = keypair.public_key();
1036
1037 // AsRef should return the compressed format
1038 assert_eq!(pubkey.as_ref(), pubkey.compressed().as_slice());
1039 }
1040
1041 #[test]
1042 fn test_public_key_debug_shows_hex() {
1043 let keypair = Secp256k1KeyPair::generate();
1044 let debug_output = format!("{:?}", keypair.public_key());
1045
1046 // Should contain the prefix "Secp256k1PublicKey("
1047 assert!(debug_output.starts_with("Secp256k1PublicKey("));
1048 // Should be hex encoded (66 characters for 33 bytes)
1049 assert!(debug_output.len() > 66);
1050 }
1051
1052 // ------------------------------------------------------------------------
1053 // Thread Safety Tests
1054 // ------------------------------------------------------------------------
1055
1056 #[test]
1057 fn test_keypair_is_send_sync() {
1058 fn assert_send_sync<T: Send + Sync>() {}
1059 assert_send_sync::<Secp256k1KeyPair>();
1060 }
1061
1062 #[test]
1063 fn test_public_key_is_send_sync() {
1064 fn assert_send_sync<T: Send + Sync>() {}
1065 assert_send_sync::<Secp256k1PublicKey>();
1066 }
1067
1068 #[test]
1069 fn test_signature_is_send_sync() {
1070 fn assert_send_sync<T: Send + Sync>() {}
1071 assert_send_sync::<Secp256k1Signature>();
1072 }
1073
1074 // ------------------------------------------------------------------------
1075 // Debug Output Tests
1076 // ------------------------------------------------------------------------
1077
1078 #[test]
1079 fn test_keypair_debug_does_not_expose_private_key() {
1080 let keypair = Secp256k1KeyPair::generate();
1081 let debug_output = format!("{:?}", keypair);
1082
1083 // Should show public key
1084 assert!(debug_output.contains("public_key"));
1085 // Should not contain the private key (indicated by finish_non_exhaustive)
1086 assert!(debug_output.contains(".."));
1087 }
1088
1089 #[test]
1090 fn test_signature_debug_shows_components() {
1091 let keypair = Secp256k1KeyPair::generate();
1092 let hash = [0x42u8; 32];
1093 let signature = keypair.sign(&hash).expect("signing should succeed");
1094
1095 let debug_output = format!("{:?}", signature);
1096
1097 assert!(debug_output.contains("r="));
1098 assert!(debug_output.contains("s="));
1099 assert!(debug_output.contains("v="));
1100 }
1101
1102 // ------------------------------------------------------------------------
1103 // Sign/Verify roundtrip tests
1104 // ------------------------------------------------------------------------
1105
1106 #[test]
1107 fn test_sign_verify_roundtrip_multiple_hashes() {
1108 let keypair = Secp256k1KeyPair::generate();
1109
1110 // Test with various hash values
1111 for i in 0u8..10 {
1112 let hash = [i; 32];
1113 let signature = keypair.sign(&hash).expect("signing should succeed");
1114
1115 // Verify using keypair's verify method
1116 assert!(
1117 keypair.verify(&hash, &signature),
1118 "signature for hash {i} should verify"
1119 );
1120 }
1121 }
1122
1123 #[test]
1124 fn test_wrong_hash_fails_verification() {
1125 let keypair = Secp256k1KeyPair::generate();
1126 let hash1 = [0x42u8; 32];
1127 let hash2 = [0x43u8; 32];
1128
1129 let signature = keypair.sign(&hash1).expect("signing should succeed");
1130
1131 // Verification with wrong hash should fail
1132 assert!(
1133 !keypair.verify(&hash2, &signature),
1134 "verification should fail with wrong hash"
1135 );
1136 }
1137
1138 #[test]
1139 fn test_ethereum_address_never_fails_hash_extraction() {
1140 // This test verifies that the defensive .get() check in ethereum_address()
1141 // always succeeds because Keccak256 always produces 32 bytes
1142 let keypair = Secp256k1KeyPair::generate();
1143 let pubkey = keypair.public_key();
1144
1145 // Call ethereum_address multiple times to ensure consistency
1146 let addr1 = pubkey.ethereum_address();
1147 let addr2 = pubkey.ethereum_address();
1148
1149 assert_eq!(addr1, addr2);
1150 assert_eq!(addr1.len(), 20);
1151 }
1152
1153 #[test]
1154 fn test_signature_normalization_both_paths() {
1155 // Test that signature normalization works correctly
1156 // We can't force a specific normalization path, but we can verify
1157 // that all signatures are in normalized form
1158 let keypair = Secp256k1KeyPair::generate();
1159
1160 for i in 0..10 {
1161 let hash = [i; 32];
1162 let signature = keypair.sign(&hash).expect("signing should succeed");
1163
1164 // Verify the signature is valid
1165 assert!(keypair.verify(&hash, &signature));
1166
1167 // Recovery ID should always be 0 or 1
1168 assert!(signature.recovery_id() <= 1);
1169 }
1170 }
1171
1172 // ========================================================================
1173 // Phase 2: Signature Normalization Coverage
1174 // ========================================================================
1175
1176 #[test]
1177 fn should_produce_normalized_signatures_with_varied_hashes() {
1178 // Arrange: Generate keypair
1179 let keypair = Secp256k1KeyPair::generate();
1180
1181 // Act & Assert: Test with many different hashes to exercise both
1182 // normalization branches (when S is already low, and when it needs normalization)
1183 for i in 0..100 {
1184 // Create varied hash patterns
1185 let mut hash = [0u8; 32];
1186 hash[0] = i;
1187 hash[31] = 255 - i;
1188
1189 let signature = keypair.sign(&hash).expect("signing should succeed");
1190
1191 // All signatures should be normalized (low-S)
1192 assert!(
1193 keypair.verify(&hash, &signature),
1194 "Normalized signature should verify"
1195 );
1196
1197 // Recovery ID should be valid (0 or 1)
1198 let recovery_id = signature.recovery_id();
1199 assert!(
1200 recovery_id <= 1,
1201 "Recovery ID should be 0 or 1, got {recovery_id}"
1202 );
1203 }
1204 }
1205
1206 #[test]
1207 fn should_handle_normalization_with_all_zero_hash() {
1208 // Arrange: All zeros hash (edge case)
1209 let keypair = Secp256k1KeyPair::generate();
1210 let hash = [0u8; 32];
1211
1212 // Act: Sign the zero hash
1213 let signature = keypair.sign(&hash).expect("signing should succeed");
1214
1215 // Assert: Signature is valid and normalized
1216 assert!(keypair.verify(&hash, &signature));
1217 assert!(signature.recovery_id() <= 1);
1218 }
1219
1220 #[test]
1221 fn should_handle_normalization_with_all_max_hash() {
1222 // Arrange: All 0xFF hash (edge case)
1223 let keypair = Secp256k1KeyPair::generate();
1224 let hash = [0xFFu8; 32];
1225
1226 // Act: Sign the max hash
1227 let signature = keypair.sign(&hash).expect("signing should succeed");
1228
1229 // Assert: Signature is valid and normalized
1230 assert!(keypair.verify(&hash, &signature));
1231 assert!(signature.recovery_id() <= 1);
1232 }
1233
1234 #[test]
1235 fn should_handle_recovery_id_flip_when_normalized() {
1236 // Arrange: Generate multiple signatures to exercise the recovery ID flip logic
1237 let keypair = Secp256k1KeyPair::generate();
1238
1239 // Act & Assert: Generate many signatures
1240 // When normalize_s() returns Some(_), recovery ID is flipped (XOR 1)
1241 // When normalize_s() returns None, recovery ID is unchanged
1242 for i in 0..50 {
1243 let hash = [i; 32];
1244 let signature = keypair.sign(&hash).expect("signing should succeed");
1245
1246 // Recovery ID should always be valid (0 or 1)
1247 assert!(signature.recovery_id() <= 1);
1248
1249 // Verify the signature with the recovery ID
1250 assert!(keypair.verify(&hash, &signature));
1251 }
1252 }
1253
1254 #[test]
1255 fn should_produce_consistent_signatures_with_same_hash() {
1256 // Arrange: Same keypair, same hash
1257 let keypair = Secp256k1KeyPair::generate();
1258 let hash = [0x42u8; 32];
1259
1260 // Act: Sign the same hash multiple times (with randomized k)
1261 let sig1 = keypair.sign(&hash).expect("signing should succeed");
1262 let sig2 = keypair.sign(&hash).expect("signing should succeed");
1263
1264 // Assert: Both should verify (but may not be identical due to random k)
1265 assert!(keypair.verify(&hash, &sig1));
1266 assert!(keypair.verify(&hash, &sig2));
1267
1268 // Both should be normalized
1269 assert!(sig1.recovery_id() <= 1);
1270 assert!(sig2.recovery_id() <= 1);
1271 }
1272
1273 #[test]
1274 fn should_handle_normalization_edge_case_patterns() {
1275 // Arrange: Test various bit patterns that might affect S normalization
1276 let keypair = Secp256k1KeyPair::generate();
1277
1278 let test_patterns = vec![
1279 [0x00u8; 32], // All zeros
1280 [0xFFu8; 32], // All ones
1281 [0xAAu8; 32], // Alternating 10101010
1282 [0x55u8; 32], // Alternating 01010101
1283 {
1284 let mut h = [0u8; 32];
1285 h[0] = 0xFF;
1286 h
1287 }, // First byte max
1288 {
1289 let mut h = [0u8; 32];
1290 h[31] = 0xFF;
1291 h
1292 }, // Last byte max
1293 ];
1294
1295 // Act & Assert: All patterns should produce valid normalized signatures
1296 for (idx, hash) in test_patterns.iter().enumerate() {
1297 let signature = keypair
1298 .sign(hash)
1299 .unwrap_or_else(|_| panic!("Pattern {idx} signing should succeed"));
1300
1301 assert!(
1302 keypair.verify(hash, &signature),
1303 "Pattern {idx} should verify"
1304 );
1305 assert!(
1306 signature.recovery_id() <= 1,
1307 "Pattern {idx} recovery ID should be valid"
1308 );
1309 }
1310 }
1311
1312 // ========================================================================
1313 // Ed25519 KeyPair Tests
1314 // ========================================================================
1315
1316 #[test]
1317 fn test_ed25519_generate_produces_valid_keypair() {
1318 let keypair = Ed25519KeyPair::generate();
1319
1320 // Public key should be 32 bytes
1321 assert_eq!(keypair.public_key().as_bytes().len(), 32);
1322 }
1323
1324 #[test]
1325 fn test_ed25519_generate_produces_unique_keys() {
1326 let keypair1 = Ed25519KeyPair::generate();
1327 let keypair2 = Ed25519KeyPair::generate();
1328
1329 // Should generate different public keys
1330 assert_ne!(
1331 keypair1.public_key().as_bytes(),
1332 keypair2.public_key().as_bytes()
1333 );
1334 }
1335
1336 #[test]
1337 fn test_ed25519_from_bytes_success() {
1338 let bytes = [0x42u8; 32];
1339 let result = Ed25519KeyPair::from_bytes(bytes);
1340 assert!(result.is_ok());
1341 }
1342
1343 #[test]
1344 fn test_ed25519_from_bytes_zero_is_valid() {
1345 // Unlike secp256k1, zero bytes are valid for ed25519
1346 let bytes = [0u8; 32];
1347 let result = Ed25519KeyPair::from_bytes(bytes);
1348 assert!(result.is_ok());
1349 }
1350
1351 #[test]
1352 fn test_ed25519_from_secret_key() {
1353 let secret = SecretKey::generate();
1354 let result = Ed25519KeyPair::from_secret_key(&secret);
1355 assert!(result.is_ok());
1356 }
1357
1358 #[test]
1359 fn test_ed25519_deterministic_public_key() {
1360 // Same private key should always produce the same public key
1361 let bytes = [0x42u8; 32];
1362
1363 let keypair1 = Ed25519KeyPair::from_bytes(bytes).expect("valid key");
1364 let keypair2 = Ed25519KeyPair::from_bytes(bytes).expect("valid key");
1365
1366 assert_eq!(
1367 keypair1.public_key().as_bytes(),
1368 keypair2.public_key().as_bytes()
1369 );
1370 }
1371
1372 #[test]
1373 fn test_ed25519_sign_produces_valid_signature() {
1374 let keypair = Ed25519KeyPair::generate();
1375 let hash = [0x42u8; 32];
1376
1377 let signature = keypair.sign(&hash).expect("signing should succeed");
1378
1379 // Ed25519 signature should be 64 bytes
1380 assert_eq!(signature.as_ref().len(), 64);
1381 }
1382
1383 #[test]
1384 fn test_ed25519_signature_is_verifiable() {
1385 let keypair = Ed25519KeyPair::generate();
1386 let hash = [0x42u8; 32];
1387
1388 let signature = keypair.sign(&hash).expect("signing should succeed");
1389
1390 // Verify using keypair's verify method
1391 assert!(
1392 keypair.verify(&hash, &signature),
1393 "signature should be valid"
1394 );
1395 }
1396
1397 #[test]
1398 fn test_ed25519_different_messages_produce_different_signatures() {
1399 let keypair = Ed25519KeyPair::generate();
1400 let hash1 = [0x42u8; 32];
1401 let hash2 = [0x43u8; 32];
1402
1403 let sig1 = keypair.sign(&hash1).expect("signing should succeed");
1404 let sig2 = keypair.sign(&hash2).expect("signing should succeed");
1405
1406 // Different messages should produce different signatures
1407 assert_ne!(sig1.as_ref(), sig2.as_ref());
1408 }
1409
1410 #[test]
1411 fn test_ed25519_wrong_hash_fails_verification() {
1412 let keypair = Ed25519KeyPair::generate();
1413 let hash1 = [0x42u8; 32];
1414 let hash2 = [0x43u8; 32];
1415
1416 let signature = keypair.sign(&hash1).expect("signing should succeed");
1417
1418 // Verification with wrong hash should fail
1419 assert!(
1420 !keypair.verify(&hash2, &signature),
1421 "verification should fail with wrong hash"
1422 );
1423 }
1424
1425 #[test]
1426 fn test_ed25519_solana_address_format() {
1427 let keypair = Ed25519KeyPair::generate();
1428 let address = keypair.public_key().solana_address();
1429
1430 // Solana addresses are base58 encoded 32-byte public keys
1431 // Typically 32-44 characters
1432 assert!(
1433 address.len() >= 32 && address.len() <= 44,
1434 "Solana address should be 32-44 characters: {address}"
1435 );
1436 }
1437
1438 #[test]
1439 fn test_ed25519_solana_address_deterministic() {
1440 let bytes = [0x42u8; 32];
1441 let keypair = Ed25519KeyPair::from_bytes(bytes).expect("valid key");
1442
1443 let address1 = keypair.public_key().solana_address();
1444 let address2 = keypair.public_key().solana_address();
1445
1446 assert_eq!(address1, address2);
1447 }
1448
1449 #[test]
1450 fn test_ed25519_sign_verify_roundtrip_multiple_hashes() {
1451 let keypair = Ed25519KeyPair::generate();
1452
1453 for i in 0u8..10 {
1454 let hash = [i; 32];
1455 let signature = keypair.sign(&hash).expect("signing should succeed");
1456
1457 assert!(
1458 keypair.verify(&hash, &signature),
1459 "signature for hash {i} should verify"
1460 );
1461 }
1462 }
1463
1464 #[test]
1465 fn test_ed25519_keypair_is_send_sync() {
1466 fn assert_send_sync<T: Send + Sync>() {}
1467 assert_send_sync::<Ed25519KeyPair>();
1468 }
1469
1470 #[test]
1471 fn test_ed25519_public_key_is_send_sync() {
1472 fn assert_send_sync<T: Send + Sync>() {}
1473 assert_send_sync::<Ed25519PublicKey>();
1474 }
1475
1476 #[test]
1477 fn test_ed25519_signature_is_send_sync() {
1478 fn assert_send_sync<T: Send + Sync>() {}
1479 assert_send_sync::<Ed25519Signature>();
1480 }
1481
1482 #[test]
1483 fn test_ed25519_keypair_debug_does_not_expose_private_key() {
1484 let keypair = Ed25519KeyPair::generate();
1485 let debug_output = format!("{:?}", keypair);
1486
1487 // Should show public key
1488 assert!(debug_output.contains("public_key"));
1489 // Should not contain the private key (indicated by finish_non_exhaustive)
1490 assert!(debug_output.contains(".."));
1491 }
1492
1493 #[test]
1494 fn test_ed25519_signature_debug_shows_hex() {
1495 let keypair = Ed25519KeyPair::generate();
1496 let hash = [0x42u8; 32];
1497 let signature = keypair.sign(&hash).expect("signing should succeed");
1498
1499 let debug_output = format!("{:?}", signature);
1500 assert!(debug_output.starts_with("Ed25519Signature("));
1501 }
1502
1503 #[test]
1504 fn test_ed25519_public_key_debug_shows_hex() {
1505 let keypair = Ed25519KeyPair::generate();
1506 let debug_output = format!("{:?}", keypair.public_key());
1507
1508 assert!(debug_output.starts_with("Ed25519PublicKey("));
1509 }
1510
1511 #[test]
1512 fn test_ed25519_public_key_as_ref_returns_bytes() {
1513 let keypair = Ed25519KeyPair::generate();
1514 let pubkey = keypair.public_key();
1515
1516 // AsRef should return the 32-byte public key
1517 assert_eq!(pubkey.as_ref().len(), 32);
1518 assert_eq!(pubkey.as_ref(), pubkey.as_bytes());
1519 }
1520
1521 #[test]
1522 fn test_ed25519_signature_from_bytes() {
1523 let bytes = [0x42u8; 64];
1524 let sig = Ed25519Signature::from_bytes(bytes);
1525 assert_eq!(sig.as_ref(), &bytes);
1526 }
1527
1528 #[test]
1529 fn test_ed25519_public_key_from_bytes() {
1530 let bytes = [0x42u8; 32];
1531 let pubkey = Ed25519PublicKey::from_bytes(bytes);
1532 assert_eq!(pubkey.as_bytes(), &bytes);
1533 }
1534
1535 #[test]
1536 fn test_ed25519_invalid_signature_fails_verification() {
1537 let keypair = Ed25519KeyPair::generate();
1538 let hash = [0x42u8; 32];
1539
1540 // Create an invalid signature (all zeros)
1541 let invalid_sig = Ed25519Signature::from_bytes([0u8; 64]);
1542
1543 // Verification should fail
1544 assert!(!keypair.verify(&hash, &invalid_sig));
1545 }
1546
1547 #[test]
1548 fn test_ed25519_known_test_vector() {
1549 // Test vector from https://ed25519.cr.yp.to/software.html
1550 // Secret key (seed): 32 bytes of 0x9d repeated
1551 let secret_bytes = [0x9du8; 32];
1552 let keypair = Ed25519KeyPair::from_bytes(secret_bytes).expect("valid key");
1553
1554 // Sign a message
1555 let message = [0u8; 32];
1556 let signature = keypair.sign(&message).expect("signing should succeed");
1557
1558 // Verify the signature
1559 assert!(keypair.verify(&message, &signature));
1560 }
1561}
1562
1563#[cfg(test)]
1564mod proptest_tests {
1565 #![allow(clippy::expect_used)]
1566
1567 use super::*;
1568 use proptest::prelude::*;
1569
1570 proptest! {
1571 #[test]
1572 fn test_sign_verify_roundtrip(hash in any::<[u8; 32]>()) {
1573 let keypair = Secp256k1KeyPair::generate();
1574 let signature = keypair.sign(&hash).expect("signing should succeed");
1575
1576 // Verify signature using keypair's verify method
1577 prop_assert!(keypair.verify(&hash, &signature));
1578 }
1579
1580 #[test]
1581 fn test_recovery_id_is_valid(hash in any::<[u8; 32]>()) {
1582 let keypair = Secp256k1KeyPair::generate();
1583 let signature = keypair.sign(&hash).expect("signing should succeed");
1584
1585 // Recovery ID should always be 0 or 1
1586 prop_assert!(signature.recovery_id() <= 1);
1587 }
1588
1589 #[test]
1590 fn test_public_key_formats_consistent(seed in any::<[u8; 32]>()) {
1591 // Skip invalid seeds (zero)
1592 if seed == [0u8; 32] {
1593 return Ok(());
1594 }
1595
1596 if let Ok(keypair) = Secp256k1KeyPair::from_bytes(seed) {
1597 let pubkey = keypair.public_key();
1598
1599 // Compressed should start with 02 or 03
1600 let prefix = pubkey.compressed()[0];
1601 prop_assert!(prefix == 0x02 || prefix == 0x03);
1602
1603 // Uncompressed should start with 04
1604 prop_assert_eq!(pubkey.uncompressed()[0], 0x04);
1605
1606 // X coordinate should be the same in both formats
1607 prop_assert_eq!(&pubkey.compressed()[1..33], &pubkey.uncompressed()[1..33]);
1608 }
1609 }
1610 }
1611}