coinbase/mesh-sdk-go

ECDSA signatures on 20 byte data

fireduck64 opened this issue ยท 10 comments

Is your feature request related to a problem? Please describe.

I am working on adding Rosetta API support for my coin, Snowblossom.
In Snowblossom, we use ECDSA signatures but the signed data is 20 bytes long.

The rosetta-cli, using rosetta-sdk-go ends up using go-ethereum for ECDSA signatures. The go-ethereum is hard coded to expect signed data to be 20 bytes. This isn't easy to change in go-ethereum, they are passing around a special data structure that is 8 64-bit integers for storing the 256-bit messages and keys. To make that change the message length would have to be plumbed through many layers.

When do a check:construction with rosetta-cli I get:

Command Failed: unable to to sign payload for 0: sign: unable to sign: invalid message length, need 32 bytes: unable to sign payloads: unable to create transaction

Describe the solution you'd like

I would like rosetta-cli to support signing 20-byte messages with ECDSA. I would hope that maybe now there is a cleaner ECDSA implementation somewhere that could be used rather than the go-ethereum one.

Describe alternatives you've considered

As an alternative, we will eventually be changing our signature method to sign 32-byte messages (as is normal) but I don't expect this will be done any time soon, as it will be consensus breaking change and we like to schedule those way out.

Another alternative would be to add RSA or DSA or DSTU4145 support to rosetta-sdk-go. I suspect adding RSA would be the easiest change. These are also supported by Snowblossom.

Additional context

Our WIP Rosetta implementation: https://github.com/snowblossomcoin/rosesnow

Thanks for reaching out @fireduck64 (love the username btw)!

I'm going to run this up the "cryptography flagpole" internally to see if there are any concerns with relaxing the 32 byte restriction. If there aren't, we can chart a path to replace the go-ethereum signer with something more flexible.

Are you hashing the signing payload (which gives you the 32 bytes or are you just returning the raw payload)? If you could attach details/preferred choice of RSA or DSA or DSTU4145, that would be greatly appreciated!

In thinking about it further, RSA and DSA have large public key sizes and signatures. I wouldn't want anyone using the Rosetta integration being stuck paying for those larger transactions.

DSTU4145 is not very popular, there are not many implementations of it. I can't find anything in go.

I really think using the ECDSA is the way to go.

I really think using the ECDSA is the way to go.

๐Ÿ‘

Are you hashing the signing payload (which gives you the 32 bytes or are you just returning the raw payload)?

Just following up on this question ^^ Our cryptography team had concerns about signing 20-byte messages as there isn't sufficient hash collision resistance. Your native library may be doing this hash conversion behind the scenes and running the raw payload through a hash function + rosetta-sdk-go/keys may provide the same signature for the same private key.

I would hope that maybe now there is a cleaner ECDSA implementation somewhere that could be used rather than the go-ethereum one.

go-ethereum vendors the bitcoin-core/secp256k1 implementation and I highly doubt we would switch off this. The bitcoin/secp256k1 library is actually the reason for the 32-byte restriction: https://github.com/bitcoin-core/secp256k1/blob/8ab24e8dad9d43fc6661842149899e3cc9213b24/include/secp256k1.h#L531-L552

With respect to your cryptography team, 20 bytes (160 bits) is exactly the security on every Bitcoin address (as they involve a ripe160). If that isn't good enough, we are all in some real trouble.

Anyways, the signed data is the result of:

hash_sha1( hash_skein256( tx_data ) )

That wasn't our plan, but based on our security analysis it isn't a problem:

https://wiki.snowblossom.org/index.php/Security/SA-1

With respect to your cryptography team, 20 bytes (160 bits) is exactly the security on every Bitcoin address (as they involve a ripe160). If that isn't good enough, we are all in some real trouble.

To clarify, the concern isn't that any 20 byte sequence is somehow "vulnerable" (i.e. Bitcoin addresses or their relation to a private key that backs it). The concern is that someone can reuse a signature generated from a private key we control to maliciously move funds. In the worst case, an attacker that knows how to cause hash collisions in the 20-byte digest could produce a nefarious transaction (moving funds to them) that hashes to the same 20-byte digest associated with a previous signature. A transaction of this kind could be accepted on-chain and would be considered valid (signature verification would pass).

I haven't reviewed your implementation in depth and can't speak as to whether this is a feasible attack vector on your project but this was the initial concern when you mentioned 20-byte digests (mostly because of known SHA-1 hash collisions).

That wasn't our plan, but based on our security analysis it isn't a problem:

https://wiki.snowblossom.org/index.php/Security/SA-1

Thanks for putting this document together! I'll pass this along and see if it assuages any concerns. Sorry for any confusion or frustration that my initial response caused (should've explained the concern more clearly)!

Hiya @fireduck64, the way you hash your data is up to you, your hash scheme seems fine. But at the end of the day, your data signed needs to be 32 bytes if you are using secp256k1.

In terms of cryptography, the length of the message being signed needs to be equal to the length of the order of the elliptic curve (in secp256k1's case, 32 bytes). You can read more about there:

The hash of the message ought to be truncated so that the bit length of the hash is the same as the bit length of  (the order of the subgroup).

As well as in the RFC, in which they leftpad in their implementation.

The output of sha1 is always 20 bytes long.

Recommended Solution: Leftpad the 20 byte payload to become 32 bytes.

Here's a playground that shows that if you leftpad to 32 bytes, your signature is the same: https://play.golang.org/p/3NtLGI_UF79

So I would recommend you do that, which would also give you backwards compatibility in theory. I bet that the libraries you are testing with are leftpadding to 32 bytes anyways.

We have been running for a few years with sha1 hashes. So if you are correct, then some layer is actually padding already. In which case, if I can figure that out then I can do that before handing the hex bytes off via the rosetta api. I'll look into that.

Wow, that actually works. Ok, let me do a few additional checks and then I think I can close this issue.

Yeah, the left pad works great. Construction check seems to be working well now. Thanks, @itstehkman and @patrick-ogrady

Wow, that actually works. Ok, let me do a few additional checks and then I think I can close this issue.

Cryptography == magic moon math haha...awesome work @itstehkman ๐Ÿš€