Does python ecdsa NIST-256p provide recovery code (byte)?

Question:

I am using the python ecdsa library for signing messages for blockchain transactions. In the blockchain specification it says, for secp256r1 that the signature should have a length of 65 bytes where:

The signature must be of length 65 bytes in the form of [r, s, v] where the first 32 bytes are r, the second 32 bytes are s and the last byte is v.

and

The v represents the recovery ID, which must be normalized to 0, 1, 2 or 3. Note that unlike EIP-155 chain ID is not used to calculate the v value

I consistently get the first 64 bytes using:
sign_deterministic(data, hashfunc=hashlib.sha256)

But not sure where ecdsa holds, or I can compute, the v byte?

I see in the source code (Rust fastcrypto) used on the blockchain it is doing:

// Compute recovery id and normalize signature
        let is_r_odd = y.is_odd();
        let is_s_high = sig.s().is_high();
        let is_y_odd = is_r_odd ^ is_s_high;
        let sig_low = sig.normalize_s().unwrap_or(sig);
        let recovery_id = RecoveryId::new(is_y_odd.into(), false);

But in ecdsa all that is hidden behind the signing function noted above.

Asked By: Frank C.

||

Answers:

Signing with ECDSA is explained e.g. here. In the following, the terms used in this post are applied:

k:                  secret number when generating the signature
R=k*G =(R.x, R.y):  associated point, G: generator
(r=R.x,s):          signature

The ecdsa library does not support the additional determination of the recovery ID when creating the signature. However, it is possible to determine the recovery ID subsequently. To do this, the k value used when creating the signature must first be determined, which is possible since you are using deterministic ECDSA according to RFC 6979. Here k depends defined on the hash, the message and the private signing key.

When running sign_deterministic(data, hashfunc=hashlib.sha256), the following logic is executed to determine k:

from ecdsa import SigningKey, NIST256p, rfc6979, ecdsa
from hashlib import sha256
...
def get_k(signing_key, message, hash):  
    def simple_r_s(r, s, order):
        return r, s, order    
    retry_gen = 0
    while True:
        digest = hash(message).digest()
        k = rfc6979.generate_k(
            NIST256p.generator.order(),
            signing_key.privkey.secret_multiplier,
            hash,
            digest,
            retry_gen=retry_gen,
            extra_entropy=b"")
        try:
            r, s, order = signing_key.sign_digest(
                digest,
                sigencode=simple_r_s,
                k=k,
                allow_truncate=True)
            break
        except ecdsa.RSZeroError:
                retry_gen += 1
    return k

The corresponding code location can be found here. Note that the sign_deterministic(data, hashfunc=hashlib.sha256) call uses the default value for the extra entropy (extra_entropy=b"") and allows too large hashes to be truncated (allow_truncate=True).

With k, R can be determined as the product of k and generator point G. Formally, k can be considered as a raw private key and R as a raw public key, so the existing functionalities for keys can be used and R can be easily determined as follows:

def get_r(signing_key, message, hash):
    k = get_k(signing_key, message, hash)
    r_sk = SigningKey.from_secret_exponent(k, curve=NIST256p)
    r_vk = r_sk.get_verifying_key()
    return (r_vk.pubkey.point.x(), r_vk.pubkey.point.y())

For known R, the recovery ID can be derived as follows:

def get_recovery_id(r):
    r_x, r_y = r
    if r_x > NIST256p.generator.order():
        if r_y % 2 == 0: recId = 2
        else: recId = 3
    else:
        if r_y % 2 == 0: recId = 0
        else: recId = 1
    return recId

For an explanation of the last logic, see here (including the comments). Note that the probability for recovery IDs 2 and 3 is vanishingly small.

A signature including recovery ID can then be generated with the ecdsa library, e.g. as follows:

signing_key = SigningKey.generate(curve=NIST256p)
message = b'This is a text message'
signature = signing_key.sign_deterministic(message, hashfunc=sha256)
R = get_r(signing_key, message, sha256)
rec_id = get_recovery_id(R)
print("Rec-Id:", str(rec_id))
print("r:", signature[:32].hex())
print("s:", signature[32:].hex())

To verify the results and also to demonstrate that there are more convenient libraries for generating a signature with recovery ID, the following pycoin code is used, which requires only the library function function sign_with_recid(), which returns a tuple with r, s and the recovery ID. By default, this method internally uses the k derivation from RFC6979 (i.e. the logic also applied in the ecdsa example):

from pycoin.ecdsa.secp256r1 import secp256r1_generator
from hashlib import sha256
...
signing_key = SigningKey.generate(curve=NIST256p)
message = b'This is a text message'
private_key = signing_key.privkey.secret_multiplier 
digest = sha256(message).digest()
(r, s, rec_id) = secp256r1_generator.sign_with_recid(private_key, int.from_bytes(digest, 'big'))
print("Rec-Id:", str(rec_id))
print("r:", r.to_bytes(32, 'big').hex())
print("s:", s.to_bytes(32, 'big').hex())

When using the same signing key, the same message and the same digest, the values are identical.


The current question is about deterministic ECDSA. But even for non-deterministic ECDSA, the ecdsa library allows the recovery ID to be determined (and much more easily than in the case of deterministic ECDSA).
Since the random k used in signing cannot be accessed, the generation of k is simply moved outside and then the signature is generated with this k (which is supported, see sign()):

import os
import ecdsa
import hashlib

from ecdsa import SigningKey, NIST256p

message = b'This is a text message'
sk = SigningKey.generate(curve=NIST256p)
k = ecdsa.util.randrange(NIST256p.generator.order(), os.urandom) # shift the internal k generation to the outside
signature = sk.sign(message, k=k, hashfunc=hashlib.sha256)

For known k, R and finally the recovery ID can be determined as above.

Answered By: Topaco