tECDSA Signature Verification Returns Different Sender Address Each Time

After reviewing the tECDSA documents and examples, I understand that the sign_with_ecdsa method is signing using the key from the canister root key or a key obtained through further derivation.

So in theory, as in the tECDSA Rust example, this sign function:

async fn sign(message: String) -> Result<SignatureReply, String> {
    let request = SignWithECDSA {
        message_hash: sha256(&message).to_vec(),
        derivation_path: vec![],
        key_id: EcdsaKeyIds::TestKeyLocalDevelopment.to_key_id(),
    };

    let (response,): (SignWithECDSAReply,) = ic_cdk::api::call::call_with_payment(
        mgmt_canister_id(),
        "sign_with_ecdsa", 
        (request,),
        25_000_000_000,
    )
    .await
    .map_err(|e| format!("sign_with_ecdsa failed {}", e.1))?;

    Ok(SignatureReply {
        signature_hex: format!("0x{}", hex::encode(&response.signature)),
    })
}

should be signed using the public key generated by this ecdsa_public_key method, given that I am not changing the deviation path between the two functions and using them in the same canister.

async fn public_key() -> Result<PublicKeyReply, String> {
    let request = ECDSAPublicKey {
        canister_id: None,
        derivation_path: vec![],
        key_id: EcdsaKeyIds::TestKeyLocalDevelopment.to_key_id(),
    };

    let (res,): (ECDSAPublicKeyReply,) =
        ic_cdk::call(mgmt_canister_id(), "ecdsa_public_key", (request,))
            .await
            .map_err(|e| format!("ecdsa_public_key failed {}", e.1))?;

    Ok(PublicKeyReply {
        public_key_hex: hex::encode(&res.public_key),
    })
}

In my code, I convert the public key to an Ethereum address. However, when I deploy this and attempt to send an Ethereum transaction, I always receive an “insufficient funds to pay for gas” error. I initially assumed the Ethereum address of the public key generated by my canister did not have enough funds.

To confirm, I used ethers to verify a signature (received from the sign function) against the message I passed in (matching the one sent to the sign function). Surprisingly, this returned a different Ethereum address each time.

I assume that there could be a possibility that the sign function is signing a different public key each time.

  1. Should I be explicit with the canister ID in the request to sign_with_ecdsa?
  2. Is there anything else that I could be overlooking?

Hi @princess_eth

I will try to go over your points

should be signed using the public key generated by this ecdsa_public_key method,

Here I would like to clarify a few things to ensure there is no misunderstanding. The public key doesn’t sign anything. What should be retrieved here is the public key whose corresponding “private key was used to sign the message” (what actually happens is that each replica uses its share of the private key to sign the message and then broadcast its signature share. Once a replica has enough signature shares it can combine them into a standard looking ECDSA signature).

To confirm, I used ethers to verify a signature (received from the sign function) against the message I passed in (matching the one sent to the sign function). Surprisingly, this returned a different Ethereum address each time.

I’m not entirely sure what you mean here. Do you mean that the library you used (ethers) did validate the signature but it recognized the signed messages as being sent from a different Ethereum address each time? If that’s the case, then most likely this is because the public key changed between your different trials and that’s most likely the case because your canister ID changed. Did you maybe redeploy each time between your trials?

I assume that there could be a possibility that the sign function is signing a different public key each time.

The public key should remain stable with your canister ID (and chosen curve). Could you maybe double check whether your canister ID changed between your trials?

  1. Should I be explicit with the canister ID in the request to sign_with_ecdsa?

This should not be necessary, the Rust doc states

pub struct EcdsaPublicKeyArgument {
    /// Canister id, default to the canister id of the caller if None.
    pub canister_id: Option<CanisterId>,
   ...
}
2 Likes