Skip to content

Commit 01bde4e

Browse files
authored
Merge pull request open-wallet-standard#233 from open-wallet-standard/fix/xrpl-leading-zero-key
fix(xrpl): sign the digest with k256 so leading-zero keys don't fail
2 parents 0e62e5f + 844ae33 commit 01bde4e

1 file changed

Lines changed: 57 additions & 34 deletions

File tree

  • ows/crates/ows-signer/src/chains

ows/crates/ows-signer/src/chains/xrpl.rs

Lines changed: 57 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,17 @@ use k256::ecdsa::signature::hazmat::PrehashSigner;
44
use k256::ecdsa::SigningKey;
55
use k256::PublicKey;
66
use ows_core::ChainType;
7+
use sha2::{Digest, Sha512};
78
use 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

Comments
 (0)