Invalid SUI Signature Error - ECDSA Signing in IC Canister for SUI Transactions

I’m building a canister that supports SUI transactions. I’m getting an “Invalid signature was given to the function” error when trying to execute SUI transactions using the IC canister’s ECDSA signing capabilities.

I am getting following errors:
Execute response: {“jsonrpc”:“2.0”,“id”:1,“error”:{“code”:-32602,“message”:“Invalid signature was given to the function”}}

The signature appears to be generated correctly (98 bytes total: 1 byte scheme flag + 64 bytes signature + 33 bytes compressed public key), but SUI RPC rejects it.

I’m using the IC management canister’s sign_with_ecdsa function to sign SUI transactions. The signature construction follows this pattern:

// Create intent message for SUI (scope + version + app_id + tx_data)
let mut intent_message = Vec::new();
intent_message.push(0x00); // TransactionData intent scope
intent_message.push(0x00); // intent version  
intent_message.push(0x00); // Sui app ID
intent_message.extend_from_slice(&tx_bytes_decoded);

// Hash with Blake2b-256 (SUI requirement)
let message_hash: [u8; 32] = Blake2b256::digest(&intent_message).into();

// Sign using IC ECDSA
let (signature_bytes, recovery_id) = wallet.sign_with_ecdsa(message_hash).await;

// Construct SUI signature format
let mut sui_signature = Vec::new();
sui_signature.push(0x00); // ECDSA secp256k1 scheme flag
sui_signature.extend_from_slice(&signature_bytes); // 64-byte signature  
sui_signature.extend_from_slice(&compressed_public_key); // 33-byte compressed public key

I am using SUI Devnet with unsafe_paySui method

Hi @Harshal0902,
How do you get the public key? In general can you share also

  • A minimal example we could try to use to reproduce
  • A sample of an invalid signature, together with the corresponding message and public key.
1 Like

I get the public key is derived using IC’s threshold ECDSA system with BIP-32 derivation:

// 1. Get the canister's root ECDSA public key
pub async fn lazy_call_ecdsa_public_key() -> EcdsaPublicKey {
    let key_id = EcdsaKeyId {
        curve: EcdsaCurve::Secp256k1,
        name: "dfx_test_key".to_string(), // or "test_key_1" for testnet
    };
    
    let (response,) = ecdsa_public_key(EcdsaPublicKeyArgument {
        canister_id: None,
        derivation_path: vec![],
        key_id,
    }).await.unwrap();
    
    EcdsaPublicKey::from(response)
}

// 2. Derive a user-specific public key using BIP-32
fn derive_public_key(owner: &Principal, public_key: &EcdsaPublicKey) -> EcdsaPublicKey {
    let derivation_path = DerivationPath::new(vec![
        DerivationIndex(vec![1u8]), // Schema version
        DerivationIndex(owner.as_slice().to_vec()), // User principal
    ]);
    
    public_key.derive_new_public_key(&derivation_path).unwrap()
}

// 3. Generate SUI address from the derived public key
pub fn sui_address(&self) -> String {
    let compressed_public_key = self.derived_public_key.as_ref().serialize_sec1(true); // 33 bytes
    
    let mut address_input = vec![0x01]; // SUI signature scheme flag for ECDSA secp256k1
    address_input.extend_from_slice(&compressed_public_key);
    
    let hash = Blake2b256::digest(&address_input);
    format!("0x{}", hex::encode(hash.as_slice()))
}

You can reproduce the error following this:

// Cargo.toml dependencies
[dependencies]
ic-cdk = "0.17"
candid = "0.10"
blake2 = "0.10"
base64 = "0.21"
serde_json = "1.0"
ic-crypto-ecdsa-secp256k1 = { git = "https://github.com/dfinity/ic", tag = "release-2024-06-26_23-01-base", package = "ic-crypto-ecdsa-secp256k1" }

// lib.rs - Minimal example
use ic_cdk::api::management_canister::ecdsa::*;
use blake2::{Blake2b, Digest};
use candid::Principal;

type Blake2b256 = Blake2b<blake2::digest::consts::U32>;

#[ic_cdk::update]
async fn test_sui_signature() -> String {
    let caller = ic_cdk::caller();
    
    // 1. Get derived public key (simplified)
    let key_id = EcdsaKeyId {
        curve: EcdsaCurve::Secp256k1,
        name: "dfx_test_key".to_string(),
    };
    
    let derivation_path = vec![
        vec![1u8], // schema
        caller.as_slice().to_vec(), // user
    ];
    
    // 2. Create a simple test message
    let test_message = b"Hello SUI from IC!";
    let message_hash: [u8; 32] = Blake2b256::digest(test_message).into();
    
    // 3. Sign with ECDSA
    let (signature_result,) = sign_with_ecdsa(SignWithEcdsaArgument {
        message_hash: message_hash.to_vec(),
        derivation_path,
        key_id,
    }).await.unwrap();
    
    let signature: [u8; 64] = signature_result.signature.try_into().unwrap();
    
    // 4. Get the public key for this derivation path
    let (pubkey_result,) = ecdsa_public_key(EcdsaPublicKeyArgument {
        canister_id: None,
        derivation_path,
        key_id,
    }).await.unwrap();
    
    // 5. Create SUI signature format
    let mut sui_signature = Vec::new();
    sui_signature.push(0x00); // SUI ECDSA scheme flag
    sui_signature.extend_from_slice(&signature);
    sui_signature.extend_from_slice(&pubkey_result.public_key);
    
    format!(
        "Message: {}\nHash: {}\nSignature: {}\nPublic Key: {}\nSUI Signature: {}",
        hex::encode(test_message),
        hex::encode(message_hash),
        hex::encode(signature),
        hex::encode(&pubkey_result.public_key),
        base64::encode(&sui_signature)
    )
}

Sample Invalid Signature Data

From my actual failing transaction:

Transaction Bytes (base64):

AAACAAgA4fUFAAAAAAAgUdoFiq9vJg3BBLWMxUmGDZ/7xSnBNBAneEefmCAehyQCAgABAQAAAQEDAAAAAAEBAK+b61C7IUbKCdgVVKjl3EH0t0MKr4tQ1YbuOTzrJfYKAaYKdjKQqe/xCQC3AKrIn3tv/2fGTynQLucZx80SVO4rBgAAAAAAAAAg7dGP6n7nD1cLDbxdrdXaZcEDS62CKY+e2KAH0bxGjKOvm+tQuyFGygnYFVSo5dxB9LdDCq+LUNWG7jk86yX2CugDAAAAAAAA6AMAAAAAAAAA

Intent Message (what I’m actually signing):

000000 + [decoded transaction bytes above]

Where 000000 = scope(0x00) + version(0x00) + app_id(0x00)

Message Hash (Blake2b-256 of intent message):

a7c4e8f2b3d1a6c9e5f8b2a4d7c3e9f1b8a5c2d6e3f7a1b9c4d8e2f5a8b1c6d9

(This is a sample - I can provide the actual hash from logs)

Raw ECDSA Signature (64 bytes):

46bd2de544b95b4495c0db997eec300f5188ee4ec9f7f3cdb7e86b2446b1a9297f0b6782448ba7dd4bc6e0d845fe25cb0c3b94e5a18a2b31a8e9e04ff4722c89

Compressed Public Key (33 bytes):

02e038c1c559c3ff138a184c3b3142c2ed195fed04d3ebfc3b905374e476d8db3d

Final SUI Signature (98 bytes, base64 encoded):

AEa9LeVEuVtElcDbmX7sMA9RiO5Oyffzzbfoa0RrGpKX8LZ4JEi6fdS8bg2EX+Jcsw85TloYorMajp4E/0ciyJAuA4wcdlnD/xOKGEwzsUQsLtGV/tBNPr/DuQU3TkdtPf=

Expected SUI Address:

0xafa5eb50bb2146ca09d81554a8e5dc41f4b7430aaf8b50d586ee393ceb25f60a

The signature verification passes in my canister using ic-crypto-ecdsa-secp256k1::PublicKey::verify_signature_prehashed(), but SUI RPC rejects it with “Invalid signature”.

Thanks, that’s helpful. I didn’t manage to look into it yet, but I stumbled on this documentation, which reports that the flag for ECDSA over secp256k1 is 0x01, while your examples uses 0x00. Can you maybe check this first?

Tried with above changes, still getting invalid signature error.

Ok, I think I understand what is going on, or at least I uncovered something wrong. According to this SUI documentation, the message to be signed is the blake2-hash of the intent message, however ECDSA still uses SHA-2 (as it is standard) as the underlying hashing algorithm of the signature scheme. Since the system API offloads the hashing of the message to the caller, you need to hash the message (i.e. the blake2-hashed intent) with SHA-2 before calling the system API.

To verify that the signature is then valid, you can either use verify_signature with the blake2-hashed intent, or verify_signature_prehashed the the SHA-2 digest.

Another thing I noticed is that in the example sig you reported, the message hash does not seem to be the correct blake2 hash of your intent message. What I computed is the following 819fddb968e803cd7d19d49a023a3b5a871d1a942ea4f270ea057bbb69efbae0. So there may be some other issue elsewhere.