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
}
Updated 11 months ago