2

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?

1
  • Are you sure that you derived the public key hash in the correct manner, because if that is wrong, then it will return invalid signature as well.
    – Tjappo
    Feb 29, 2020 at 12:16

2 Answers 2

2

I ran your code, and found out that your payload hash is not correct; these lines of code:

const payload: string = bufferToHexString(Buffer.from(bb));
await sodium.ready;
const payloadHash = bufferToHexString(Buffer.from(sodium.crypto_generichash(32, 
       payload)))

. The function sodium.crypto_generichash involving the hashing of a uint8array and the hashing of a string do not yield the same result. If you convert the payload to a uint8array, it does work. In other words, you can change the lines above to:

await sodium.ready;
const payloadHash = bufferToHexString(Buffer.from(sodium.crypto_generichash(32, 
       bb)))

. You do not have to convert the bb (uint8array) to a buffer and then to a hex string.

1
  • Can't believe it was actually something that simple :) Mar 1, 2020 at 22:25
2

Util Link: base58.ml

The pre-apply RPC call validates the signature prefix, while the inject operation call doesn't.

You should use the specific curve prefix.

sig([4, 130, 43]) # sig(96) generic signature (Used after the operation injection)

edsig([9, 245, 205, 134, 18]) # edsig(99) ED25519 signature (Used for tz1 signatures)

spsig1([13, 115, 101, 19, 63]) # spsig1(99) SECP256K1 signature (Used for tz2 signatures)

p2sig([54, 240, 44, 52]) # p2sig(98) SECP256R1 signature (Used for tz3 signatures)

1
  • Apologies, the signature examples I posted were the generic 'sig' property values returned by the Signer's 'sign' function. I just now added the 'prefixSig' property values. This line of code -> prefixSig: b58cencode(signatureBuffer, prefix[Prefix.SPSIG]) does add [13, 115, 101, 19, 63] to the signature. Feb 28, 2020 at 20:00

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.