I am trying to broadcast a Tezos transaction on the Babylon testnet.
I'm using an external military-grade hardware security module (HSM) located on a remote server. Its purpose is to never expose the private key to the outside, but it will sign payloads that I send to it and returns those to me. It uses the SECP256K1 elliptic curve methodology for signing and as such I have to use 'tz2' addresses, as they lie on the SECP256K1 curve.
For building and transmitting the transaction I use the Taquito library. The address I'm trying to send from is: https://babylonnet.tzstats.com/tz2RzEYWusHfd1ZCrEKfMFPaL4pCaYV61eeL Its public key is:
{
x: 'da17e06b2c2be5cf311eb08061681864e13dafd4bac36ba659a3821a6805bf3c',
y: '34182f94d154f0761d91712913cf317c0491219db7c986316fbaa28e56c9cbab'
}
Taquito allows you to implement your own Signer object: https://github.com/ecadlabs/taquito/blob/master/packages/taquito/src/signer/interface.ts. The custom signer connects to the HSM, the HSM signs the payload and returns a DER signature, and the custom signer adds the R and S of the DER signature to the payload in the 'sbytes' property used for broadcasting.
Here is my custom Signer class and sign function (I left out the 3 other interface functions here). Most of it is just copied from the Taquito signer: https://github.com/ecadlabs/taquito/blob/master/packages/taquito-signer/src/taquito-signer.ts
export class XtzCustomSigner implements Signer {
async sign(bytes: string, watermark?: Uint8Array) {
try {
let bb = hex2buf(bytes);
if (typeof watermark !== 'undefined') {
bb = mergebuf(watermark, bb);
}
const payload: string = bufferToHexString(Buffer.from(bb));
await sodium.ready;
const payloadHash = bufferToHexString(Buffer.from(sodium.crypto_generichash(32,
payload)))
// This line signs the payload with the HSM
const signature = await this.hsmCommunicationService.requestSign(this.modelPortfolio,
this.authKey, this.coinAccount, this._publicKey, payloadHash, this.derivationPath);
// Get R + (canonical) S from signature
const signatureBuffer = extractRSFromDERSignature(signature, true);
const sbytes = bytes + buf2hex(signatureBuffer);
const result = {
bytes: undefined,
sig: undefined,
prefixSig: b58cencode(signatureBuffer, prefix[Prefix.SPSIG]),
sbytes,
};
return result;
} catch (ex) {
console.log(ex);
throw ex;
}
}
}
This sign function is called by taquito automatically if the custom signer is passed into the Provider. The code that calls this signer:
public async buildTransaction(input: ICoinTransactionInput): Promise<void> {
// Set up connection to Babylonnet using custom signer
Tezos.setProvider({ rpc: 'https://api.tez.ie/rpc/babylonnet', signer: this.xtzCustomSigner });
// Modifies the custom signer with correct input credentials
this.setData(input.senderCredentials, input.senderCoinAccount);
const receivingAddress = 'tz2H81kw4NahMKoenoGSizWxes82B9PiLiyG';
// Here we get a fee estimate and the amount to transfer (in units of Tezos, so not the 10^-6 unit).
const { amount, estimate } = await this.adjustTransactionAmountFee(input.amount, receivingAddress);
// Here we do the transaction broadcasting
let op: TransactionOperation;
try {
op = await Tezos.contract.transfer({
to: receivingAddress,
amount,
fee: estimate.suggestedFeeMutez,
gasLimit: estimate.gasLimit,
storageLimit: estimate.storageLimit,
});
} catch (e) {
console.log(e);
}
// Wait for at least 1 confirmation of the transaction
const blockHeight = await op.confirmation(1);
}
Upon attempting to broadcast, Babylon returns the following error:
HttpResponseError {
message:
'Http error response: (500) [{"kind":"permanent","id":"proto.005-PsBabyM1.operation.invalid_signature"}]\n',
status: 500,
statusText: 'Internal Server Error',
body:
'[{"kind":"permanent","id":"proto.005-PsBabyM1.operation.invalid_signature"}]\n',
name: 'HttpResponse' }
So it says the signature is invalid. To rule some things out:
I've implemented BTC and ETH transactions using the same signing function with the HSM. Signing and broadcasting works successfully for both coins.
The HSM and the botan library that I use to verify the DER signature both don't throw an error, meaning that the public key is successfully used to verify the signature.
I am aware of the prefixes that Tezos uses. For the SECP256K1 elliptic curve you need:
- 'tz2' prefix before a Tezos address (public key hash)
- 'sppk' prefix before a Tezos public key
- 'sig' prefix before the 'sig' property
- 'spsig' prefix before the 'prefixSig' property
I use the same function to extract R and S from the signature that I use for BTC and ETH. For Tezos, I know that you need the 'Lower S' (canonical) form of the signature. Thus I add the following check if canonical is set to true:
export const extractRSFromDERSignature = (signature: string,
canonical: boolean = false): Buffer => {
let R: string;
let S: string;
// -------------
// Here is standard code not relevant right now to get R and S from the DER signature
// -------------
if (canonical) {
const bnS = new BigNumber(S);
// This is the order of the elliptic curve N
const bnN = new BigNumber('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141', 16);
// if S > N / 2
if (bnS.isGreaterThan(bnN.div(new BigNumber(2)))) {
// then S = N - S
S = bnN.minus(bnS).toString(16);
}
const RS: string = R + S;
return hexStringToBuffer(RS.toLowerCase());
}
I used the Taquito default signer to successfully sign and broadcast from a 'tz1' address to 'tz2' addresses: https://babylonnet.tzstats.com/tz1NZuMdXsXhDQHVRX1yHEyViKrkYmKB5TPw. All I did was comment out my custom signer and use the default signer and it worked. I checked the signatures' format to compare:
Signing with the taquito default signer (tz1 address, uses Edward's curve), 'sig' property is:
sighWTZ1HNXf3u5BsmFvGfWrW3rQFMYbf3htFLXiftx5PM5CemciKG6DCchVP24sJuDB6MPv67VVwtPjYnHZG7uJXz34ej6e
'prefixSig' property is:
edsighWTZ1HNXf3u5BsmFvGfWrW3rQFMYbf3htFLXiftx5PM5CemciKG6DCchVP24sJuDB6MPv67VVwtPjYnHZG7uJXz34ej6e
Signing with my custom signer, 'sig' property is:
sigsAujsNePapNNGsVotTvcKWMNNJja9B4a2FfAe8vExzFhEgEo1GTQStiif22uSA6iNxPGCGsXsRyeLHzeLbJL2y8CnYuNe
'prefixSig' property is:
spsigsAujsNePapNNGsVotTvcKWMNNJja9B4a2FfAe8vExzFhEgEo1GTQStiif22uSA6iNxPGCGsXsRyeLHzeLbJL2y8CnYuNe
The prefix is correct, the length is correct. Does anyone have an idea what the problem might be? Any tips on what parts of code I should check out to find the error?