3

On the bitcoin wiki I found that bitcoin uses the ECDSA algorithm with the Secp256k1 curve.

Relevant Links:

On the first link, it says private key should be 32 bytes, public key 64 bytes, and the signature typically between 71-73 bytes. It says the signature can be smaller with small probability.

However, when I run the following python3 code

>>> from ecdsa import SigningKey, SECP256k1
>>> private_key = SigningKey.generate(curve=SECP256k1)
>>> public_key = private_key.get_verifying_key()
>>> signature = private_key.sign(b'message')
>>> print((len(private_key.to_string()), len(public_key.to_string()), len(signature)))

I get (32, 64, 64) as output. I'd expect to get something like (32, 64, 72).

I figure one of the following is happening:

  • I am misunderstanding the wiki article.
  • I am using python-ecdsa incorrectly
  • the bitcoin wiki is incorrect
  • python-ecdsa is not implemented correctly

The first two are the more likely ones.

Can anyone explain to me why I am getting a mismatch between what I expect, and what I am actually getting?

zrbecker
  • 1,394
  • 1
  • 19
  • 39

2 Answers2

7

An ECDSA signature consists of two numbers, r and s which are numbers in the range [1..n-1] where n is the order of the curve. n is a (known) number in the range [2^(k-1)..2^k-1] where k is the key size. So the size of r and s are generally the same and sometimes somewhat smaller as the key size in bytes.

Now r and s can be encoded in multiple ways, of which two are common:

  1. r and s are DER encoded as two ASN.1 signed INTEGER types within an ASN.1 SEQUENCE.
  2. r and s are encoded as two statically sized, unsigned integers with the same size as the key size (or order) in octets or bytes.

So the difference in size is just because the values r and s are encoded differently. Of course, you need to know the type of encoding before you can verify the signature.

As r and s are exactly the same independents of the encoding it is relatively simple to convert between the two versions (if you can call anything that requires generation or parsing of DER-encoded ASN.1 structures "simple").

Type 1 has been standardized in ANSI X9.62 and type 2, often called a flat encoding, is commonly used on embedded platforms or smart cards.


r and s are just very likely the same size as n / the key size, but in principle, they could be e.g. a number 3. The chance of that happening is abysmally small. You should, however, not perform any tests on the size of r and s. If either of them is more than 8 bytes smaller then you may start to scratch your head because the chance of that happening is between 1/2^63 and 1/2^64, i.e. extremely unlikely.


So:

  • I am misunderstanding the wiki article.

No, the wiki article assumes the standardized encoding of ANSI X9.62.

  • I am using python-ecdsa incorrectly

No, the python-ecdsa package just uses a different encoding and you are surprised.

  • the bitcoin wiki is incorrect

No, the bitcoin wiki assumed a particular encoding chosen for their protocol.

  • python-ecdsa is not implemented correctly

Definitely not; at least not with regards to the size of the signature.


Now for the implementation details; the following is in the documentation:

There are also multiple ways to represent a signature. The default sk.sign() and vk.verify() methods present it as a short string, for simplicity and minimal overhead. To use a different scheme, use the sk.sign(sigencode=) and vk.verify(sigdecode=) arguments. There are helper functions in the "ecdsa.util" module that can be useful here.

So try to use sigencode=sigencode_der to get the format expected by the wiki article. The util.py source has all the conversions you are likely to need. It uses number_to_string to create statically sized numbers. This function is also known as I2OSP or Integer to Octet String primitive in PKCS#1 (RSA). Note that "strings" in the code refer to octet strings, also known as byte arrays - not text strings.

Nazim Kerimbekov
  • 4,712
  • 8
  • 34
  • 58
Maarten Bodewes
  • 90,524
  • 13
  • 150
  • 263
  • That wiki is out of date and incomplete. For transaction signatures, ASN.1 DER encoding for secp256k1 is usually 72, 71, or 70 octets, and rarely less, and bitcoin adds one octet for 'sighash'. But since BIP62 in 2014 Bitcoin uses only **low-S** signatures, which excludes the 72-octet case leaving 71 or 70 usually and rarely less, plus 1. For _message_ signatures Bitcoin uses plain/P1363/PKSC11/CVC format of exactly 64 octets plus one octet for key-recovery. In general n is the _generator and subgroup_ order, not necessarily the curve order, but all X9/Certicom/NIST prime curves ... – dave_thompson_085 Sep 09 '20 at 03:06
  • ... were chosen to have cofactor 1. #E(Fp) is within the Hasse bound of p, but since p is chosen very close under 2^k, this _can_ exceed 2^k, and does for secp224k1 (but not secp256k1). The private key is the size of n, but the public key coordinate(s) is(are) the size of p. – dave_thompson_085 Sep 09 '20 at 03:06
  • Thanks as always on the remarks! In this case though, I wonder if an answer would be a better fit (?) – Maarten Bodewes Sep 09 '20 at 07:38
-1

The only problem with python-ecdsa is performance, because it's too slow.

A better library: starkbank-ecdsa

How to install it:

pip install starkbank-ecdsa

How to use it:

# Generate Keys
privateKey = PrivateKey()
publicKey = privateKey.publicKey()

message = "My test message"

# Generate Signature
signature = Ecdsa.sign(message, privateKey)

# Verify if signature is valid
print Ecdsa.verify(message, signature, publicKey)

Full reference: https://github.com/starkbank/ecdsa-python

rcmstark
  • 1,031
  • 1
  • 14
  • 18