44//!
55//! # Security: Signature Malleability
66//!
7- //! This implementation uses the `k256` crate which produces normalized (low-S)
8- //! ECDSA signatures by default. This prevents signature malleability attacks
9- //! where an attacker could create an alternative valid signature `(r, -s mod n)`
10- //! for the same message.
7+ //! This implementation enforces **low-S only** signatures to match the Aptos
8+ //! blockchain's on-chain verification, which rejects high-S signatures to
9+ //! prevent signature malleability attacks.
1110//!
12- //! When parsing external signatures via [`Secp256k1Signature::from_bytes`], the
13- //! `k256` crate accepts both low-S and high-S signatures. If strict low-S
14- //! enforcement is required, callers should normalize or reject high-S signatures.
11+ //! - **Signing** always produces low-S signatures (the `k256` crate normalizes
12+ //! by default).
13+ //! - **Parsing** (`from_bytes`, `from_hex`) rejects high-S signatures.
14+ //! - **Verification** also rejects high-S signatures as a defense-in-depth
15+ //! measure.
1516
1617use crate :: crypto:: traits:: { PublicKey , Signature , Signer , Verifier } ;
1718use crate :: error:: { AptosError , AptosResult } ;
@@ -125,18 +126,29 @@ impl Secp256k1PrivateKey {
125126 }
126127 }
127128
128- /// Signs a message (pre-hashed with SHA256) and returns the signature.
129+ /// Signs a message (pre-hashed with SHA256) and returns a low-S signature.
130+ ///
131+ /// The `k256` crate produces low-S signatures by default. An additional
132+ /// normalization step is included as defense-in-depth to guarantee Aptos
133+ /// on-chain compatibility.
129134 pub fn sign ( & self , message : & [ u8 ] ) -> Secp256k1Signature {
130135 // Hash the message with SHA256 first (standard for ECDSA)
131136 let hash = crate :: crypto:: sha2_256 ( message) ;
132137 let signature: K256Signature = self . inner . sign ( & hash) ;
133- Secp256k1Signature { inner : signature }
138+ // SECURITY: Ensure low-S (defense-in-depth; k256 should already do this)
139+ let normalized = signature. normalize_s ( ) . unwrap_or ( signature) ;
140+ Secp256k1Signature { inner : normalized }
134141 }
135142
136- /// Signs a pre-hashed message directly.
143+ /// Signs a pre-hashed message directly and returns a low-S signature.
144+ ///
145+ /// The `k256` crate produces low-S signatures by default. An additional
146+ /// normalization step is included as defense-in-depth.
137147 pub fn sign_prehashed ( & self , hash : & [ u8 ; 32 ] ) -> Secp256k1Signature {
138148 let signature: K256Signature = self . inner . sign ( hash) ;
139- Secp256k1Signature { inner : signature }
149+ // SECURITY: Ensure low-S (defense-in-depth; k256 should already do this)
150+ let normalized = signature. normalize_s ( ) . unwrap_or ( signature) ;
151+ Secp256k1Signature { inner : normalized }
140152 }
141153}
142154
@@ -234,11 +246,21 @@ impl Secp256k1PublicKey {
234246
235247 /// Verifies a signature against a message.
236248 ///
249+ /// # Security
250+ ///
251+ /// Rejects high-S signatures before verification, matching Aptos on-chain
252+ /// behavior. This is a defense-in-depth check; signatures created through
253+ /// this SDK's `from_bytes` are already guaranteed to be low-S.
254+ ///
237255 /// # Errors
238256 ///
239- /// Returns [`AptosError::SignatureVerificationFailed`] if the signature is invalid or does not match the message.
257+ /// Returns [`AptosError::SignatureVerificationFailed`] if the signature has
258+ /// a high-S value, is invalid, or does not match the message.
240259 pub fn verify ( & self , message : & [ u8 ] , signature : & Secp256k1Signature ) -> AptosResult < ( ) > {
241- // Hash the message with SHA256 first
260+ // SECURITY: Reject high-S signatures (matches aptos-core behavior)
261+ if signature. inner . normalize_s ( ) . is_some ( ) {
262+ return Err ( AptosError :: SignatureVerificationFailed ) ;
263+ }
242264 let hash = crate :: crypto:: sha2_256 ( message) ;
243265 self . inner
244266 . verify ( & hash, & signature. inner )
@@ -247,14 +269,24 @@ impl Secp256k1PublicKey {
247269
248270 /// Verifies a signature against a pre-hashed message.
249271 ///
272+ /// # Security
273+ ///
274+ /// Rejects high-S signatures before verification, matching Aptos on-chain
275+ /// behavior.
276+ ///
250277 /// # Errors
251278 ///
252- /// Returns [`AptosError::SignatureVerificationFailed`] if the signature is invalid or does not match the hash.
279+ /// Returns [`AptosError::SignatureVerificationFailed`] if the signature has
280+ /// a high-S value, is invalid, or does not match the hash.
253281 pub fn verify_prehashed (
254282 & self ,
255283 hash : & [ u8 ; 32 ] ,
256284 signature : & Secp256k1Signature ,
257285 ) -> AptosResult < ( ) > {
286+ // SECURITY: Reject high-S signatures (matches aptos-core behavior)
287+ if signature. inner . normalize_s ( ) . is_some ( ) {
288+ return Err ( AptosError :: SignatureVerificationFailed ) ;
289+ }
258290 self . inner
259291 . verify ( hash, & signature. inner )
260292 . map_err ( |_| AptosError :: SignatureVerificationFailed )
@@ -346,11 +378,18 @@ pub struct Secp256k1Signature {
346378impl Secp256k1Signature {
347379 /// Creates a signature from raw bytes (64 bytes, r || s).
348380 ///
381+ /// # Security
382+ ///
383+ /// Rejects high-S signatures to match Aptos on-chain verification behavior.
384+ /// The Aptos VM only accepts low-S (canonical) ECDSA signatures to prevent
385+ /// signature malleability attacks.
386+ ///
349387 /// # Errors
350388 ///
351389 /// Returns [`AptosError::InvalidSignature`] if:
352390 /// - The byte slice length is not exactly 64 bytes
353391 /// - The bytes do not represent a valid Secp256k1 signature
392+ /// - The signature has a high-S value (not canonical)
354393 pub fn from_bytes ( bytes : & [ u8 ] ) -> AptosResult < Self > {
355394 if bytes. len ( ) != SECP256K1_SIGNATURE_LENGTH {
356395 return Err ( AptosError :: InvalidSignature ( format ! (
@@ -361,6 +400,15 @@ impl Secp256k1Signature {
361400 }
362401 let signature = K256Signature :: from_slice ( bytes)
363402 . map_err ( |e| AptosError :: InvalidSignature ( e. to_string ( ) ) ) ?;
403+ // SECURITY: Reject high-S signatures. Aptos on-chain verification only
404+ // accepts low-S (canonical) signatures to prevent malleability.
405+ // normalize_s() returns Some(_) if the signature was high-S.
406+ if signature. normalize_s ( ) . is_some ( ) {
407+ return Err ( AptosError :: InvalidSignature (
408+ "high-S signature rejected: Aptos requires low-S (canonical) ECDSA signatures"
409+ . into ( ) ,
410+ ) ) ;
411+ }
364412 Ok ( Self { inner : signature } )
365413 }
366414
@@ -550,6 +598,58 @@ mod tests {
550598 assert ! ( result. is_err( ) ) ;
551599 }
552600
601+ #[ test]
602+ fn test_high_s_signature_rejected ( ) {
603+ use k256:: elliptic_curve:: ops:: Neg ;
604+
605+ // Sign a message (produces low-S)
606+ let private_key = Secp256k1PrivateKey :: generate ( ) ;
607+ let signature = private_key. sign ( b"test message" ) ;
608+
609+ // Construct high-S by negating the S component: S' = n - S
610+ let low_s_sig = k256:: ecdsa:: Signature :: from_slice ( & signature. to_bytes ( ) ) . unwrap ( ) ;
611+ let ( r, s) = low_s_sig. split_scalars ( ) ;
612+ let neg_s = s. neg ( ) ;
613+ let high_s_sig = k256:: ecdsa:: Signature :: from_scalars ( r, neg_s) . unwrap ( ) ;
614+ // Confirm it really is high-S (normalize_s returns Some for high-S)
615+ assert ! (
616+ high_s_sig. normalize_s( ) . is_some( ) ,
617+ "constructed signature should be high-S"
618+ ) ;
619+ let high_s_bytes = high_s_sig. to_bytes ( ) ;
620+
621+ // from_bytes should reject high-S
622+ let result = Secp256k1Signature :: from_bytes ( & high_s_bytes) ;
623+ assert ! ( result. is_err( ) , "high-S signature should be rejected" ) ;
624+ assert ! (
625+ result
626+ . unwrap_err( )
627+ . to_string( )
628+ . contains( "high-S signature rejected" ) ,
629+ "error message should mention high-S rejection"
630+ ) ;
631+
632+ // Verify should also reject high-S (defense-in-depth via inner field)
633+ let sig_with_high_s = Secp256k1Signature { inner : high_s_sig } ;
634+ let public_key = private_key. public_key ( ) ;
635+ let result = public_key. verify ( b"test message" , & sig_with_high_s) ;
636+ assert ! ( result. is_err( ) , "verify should reject high-S signature" ) ;
637+ }
638+
639+ #[ test]
640+ fn test_signing_always_produces_low_s ( ) {
641+ // Run multiple iterations to increase confidence
642+ for _ in 0 ..20 {
643+ let private_key = Secp256k1PrivateKey :: generate ( ) ;
644+ let signature = private_key. sign ( b"test low-s" ) ;
645+ // normalize_s returns None if already low-S
646+ assert ! (
647+ signature. inner. normalize_s( ) . is_none( ) ,
648+ "signing must always produce low-S signatures"
649+ ) ;
650+ }
651+ }
652+
553653 #[ test]
554654 fn test_json_serialization_public_key ( ) {
555655 let private_key = Secp256k1PrivateKey :: generate ( ) ;
0 commit comments