diff --git a/src/attack/nonce_reuse.rs b/src/attack/nonce_reuse.rs index a3aa632..2d995b7 100644 --- a/src/attack/nonce_reuse.rs +++ b/src/attack/nonce_reuse.rs @@ -5,6 +5,8 @@ use crate::math::{ recover_nonce, recover_private_key, scalar_to_decimal_string, scalar_to_hex_string, }; use crate::signature::group_by_r_and_pubkey; +use k256::elliptic_curve::sec1::ToEncodedPoint; +use k256::{AffinePoint, ProjectivePoint}; pub struct NonceReuseAttack; @@ -50,15 +52,47 @@ fn try_recover_pair( sig2: &Signature, pubkey: &Option, ) -> Option { - let k = recover_nonce(&sig1.z, &sig2.z, &sig1.s, &sig2.s)?; - let priv_key = recover_private_key(&sig1.r, &sig1.s, &sig1.z, &k)?; - - Some(RecoveredKey { - private_key: priv_key, - private_key_decimal: scalar_to_decimal_string(&priv_key), - private_key_hex: scalar_to_hex_string(&priv_key), - pubkey: pubkey.clone(), - }) + if pubkey.is_some() { + // With pubkey: try both s2 polarities and verify d*G == pubkey. + // Handles mixed low-s/high-s normalization (BIP62). + for &s2 in &[sig2.s, -sig2.s] { + let k = match recover_nonce(&sig1.z, &sig2.z, &sig1.s, &s2) { + Some(k) => k, + None => continue, + }; + let priv_key = match recover_private_key(&sig1.r, &sig1.s, &sig1.z, &k) { + Some(d) => d, + None => continue, + }; + if verify_key_matches_pubkey(&priv_key, pubkey.as_ref().unwrap()) { + return Some(RecoveredKey { + private_key: priv_key, + private_key_decimal: scalar_to_decimal_string(&priv_key), + private_key_hex: scalar_to_hex_string(&priv_key), + pubkey: pubkey.clone(), + }); + } + } + None + } else { + let k = recover_nonce(&sig1.z, &sig2.z, &sig1.s, &sig2.s)?; + let priv_key = recover_private_key(&sig1.r, &sig1.s, &sig1.z, &k)?; + Some(RecoveredKey { + private_key: priv_key, + private_key_decimal: scalar_to_decimal_string(&priv_key), + private_key_hex: scalar_to_hex_string(&priv_key), + pubkey: pubkey.clone(), + }) + } +} + +fn verify_key_matches_pubkey(d: &Scalar, pubkey: &str) -> bool { + let computed = ProjectivePoint::GENERATOR * *d; + let affine: AffinePoint = computed.into(); + let compressed = hex::encode(affine.to_encoded_point(true).as_bytes()); + let uncompressed = hex::encode(affine.to_encoded_point(false).as_bytes()); + let pk = pubkey.to_lowercase(); + pk == compressed || pk == uncompressed } #[cfg(test)] @@ -116,6 +150,106 @@ mod tests { assert_eq!(recovered.private_key_decimal, expected); } + #[test] + fn test_nonce_reuse_recovery_with_pubkey_verification() { + use k256::elliptic_curve::ff::PrimeField; + + let d = Scalar::from(42u64); + let pubkey_point = (ProjectivePoint::GENERATOR * d).to_affine(); + let pubkey_hex = hex::encode(pubkey_point.to_encoded_point(true).as_bytes()); + + let k = Scalar::from(1000u64); + let z1 = Scalar::from(111u64); + let z2 = Scalar::from(222u64); + + let kg = ProjectivePoint::GENERATOR * k; + let r = Scalar::from_repr_vartime(*kg.to_affine().to_encoded_point(false).x().unwrap()) + .unwrap(); + + let k_inv = Option::::from(k.invert()).unwrap(); + let s1 = k_inv * (z1 + r * d); + let s2 = k_inv * (z2 + r * d); + + let sigs = vec![ + Signature { + r, + s: s1, + z: z1, + pubkey: Some(pubkey_hex.clone()), + timestamp: None, + kp: None, + }, + Signature { + r, + s: s2, + z: z2, + pubkey: Some(pubkey_hex.clone()), + timestamp: None, + kp: None, + }, + ]; + + let attack = NonceReuseAttack; + let vulns = attack.detect(&sigs); + assert_eq!(vulns.len(), 1); + + let recovered = attack.recover(&vulns[0]).unwrap(); + assert_eq!(recovered.private_key, d); + } + + #[test] + fn test_nonce_reuse_recovery_negated_s_with_pubkey() { + use k256::elliptic_curve::ff::PrimeField; + + let d = Scalar::from(42u64); + let pubkey_point = (ProjectivePoint::GENERATOR * d).to_affine(); + let pubkey_hex = hex::encode(pubkey_point.to_encoded_point(true).as_bytes()); + + let k = Scalar::from(1000u64); + let z1 = Scalar::from(111u64); + let z2 = Scalar::from(222u64); + + let kg = ProjectivePoint::GENERATOR * k; + let r = Scalar::from_repr_vartime(*kg.to_affine().to_encoded_point(false).x().unwrap()) + .unwrap(); + + let k_inv = Option::::from(k.invert()).unwrap(); + let s1 = k_inv * (z1 + r * d); + let s2_raw = k_inv * (z2 + r * d); + // Negate s2 to simulate mixed low-s/high-s normalization (BIP62) + let s2_negated = -s2_raw; + + let sigs = vec![ + Signature { + r, + s: s1, + z: z1, + pubkey: Some(pubkey_hex.clone()), + timestamp: None, + kp: None, + }, + Signature { + r, + s: s2_negated, + z: z2, + pubkey: Some(pubkey_hex.clone()), + timestamp: None, + kp: None, + }, + ]; + + let attack = NonceReuseAttack; + let vulns = attack.detect(&sigs); + assert_eq!(vulns.len(), 1); + + let recovered = attack.recover(&vulns[0]); + assert!( + recovered.is_some(), + "Should recover key even with negated s when pubkey is available" + ); + assert_eq!(recovered.unwrap().private_key, d); + } + #[test] fn test_no_false_positives_different_r() { let sigs = vec![