Ethereum Transaction Signing

Signing Ethereum transactions using the TSM can be done using the go-ethereum sdk; https://github.com/ethereum/go-ethereum. The following example assumes that all nodes are controlled using a single tsm.ECDSAClient and that an ECDSA key with curve secp256k1 has already been generated. Helper functions are provided below.

Ethereum address of a TSM key

pkDER, err := ecdsaClient.PublicKey(keyID, nil)
if err != nil {
	// handle error
}
pk, err := ASN1ParseSecp256k1PublicKey(pkDER)
if err != nil {
	// handle error
}
address := crypto.PubkeyToAddress(*pk)

Signing a Ethereum transaction using the TSM

signer := types.NewEIP155Signer(chainID)

// prepare unsigned transaction
txData := &types.LegacyTx{
  // ...
}
unsignedTx := types.NewTx(txData)

// generate signature
h := signer.Hash(unsignedTx)
signatureDER, recoveryID, err := ecdsaClient.Sign(keyID, nil, h[:])
if err != nil {
  // handle error
}
r, s, err := ASN1ParseSecp256k1Signature(signatureDER)
if err != nil {
	// handle error
}
signature := make([]byte, 2*32+1)
r.FillBytes(signature[0:32])
s.FillBytes(signature[32:64])
signature[64] = byte(recoveryID)

// add signature to transaction
signedTx, err := unsignedTx.WithSignature(signer, signature)
if err != nil {
	// handle error
}
import java.io.IOException;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.ProviderException;
import java.security.Security;
import java.util.List;

import org.bouncycastle.crypto.signers.StandardDSAEncoding;
import org.bouncycastle.jcajce.provider.digest.Keccak;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.web3j.crypto.RawTransaction;
import org.web3j.crypto.Sign;
import org.web3j.crypto.Sign.SignatureData;
import org.web3j.crypto.TransactionEncoder;
import org.web3j.crypto.transaction.type.TransactionType;
import org.web3j.rlp.RlpEncoder;
import org.web3j.rlp.RlpList;
import org.web3j.rlp.RlpType;
import org.web3j.tx.ChainIdLong;
import org.web3j.utils.Convert;
import org.web3j.utils.Numeric;

import com.sepior.tsm.sdk.NativeSdk;

// This uses the open source project web3j (https://github.com/web3j/web3j) to create Eth
// transactions and Bouncy Castle for hash function support.
public class CreateEip155Transaction {
    public static void main(String[] args) throws Exception {
        String transferAmount = args[0]; // E.g. "1.0" 
        BigInteger nonce = new BigInteger(args[1]);
        BigInteger gasPrice = Convert.toWei(args[2], Convert.Unit.GWEI).toBigInteger();
        BigInteger gasLimit = new BigInteger(args[3]);

        Security.addProvider(new BouncyCastleProvider());
        NativeSdk sdk = getNativeSdk();
        final String keyID = sdk.ecdsaKeygenWithSessionID(sdk.generateSessionID(),"secp256k1");
        byte[] publicKey = sdk.ecdsaPublicKey(keyID, null);
        String address = Utils.genEtheriumAddress(publicKey);
        
        // See https://docs.web3j.io/4.9.7/transactions/transfer_eth/
        System.out.println("Creating transaction with:");
        System.out.println(" - to:        " + address);
        System.out.println(" - amount:    " + transferAmount);
        System.out.println(" - nonce:     " + nonce);
        System.out.println(" - gas price: " + gasPrice);
        System.out.println(" - gas limit: " + gasLimit);
        BigInteger value = Convert.toWei(transferAmount, Convert.Unit.ETHER).toBigInteger();
        RawTransaction transaction = RawTransaction.createEtherTransaction(nonce, gasPrice, gasLimit, address, value);
        
        byte[] toBeSigned = TransactionEncoder.encode(transaction, ChainIdLong.MAINNET);
        
        byte[] hash = sha3Digest(toBeSigned);
        Sign.SignatureData signature = sign(sdk, keyID, hash);

        byte[] signedTransaction = encode(transaction, signature);
        System.out.println("Transaction: " + Numeric.toHexString(signedTransaction));
    }

    static NativeSdk getNativeSdk() {
        NativeSdk s = new NativeSdk();
        s.init(getCredentialsJson());
        s.setNetworkTimeout(30);
        
        return s;
    }

    static String getCredentialsJson() {
        final String path = System.getenv("CREDENTIALS_PATH");
        if (path == null) {
            throw new ProviderException(String.format("Credentials string not set for %s", "CREDENTIALS_PATH"));
        }
        try {
            return new String(Files.readAllBytes(Paths.get(path)), StandardCharsets.UTF_8);
        } catch(IOException e){
            throw new ProviderException("Example could not read Credentials file " + path, e);
        }
    }

    static Sign.SignatureData sign(NativeSdk sdk, String keyID, byte[] hash) throws IOException {
        final byte[] sigP = sdk.ecdsaPartialSign(sdk.generateSessionID(), keyID, null, hash);
        
        final byte[][] ps = new byte[][]{sigP};
        final NativeSdk.SignatureWithRecoveryID fs = sdk.ecdsaFinalize(ps);
        
        BigInteger[] parsed = StandardDSAEncoding.INSTANCE.decode(null, fs.getSignature());
        
        int recId = 27+fs.getRecoveryID();
        SignatureData sigData = new Sign.SignatureData((byte) recId, parsed[0].toByteArray(), parsed[1].toByteArray());
        return TransactionEncoder.createEip155SignatureData(sigData, ChainIdLong.MAINNET);
    }

    public static byte[] sha3Digest(byte[] input) {
        Keccak.DigestKeccak sha3 = new Keccak.Digest256();
        return sha3.digest(input);
    }

    private static byte[] encode(RawTransaction rawTransaction, Sign.SignatureData signatureData) {
        // This method is taken from Transaction encoder, where it is private
        List<RlpType> values = TransactionEncoder.asRlpValues(rawTransaction, signatureData);
        RlpList rlpList = new RlpList(values);
        byte[] encoded = RlpEncoder.encode(rlpList);
        if (!rawTransaction.getType().equals(TransactionType.LEGACY)) {
            return ByteBuffer.allocate(encoded.length + 1)
                    .put(rawTransaction.getType().getRlpType())
                    .put(encoded)
                    .array();
        }
        return encoded;
    }
}

Helper functions

func ASN1ParseSecp256k1PublicKey(publicKey []byte) (*ecdsa.PublicKey, error) {
	publicKeyInfo := struct {
		Raw       asn1.RawContent
		Algorithm pkix.AlgorithmIdentifier
		PublicKey asn1.BitString
	}{}

	postfix, err := asn1.Unmarshal(publicKey, &publicKeyInfo)
	if err != nil || len(postfix) > 0 {
		return nil, errors.New("invalid or incomplete ASN1")
	}
	// check params

	pk, err := btcec.ParsePubKey(publicKeyInfo.PublicKey.Bytes)
	if err != nil {
		return nil, err
	}
	return pk.ToECDSA(), nil
}

func ASN1ParseSecp256k1Signature(signature []byte) (r, s *big.Int, err error) {
	sig := struct {
		R *big.Int
		S *big.Int
	}{}
	postfix, err := asn1.Unmarshal(signature, &sig)
	if err != nil {
		return nil, nil, err
	}
	if len(postfix) > 0 {
		return nil, nil, errors.New("trailing bytes for ASN1 ecdsa signature")
	}
	return sig.R, sig.S, nil
}