@@ -4,10 +4,17 @@ use k256::ecdsa::signature::hazmat::PrehashSigner;
44use k256:: ecdsa:: SigningKey ;
55use k256:: PublicKey ;
66use ows_core:: ChainType ;
7+ use sha2:: { Digest , Sha512 } ;
78use xrpl:: core:: binarycodec:: { decode as xrpl_decode, encode as xrpl_encode} ;
8- use xrpl:: core:: keypairs:: {
9- derive_classic_address, CryptoImplementation , Secp256k1 as XrplSecp256k1 ,
10- } ;
9+ use xrpl:: core:: keypairs:: derive_classic_address;
10+
11+ /// XRPL hash: the first 32 bytes of SHA-512 ("SHA512Half").
12+ fn sha512_half ( data : & [ u8 ] ) -> [ u8 ; 32 ] {
13+ let hash = Sha512 :: digest ( data) ;
14+ hash[ ..32 ]
15+ . try_into ( )
16+ . expect ( "SHA-512 output is always 64 bytes" )
17+ }
1118
1219/// XRPL chain signer (secp256k1).
1320///
@@ -77,11 +84,11 @@ impl ChainSigner for XrplSigner {
7784 /// `encode(tx)` — the serialized transaction fields with no hash prefix.
7885 ///
7986 /// Internally prepends the XRPL single-signing prefix `STX\0` (0x53545800),
80- /// then delegates to `xrpl::core::keypairs::Secp256k1::sign` which computes
81- /// SHA512-half and produces a DER-encoded secp256k1 signature.
87+ /// computes the SHA512-half digest, and signs it with secp256k1 (k256) to
88+ /// produce a DER-encoded signature.
8289 ///
83- /// Returns a `SignOutput` with the DER signature and the compressed public key
84- /// (33 bytes), both required by `encode_signed_transaction` .
90+ /// Returns a `SignOutput` carrying the DER signature; the `SigningPubKey` is
91+ /// already embedded in `tx_bytes`, so no public key is returned here .
8592 fn sign_transaction (
8693 & self ,
8794 private_key : & [ u8 ] ,
@@ -93,28 +100,18 @@ impl ChainSigner for XrplSigner {
93100 ) ) ;
94101 }
95102
96- // Validate private key before signing.
97- SigningKey :: from_slice ( private_key)
98- . map_err ( |e| SignerError :: InvalidPrivateKey ( e. to_string ( ) ) ) ?;
99-
100- // STX\0 (0x53545800) is the XRPL single-signing hash prefix. It is prepended
101- // to the serialized fields before SHA512-half, matching the XRPL signing spec.
103+ // STX\0 (0x53545800) is the XRPL single-signing hash prefix, prepended to
104+ // the serialized fields before the SHA512-half digest that gets signed.
102105 let mut prefixed = Vec :: with_capacity ( 4 + tx_bytes. len ( ) ) ;
103106 prefixed. extend_from_slice ( & [ 0x53 , 0x54 , 0x58 , 0x00 ] ) ;
104107 prefixed. extend_from_slice ( tx_bytes) ;
105108
106- // xrpl-rust's Secp256k1::sign hashes with SHA512-half internally.
107- // The key format expected is "00"-prefixed uppercase hex (secp256k1 convention).
108- let privkey_hex = format ! ( "00{}" , hex:: encode_upper( private_key) ) ;
109- let sig_bytes = XrplSecp256k1
110- . sign ( & prefixed, & privkey_hex)
111- . map_err ( |e| SignerError :: SigningFailed ( e. to_string ( ) ) ) ?;
112-
113- Ok ( SignOutput {
114- signature : sig_bytes,
115- recovery_id : None ,
116- public_key : None ,
117- } )
109+ // Sign the SHA512-half digest directly with k256 (low-S, DER), the same path
110+ // as `sign`. We deliberately do not route through xrpl-rust's keypair signer:
111+ // it trims a leading "00" off the key hex, which also strips a leading zero
112+ // byte of the scalar and corrupts roughly 1 in 256 keys into InvalidSecretKey.
113+ let digest = sha512_half ( & prefixed) ;
114+ self . sign ( private_key, & digest)
118115 }
119116
120117 /// Encode a fully-signed XRPL transaction ready for broadcast.
@@ -169,15 +166,6 @@ mod tests {
169166 use super :: * ;
170167 use crate :: hd:: HdDeriver ;
171168 use crate :: mnemonic:: Mnemonic ;
172- use sha2:: { Digest , Sha512 } ;
173-
174- /// XRPL hash function: first 32 bytes of SHA-512.
175- /// Used only in tests to verify sign_transaction's internal hashing.
176- fn sha512_half ( data : & [ u8 ] ) -> [ u8 ; 32 ] {
177- let hash = Sha512 :: digest ( data) ;
178- hash[ ..32 ] . try_into ( ) . expect ( "sha512 output is 64 bytes" )
179- }
180-
181169 const ABANDON_PHRASE : & str = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" ;
182170
183171 /// Known test private key (32 bytes).
@@ -288,6 +276,41 @@ mod tests {
288276 assert ! ( signer. sign_transaction( & privkey, b"" ) . is_err( ) ) ;
289277 }
290278
279+ #[ test]
280+ fn test_sign_transaction_leading_zero_scalar_key ( ) {
281+ // Regression: a secp256k1 private key whose scalar begins with a 0x00 byte
282+ // must still sign. The previous XRPL keypair path trimmed that leading zero
283+ // along with the "00" type prefix, producing a short key and an
284+ // InvalidSecretKey error for roughly 1 in 256 derived keys.
285+ let mut privkey = [ 0u8 ; 32 ] ;
286+ for ( i, b) in privkey. iter_mut ( ) . enumerate ( ) . skip ( 1 ) {
287+ * b = ( i as u8 ) . wrapping_mul ( 7 ) . wrapping_add ( 1 ) ;
288+ }
289+ assert_eq ! ( privkey[ 0 ] , 0x00 , "test key must have a leading zero byte" ) ;
290+
291+ let signer = XrplSigner ;
292+ let result = signer. sign_transaction ( & privkey, b"leading_zero_scalar_tx" ) ;
293+ assert ! (
294+ result. is_ok( ) ,
295+ "leading-zero-scalar key must sign: {:?}" ,
296+ result. err( )
297+ ) ;
298+ let sig = result. unwrap ( ) ;
299+ assert_eq ! ( sig. signature[ 0 ] , 0x30 , "expected DER signature tag" ) ;
300+
301+ // Prove the signature is cryptographically valid, not just well-formed: it
302+ // must verify against the key's public key over the same SHA512-half digest.
303+ use k256:: ecdsa:: signature:: hazmat:: PrehashVerifier ;
304+ let mut prefixed = vec ! [ 0x53 , 0x54 , 0x58 , 0x00 ] ;
305+ prefixed. extend_from_slice ( b"leading_zero_scalar_tx" ) ;
306+ let digest = sha512_half ( & prefixed) ;
307+ let der = k256:: ecdsa:: Signature :: from_der ( & sig. signature ) . expect ( "valid DER" ) ;
308+ let sk = SigningKey :: from_slice ( & privkey) . unwrap ( ) ;
309+ sk. verifying_key ( )
310+ . verify_prehash ( & digest, & der)
311+ . expect ( "signature must verify against the derived public key" ) ;
312+ }
313+
291314 #[ test]
292315 fn test_sign_transaction_equals_sign_of_sha512_half ( ) {
293316 // sign_transaction(privkey, bytes) must equal sign(privkey, sha512_half(STX\0 || bytes))
0 commit comments