Skip to content

Commit f9a3192

Browse files
committed
test: add tests to compare old and new secp256k1 implementations
1 parent 9de6acc commit f9a3192

File tree

3 files changed

+156
-6
lines changed

3 files changed

+156
-6
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

stacks-common/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ sha2 = { version = "0.10" }
7575

7676
[dev-dependencies]
7777
proptest = "1.6.0"
78+
stacks_common_v3_1_00_13 = { package = "stacks-common", git = "https://github.com/stacks-network/stacks-core.git", rev="8a79aaa7df0f13dfc5ab0d0d0bcb8201c90bcba2", features = ["testing", "default"]}
7879

7980
[target.'cfg(windows)'.dev-dependencies]
8081
winapi = { version = "0.3", features = ["fileapi", "processenv", "winnt"] }

stacks-common/src/util/secp256k1.rs

Lines changed: 154 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -250,12 +250,22 @@ impl Secp256k1PublicKey {
250250
.to_secp256k1_recoverable()
251251
.ok_or("Invalid signature: failed to decode recoverable signature")?;
252252

253-
let recovered_key = K256VerifyingKey::recover_from_prehash(
254-
msg,
255-
&recoverable_sig.signature,
256-
recoverable_sig.recovery_id,
257-
)
258-
.map_err(|_| "Invalid signature: failed to recover public key")?;
253+
let RecoverableSignature {
254+
signature,
255+
recovery_id,
256+
} = recoverable_sig;
257+
258+
let normalized_signature = signature.normalize_s();
259+
let (signature_ref, recovery_id) = if let Some(normalized) = normalized_signature.as_ref() {
260+
let flipped_recovery =
261+
K256RecoveryId::new(!recovery_id.is_y_odd(), recovery_id.is_x_reduced());
262+
(normalized, flipped_recovery)
263+
} else {
264+
(&signature, recovery_id)
265+
};
266+
267+
let recovered_key = K256VerifyingKey::recover_from_prehash(msg, signature_ref, recovery_id)
268+
.map_err(|_| "Invalid signature: failed to recover public key")?;
259269

260270
Ok(Secp256k1PublicKey {
261271
key: recovered_key,
@@ -536,7 +546,12 @@ pub fn secp256k1_verify(
536546

537547
#[cfg(test)]
538548
mod tests {
549+
use proptest::prelude::*;
539550
use rand::RngCore;
551+
use stacks_common_v3_1_00_13::types::{
552+
PrivateKey as LegacyPrivateKey, PublicKey as LegacyPublicKey,
553+
};
554+
use stacks_common_v3_1_00_13::util::secp256k1 as legacy;
540555

541556
use super::*;
542557
use crate::util::get_epoch_time_ms;
@@ -1023,4 +1038,137 @@ mod tests {
10231038
panic!("High-s signature should normalize to low-s");
10241039
}
10251040
}
1041+
1042+
fn is_high_s_signature(sig_bytes: &[u8]) -> bool {
1043+
assert_eq!(sig_bytes.len(), 64);
1044+
let signature =
1045+
K256Signature::from_slice(sig_bytes).expect("signature bytes should form valid r,s");
1046+
signature.normalize_s().is_some()
1047+
}
1048+
1049+
fn to_high_s_rsv(signature_rsv: &[u8]) -> Vec<u8> {
1050+
assert_eq!(signature_rsv.len(), 65);
1051+
if is_high_s_signature(&signature_rsv[..64]) {
1052+
return signature_rsv.to_vec();
1053+
}
1054+
1055+
let low_sig =
1056+
K256Signature::from_slice(&signature_rsv[..64]).expect("valid signature bytes");
1057+
let (r, s) = (low_sig.r(), low_sig.s());
1058+
let high_sig =
1059+
K256Signature::from_scalars(*r, -*s).expect("valid signature when flipping s");
1060+
let mut signature = signature_rsv.to_vec();
1061+
let high_bytes = high_sig.to_bytes();
1062+
signature[..64].copy_from_slice(high_bytes.as_slice());
1063+
signature[64] ^= 0x01;
1064+
1065+
assert!(is_high_s_signature(&signature[..64]));
1066+
signature
1067+
}
1068+
1069+
proptest! {
1070+
#[test]
1071+
fn prop_from_seed_matches_legacy(seed in proptest::collection::vec(any::<u8>(), 1..128)) {
1072+
let new_sk = Secp256k1PrivateKey::from_seed(&seed);
1073+
let legacy_sk = legacy::Secp256k1PrivateKey::from_seed(&seed);
1074+
1075+
prop_assert_eq!(new_sk.to_hex(), legacy_sk.to_hex());
1076+
1077+
let new_pk = Secp256k1PublicKey::from_private(&new_sk);
1078+
let legacy_pk = legacy::Secp256k1PublicKey::from_private(&legacy_sk);
1079+
1080+
prop_assert_eq!(new_pk.to_hex(), legacy_pk.to_hex());
1081+
}
1082+
1083+
#[test]
1084+
fn prop_low_s_paths_match_legacy(
1085+
seed in proptest::collection::vec(any::<u8>(), 1..128),
1086+
msg in any::<[u8; 32]>()
1087+
) {
1088+
let new_sk = Secp256k1PrivateKey::from_seed(&seed);
1089+
let legacy_sk = legacy::Secp256k1PrivateKey::from_seed(&seed);
1090+
1091+
let new_pk = Secp256k1PublicKey::from_private(&new_sk);
1092+
let legacy_pk = legacy::Secp256k1PublicKey::from_private(&legacy_sk);
1093+
1094+
let sig_new = new_sk.sign(&msg).expect("new signature");
1095+
let sig_legacy = legacy_sk.sign(&msg).expect("legacy signature");
1096+
1097+
prop_assert_eq!(sig_new.to_rsv(), sig_legacy.to_rsv());
1098+
1099+
prop_assert!(new_pk.verify(&msg, &sig_new).expect("new verify low-S"));
1100+
prop_assert!(legacy_pk.verify(&msg, &sig_legacy).expect("legacy verify low-S"));
1101+
1102+
let recovered_new = Secp256k1PublicKey::recover_to_pubkey(&msg, &sig_new)
1103+
.expect("new recover");
1104+
let recovered_legacy = legacy::Secp256k1PublicKey::recover_to_pubkey(&msg, &sig_legacy)
1105+
.expect("legacy recover");
1106+
prop_assert_eq!(recovered_new.to_hex(), recovered_legacy.to_hex());
1107+
1108+
let sig_rsv = sig_new.to_rsv();
1109+
let pk_bytes_new = new_pk.to_bytes_compressed();
1110+
let pk_bytes_legacy = legacy_pk.to_bytes();
1111+
1112+
let recover_new = secp256k1_recover(&msg, &sig_rsv);
1113+
let recover_legacy = legacy::secp256k1_recover(&msg, &sig_rsv);
1114+
prop_assert!(recover_new.is_ok());
1115+
prop_assert!(recover_legacy.is_ok());
1116+
prop_assert_eq!(recover_new.unwrap(), recover_legacy.unwrap());
1117+
1118+
let verify_new = secp256k1_verify(&msg, &sig_rsv, &pk_bytes_new);
1119+
let verify_legacy = legacy::secp256k1_verify(&msg, &sig_rsv, &pk_bytes_legacy);
1120+
prop_assert!(verify_new.is_ok());
1121+
prop_assert!(verify_legacy.is_ok());
1122+
}
1123+
1124+
#[test]
1125+
fn prop_high_s_paths_match_legacy(
1126+
seed in proptest::collection::vec(any::<u8>(), 1..128),
1127+
msg in any::<[u8; 32]>()
1128+
) {
1129+
let new_sk = Secp256k1PrivateKey::from_seed(&seed);
1130+
let legacy_sk = legacy::Secp256k1PrivateKey::from_seed(&seed);
1131+
1132+
let new_pk = Secp256k1PublicKey::from_private(&new_sk);
1133+
let legacy_pk = legacy::Secp256k1PublicKey::from_private(&legacy_sk);
1134+
1135+
let sig_new = new_sk.sign(&msg).expect("new signature");
1136+
1137+
let high_s_rsv = to_high_s_rsv(&sig_new.to_rsv());
1138+
prop_assert!(is_high_s_signature(&high_s_rsv[..64]));
1139+
1140+
let mut high_s_vrs = high_s_rsv.clone();
1141+
high_s_vrs.rotate_right(1);
1142+
1143+
let high_s_new = MessageSignature::from_raw(&high_s_vrs);
1144+
let high_s_legacy = legacy::MessageSignature::from_raw(&high_s_vrs);
1145+
1146+
let verify_new = new_pk.verify(&msg, &high_s_new);
1147+
let verify_legacy = legacy_pk.verify(&msg, &high_s_legacy);
1148+
prop_assert_eq!(
1149+
verify_new.map_err(|_| ()),
1150+
verify_legacy.map_err(|_| ())
1151+
);
1152+
if let Ok(valid) = verify_new {
1153+
prop_assert!(!valid);
1154+
}
1155+
1156+
let recovered_new = Secp256k1PublicKey::recover_to_pubkey(&msg, &high_s_new)
1157+
.map(|pk| pk.to_hex())
1158+
.map_err(|_| ());
1159+
let recovered_legacy =
1160+
legacy::Secp256k1PublicKey::recover_to_pubkey(&msg, &high_s_legacy)
1161+
.map(|pk| pk.to_hex())
1162+
.map_err(|_| ());
1163+
prop_assert_eq!(recovered_new, recovered_legacy);
1164+
1165+
let recover_new = secp256k1_recover(&msg, &high_s_rsv).map_err(|_| ());
1166+
let recover_legacy = legacy::secp256k1_recover(&msg, &high_s_rsv).map_err(|_| ());
1167+
prop_assert_eq!(recover_new, recover_legacy);
1168+
1169+
let verify_new = secp256k1_verify(&msg, &high_s_rsv, &new_pk.to_bytes_compressed());
1170+
let verify_legacy = legacy::secp256k1_verify(&msg, &high_s_rsv, &legacy_pk.to_bytes());
1171+
prop_assert_eq!(verify_new.is_ok(), verify_legacy.is_ok());
1172+
}
1173+
}
10261174
}

0 commit comments

Comments
 (0)