Vulnerability Overview
- Repository: XRPLF/xrpl-rust
- Type: Cryptographic Weakness
- Severity: LOW
- Confidence: HIGH
- Location:
src/core/keypairs/algorithms.rs (lines 275–277)
Description
Secp256k1::sign strips leading 0 characters from secp256k1 private keys before parsing them. For rare keys that begin with 00, the function can parse or sign with the wrong secret, producing signatures that fail verification against the intended public key.
Detailed Analysis
Code Context
The core::keypairs module exposes public helpers for seed generation, keypair derivation, signing, and signature verification. sign() dispatches to Ed25519 or secp256k1 based on the key prefix, then forwards the caller-supplied private key into the algorithm implementation.
Vulnerability Details
Secp256k1::sign should parse the full 32-byte private key string as provided. Instead, it removes all leading 0 characters with trim_start_matches('0') before calling SecretKey::from_str, which can change the key being signed with or cause parse failure.
Impact
Affected callers can generate signatures that do not match the intended secp256k1 public key, causing message verification or XRPL transaction submission to fail. The issue affects signing integrity and reliability, but does not expose secrets or grant new access.
Exploit Scenario
An application calls xrpl::core::keypairs::sign() with a secp256k1 private key whose first byte is 0x00. The signer strips the leading zeroes, signs with a different secret, and returns a valid-looking signature that later fails verification and is rejected.
Reachability Evidence
Verdict: REACHABLE
Summary: Both conditions are satisfied: callers can reach the vulnerable secp256k1 signing path by directly calling the public sign API with a 00... private key, and such keys are a rare but real edge case occurring with approximately 1/256 probability.
Condition Assessments
MET: The caller invokes the public sign function (src/core/keypairs/mod.rs) directly with a secp256k1 private key string whose first byte (key[0]) is exactly 0x00, bypassing the derive_keypair validation path which would have caught the mismatch.
src/core/keypairs/mod.rs exposes pub fn sign(message, private_key) as a public API that accepts any caller-supplied key string and directly dispatches based on the first two characters. _get_algorithm_engine_from_key treats any key not starting with "ED" as secp256k1, so a "00..." key is routed to Secp256k1::sign. That signer trims leading '0' characters before parsing, while derive_keypair separately signs a fixed test message and verifies it against the derived public key before returning the pair. Therefore, a caller can invoke the public sign API directly with a secp256k1 key beginning with 0x00 and avoid the derive_keypair validation check that would have rejected a mismatched key/signature pair.
src/core/keypairs/mod.rs:231-234
pub fn sign(message: &[u8], private_key: &str) -> XRPLCoreResult<String> {
let module = _get_algorithm_engine_from_key(private_key);
Ok(hex::encode_upper(module.sign(message, private_key)?))
}
MET: The probability of a randomly-derived SECP256K1 key having key[0] = 0x00 is approximately 1/256 (≈ 0.4%), making this an edge-case affecting a small but real subset of keys.
The secp256k1 derivation code produces a 32-byte candidate secret by taking sha512_first_half(...), and _is_secret_valid only constrains that 32-byte value to the normal secp256k1 scalar range 1..=curve_order; it does not filter or special-case a leading 0x00 byte. For randomly generated seeds, generate_seed uses Hc128Rng::from_entropy(), so the resulting secp256k1 secrets are derived from fresh random entropy. Because the private key is a 32-byte big-endian value and nothing in the code excludes first-byte 0x00, the chance that the most significant byte equals 0x00 is approximately 1 out of 256 (about 0.39%). The curve-order bound is very close to 2^256, so this remains an edge case affecting a small but real subset of keys.
src/core/keypairs/algorithms.rs:128-137
fn _get_secret(
input: &[u8],
phase: &Secp256k1Phase,
) -> XRPLCoreResult<[u8; SHA512_HASH_LENGTH]> {
for raw_root in 0..SECP256K1_SEQUENCE_MAX {
let root = (raw_root as u32).to_be_bytes();
let candidate = sha512_first_half(&Self::_candidate_merger(input, &root, phase));
if Self::_is_secret_valid(candidate) {
return Ok(candidate);
Vulnerability Overview
src/core/keypairs/algorithms.rs(lines 275–277)Description
Secp256k1::signstrips leading0characters from secp256k1 private keys before parsing them. For rare keys that begin with00, the function can parse or sign with the wrong secret, producing signatures that fail verification against the intended public key.Detailed Analysis
Code Context
The
core::keypairsmodule exposes public helpers for seed generation, keypair derivation, signing, and signature verification.sign()dispatches to Ed25519 or secp256k1 based on the key prefix, then forwards the caller-supplied private key into the algorithm implementation.Vulnerability Details
Secp256k1::signshould parse the full 32-byte private key string as provided. Instead, it removes all leading0characters withtrim_start_matches('0')before callingSecretKey::from_str, which can change the key being signed with or cause parse failure.Impact
Affected callers can generate signatures that do not match the intended secp256k1 public key, causing message verification or XRPL transaction submission to fail. The issue affects signing integrity and reliability, but does not expose secrets or grant new access.
Exploit Scenario
An application calls
xrpl::core::keypairs::sign()with a secp256k1 private key whose first byte is0x00. The signer strips the leading zeroes, signs with a different secret, and returns a valid-looking signature that later fails verification and is rejected.Reachability Evidence
Verdict: REACHABLE
Summary: Both conditions are satisfied: callers can reach the vulnerable secp256k1 signing path by directly calling the public
signAPI with a00...private key, and such keys are a rare but real edge case occurring with approximately 1/256 probability.Condition Assessments
MET: The caller invokes the public
signfunction (src/core/keypairs/mod.rs) directly with a secp256k1 private key string whose first byte (key[0]) is exactly 0x00, bypassing thederive_keypairvalidation path which would have caught the mismatch.src/core/keypairs/mod.rsexposespub fn sign(message, private_key)as a public API that accepts any caller-supplied key string and directly dispatches based on the first two characters._get_algorithm_engine_from_keytreats any key not starting with"ED"as secp256k1, so a"00..."key is routed toSecp256k1::sign. That signer trims leading'0'characters before parsing, whilederive_keypairseparately signs a fixed test message and verifies it against the derived public key before returning the pair. Therefore, a caller can invoke the publicsignAPI directly with a secp256k1 key beginning with0x00and avoid thederive_keypairvalidation check that would have rejected a mismatched key/signature pair.src/core/keypairs/mod.rs:231-234MET: The probability of a randomly-derived SECP256K1 key having key[0] = 0x00 is approximately 1/256 (≈ 0.4%), making this an edge-case affecting a small but real subset of keys.
The secp256k1 derivation code produces a 32-byte candidate secret by taking
sha512_first_half(...), and_is_secret_validonly constrains that 32-byte value to the normal secp256k1 scalar range1..=curve_order; it does not filter or special-case a leading0x00byte. For randomly generated seeds,generate_seedusesHc128Rng::from_entropy(), so the resulting secp256k1 secrets are derived from fresh random entropy. Because the private key is a 32-byte big-endian value and nothing in the code excludes first-byte0x00, the chance that the most significant byte equals0x00is approximately 1 out of 256 (about 0.39%). The curve-order bound is very close to2^256, so this remains an edge case affecting a small but real subset of keys.src/core/keypairs/algorithms.rs:128-137