@@ -12,9 +12,11 @@ use alloy_signer_local::{
1212} ;
1313use alloy_sol_types:: SolValue ;
1414use k256:: {
15- ecdsa:: SigningKey ,
15+ FieldBytes , Scalar ,
16+ ecdsa:: { SigningKey , hazmat} ,
1617 elliptic_curve:: { bigint:: ArrayEncoding , sec1:: ToEncodedPoint } ,
1718} ;
19+
1820use p256:: ecdsa:: {
1921 Signature as P256Signature , SigningKey as P256SigningKey , signature:: hazmat:: PrehashSigner ,
2022} ;
@@ -51,6 +53,16 @@ impl Cheatcode for sign_0Call {
5153 }
5254}
5355
56+ impl Cheatcode for signWithNonceUnsafeCall {
57+ fn apply ( & self , _state : & mut Cheatcodes ) -> Result {
58+ let pk: U256 = self . privateKey ;
59+ let digest: B256 = self . digest ;
60+ let nonce: U256 = self . nonce ;
61+ let sig: alloy_primitives:: Signature = sign_with_nonce ( & pk, & digest, & nonce) ?;
62+ Ok ( encode_full_sig ( sig) )
63+ }
64+ }
65+
5466impl Cheatcode for signCompact_0Call {
5567 fn apply ( & self , _state : & mut Cheatcodes ) -> Result {
5668 let Self { wallet, digest } = self ;
@@ -241,6 +253,86 @@ fn sign(private_key: &U256, digest: &B256) -> Result<alloy_primitives::Signature
241253 Ok ( sig)
242254}
243255
256+ /// Signs `digest` on secp256k1 using a user-supplied ephemeral nonce `k` (no RFC6979).
257+ /// - `private_key` and `nonce` must be in (0, n)
258+ /// - `digest` is a 32-byte prehash.
259+ ///
260+ /// # Warning
261+ ///
262+ /// Use [`sign_with_nonce`] with extreme caution!
263+ /// Reusing the same nonce (`k`) with the same private key in ECDSA will leak the private key.
264+ /// Always generate `nonce` with a cryptographically secure RNG, and never reuse it across
265+ /// signatures.
266+ fn sign_with_nonce (
267+ private_key : & U256 ,
268+ digest : & B256 ,
269+ nonce : & U256 ,
270+ ) -> Result < alloy_primitives:: Signature > {
271+ let d_scalar: Scalar =
272+ <Scalar as k256:: elliptic_curve:: PrimeField >:: from_repr ( private_key. to_be_bytes ( ) . into ( ) )
273+ . into_option ( )
274+ . ok_or_else ( || fmt_err ! ( "invalid private key scalar" ) ) ?;
275+ if bool:: from ( d_scalar. is_zero ( ) ) {
276+ return Err ( fmt_err ! ( "private key cannot be 0" ) ) ;
277+ }
278+
279+ let k_scalar: Scalar =
280+ <Scalar as k256:: elliptic_curve:: PrimeField >:: from_repr ( nonce. to_be_bytes ( ) . into ( ) )
281+ . into_option ( )
282+ . ok_or_else ( || fmt_err ! ( "invalid nonce scalar" ) ) ?;
283+ if bool:: from ( k_scalar. is_zero ( ) ) {
284+ return Err ( fmt_err ! ( "nonce cannot be 0" ) ) ;
285+ }
286+
287+ let mut z = [ 0u8 ; 32 ] ;
288+ z. copy_from_slice ( digest. as_slice ( ) ) ;
289+ let z_fb: FieldBytes = FieldBytes :: from ( z) ;
290+
291+ // Hazmat signing using the scalar `d` (SignPrimitive is implemented for `Scalar`)
292+ // Note: returns (Signature, Option<RecoveryId>)
293+ let ( sig_raw, recid_opt) =
294+ <Scalar as hazmat:: SignPrimitive < k256:: Secp256k1 > >:: try_sign_prehashed (
295+ & d_scalar, k_scalar, & z_fb,
296+ )
297+ . map_err ( |e| fmt_err ! ( "sign_prehashed failed: {e}" ) ) ?;
298+
299+ // Enforce low-s; if mirrored, parity flips (we’ll account for it below if we use recid)
300+ let ( sig_low, flipped) =
301+ if let Some ( norm) = sig_raw. normalize_s ( ) { ( norm, true ) } else { ( sig_raw, false ) } ;
302+
303+ let r_u256 = U256 :: from_be_bytes ( sig_low. r ( ) . to_bytes ( ) . into ( ) ) ;
304+ let s_u256 = U256 :: from_be_bytes ( sig_low. s ( ) . to_bytes ( ) . into ( ) ) ;
305+
306+ // Determine v parity in {0,1}
307+ let v_parity = if let Some ( id) = recid_opt {
308+ let mut v = id. to_byte ( ) & 1 ;
309+ if flipped {
310+ v ^= 1 ;
311+ }
312+ v
313+ } else {
314+ // Fallback: choose parity by recovery to expected address
315+ let expected_addr = {
316+ let sk: SigningKey = parse_private_key ( private_key) ?;
317+ alloy_signer:: utils:: secret_key_to_address ( & sk)
318+ } ;
319+ // Try v = 0
320+ let cand0 = alloy_primitives:: Signature :: new ( r_u256, s_u256, false ) ;
321+ if cand0. recover_address_from_prehash ( digest) . ok ( ) == Some ( expected_addr) {
322+ return Ok ( cand0) ;
323+ }
324+ // Try v = 1
325+ let cand1 = alloy_primitives:: Signature :: new ( r_u256, s_u256, true ) ;
326+ if cand1. recover_address_from_prehash ( digest) . ok ( ) == Some ( expected_addr) {
327+ return Ok ( cand1) ;
328+ }
329+ return Err ( fmt_err ! ( "failed to determine recovery id for signature" ) ) ;
330+ } ;
331+
332+ let y_parity = v_parity != 0 ;
333+ Ok ( alloy_primitives:: Signature :: new ( r_u256, s_u256, y_parity) )
334+ }
335+
244336fn sign_with_wallet (
245337 state : & mut Cheatcodes ,
246338 signer : Option < Address > ,
@@ -393,6 +485,7 @@ fn derive_wallets<W: Wordlist>(
393485mod tests {
394486 use super :: * ;
395487 use alloy_primitives:: { FixedBytes , hex:: FromHex } ;
488+ use k256:: elliptic_curve:: Curve ;
396489 use p256:: ecdsa:: signature:: hazmat:: PrehashVerifier ;
397490
398491 #[ test]
@@ -438,4 +531,69 @@ mod tests {
438531 let result = sign_p256 ( & U256 :: ZERO , & digest) ;
439532 assert_eq ! ( result. err( ) . unwrap( ) . to_string( ) , "private key cannot be 0" ) ;
440533 }
534+
535+ #[ test]
536+ fn test_sign_with_nonce_varies_and_recovers ( ) {
537+ // Given a fixed private key and digest
538+ let pk_u256: U256 = U256 :: from ( 1u64 ) ;
539+ let digest = FixedBytes :: from_hex (
540+ "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" ,
541+ )
542+ . unwrap ( ) ;
543+
544+ // Two distinct nonces
545+ let n1: U256 = U256 :: from ( 123u64 ) ;
546+ let n2: U256 = U256 :: from ( 456u64 ) ;
547+
548+ // Sign with both nonces
549+ let sig1 = sign_with_nonce ( & pk_u256, & digest, & n1) . expect ( "sig1" ) ;
550+ let sig2 = sign_with_nonce ( & pk_u256, & digest, & n2) . expect ( "sig2" ) ;
551+
552+ // (r,s) must differ when nonce differs
553+ assert ! (
554+ sig1. r( ) != sig2. r( ) || sig1. s( ) != sig2. s( ) ,
555+ "signatures should differ with different nonces"
556+ ) ;
557+
558+ // ecrecover must yield the address for both signatures
559+ let sk = parse_private_key ( & pk_u256) . unwrap ( ) ;
560+ let expected = alloy_signer:: utils:: secret_key_to_address ( & sk) ;
561+
562+ assert_eq ! ( sig1. recover_address_from_prehash( & digest) . unwrap( ) , expected) ;
563+ assert_eq ! ( sig2. recover_address_from_prehash( & digest) . unwrap( ) , expected) ;
564+ }
565+
566+ #[ test]
567+ fn test_sign_with_nonce_zero_nonce_errors ( ) {
568+ // nonce = 0 should be rejected
569+ let pk_u256: U256 = U256 :: from ( 1u64 ) ;
570+ let digest = FixedBytes :: from_hex (
571+ "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb" ,
572+ )
573+ . unwrap ( ) ;
574+ let n0: U256 = U256 :: ZERO ;
575+
576+ let err = sign_with_nonce ( & pk_u256, & digest, & n0) . unwrap_err ( ) ;
577+ let msg = err. to_string ( ) ;
578+ assert ! ( msg. contains( "nonce cannot be 0" ) , "unexpected error: {msg}" ) ;
579+ }
580+
581+ #[ test]
582+ fn test_sign_with_nonce_nonce_ge_order_errors ( ) {
583+ // nonce >= n should be rejected
584+ use k256:: Secp256k1 ;
585+ // Curve order n as U256
586+ let n_u256 = U256 :: from_be_slice ( & Secp256k1 :: ORDER . to_be_byte_array ( ) ) ;
587+
588+ let pk_u256: U256 = U256 :: from ( 1u64 ) ;
589+ let digest = FixedBytes :: from_hex (
590+ "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" ,
591+ )
592+ . unwrap ( ) ;
593+
594+ // Try exactly n (>= n invalid)
595+ let err = sign_with_nonce ( & pk_u256, & digest, & n_u256) . unwrap_err ( ) ;
596+ let msg = err. to_string ( ) ;
597+ assert ! ( msg. contains( "invalid nonce scalar" ) , "unexpected error: {msg}" ) ;
598+ }
441599}
0 commit comments