Solana/Private Key, Public Key and Address/A Cryptographic Explanation of Private Key (Part 4)
In the digital world, how to verify someone's identity without revealing their private keys is a ultimate challenge in cryptography. Traditional password and certificate systems may be secure, but are vulnerable to attacks; on the other hand, public-private key systems provide new possibilities for digital trust based on strong mathematical foundations.
In this section, we will learn about three common functions of the ECDSA (Elliptic Curve Digital Signature Algorithm) used in the secp256k1 curve: signing, verifying, and recovering the public key.
ECDSA Signing
We often sign documents with our name to indicate who signed them and to acknowledge the contents of the document. In digital signature, we use private keys to create signatures that can be verified by others using their corresponding public keys. Unlike traditional signings, digital signatures are more secure because they cannot be tampered with.
The ECDSA signing algorithm is based on the secp256k1 curve and provides a way to prove the integrity of information and ensure that only the owner of the private key can create a signature.
Recall from previous sections: secp256k1 private keys are arbitrary scalars k, while public keys are generated by multiplying g with k, i.e., g * k.
The signing process is as follows:
- Use a hash function (e.g. SHA-256) to hash the information and get the message digest m.
- Choose a random integer k between 1 and n-1 (where n is the order of the elliptic curve).
- Calculate c = g * k and set the x-coordinate of c as r. If r equals 0, choose another value for k and repeat this process.
- Calculate s = k⁻¹ * (m + r * prikey) where k⁻¹ is the multiplicative inverse of k. If s equals 0, choose another value for k and repeat this process.
- The digital signature consists of a pair (r, s).
The implementation code is as follows:
import itertools
import random
import typing
import pabtc.secp256k1
def sign(prikey: pabtc.secp256k1.Fr, m: pabtc.secp256k1.Fr) -> typing.Tuple[pabtc.secp256k1.Fr, pabtc.secp256k1.Fr, int]:
# https://www.secg.org/sec1-v2.pdf
# 4.1.3 Signing Operation
for _ in itertools.repeat(0):
k = pabtc.secp256k1.Fr(max(1, secrets.randbelow(pabtc.secp256k1.N)))
R = pabtc.secp256k1.G * k
r = pabtc.secp256k1.Fr(R.x.x)
if r.x == 0:
continue
s = (m + prikey * r) / k
if s.x == 0:
continue
v = 0
if R.y.x & 1 == 1:
v |= 1
if R.x.x >= pabtc.secp256k1.N:
v |= 2
return r, s, v
You may notice in the code implementation that the signature function not only returns (r, s), but also returns an additional v value. This is the recovery identifier used to determine the signer's public key from the signature. It uses two bits, the lowest bit marks the parity of the y-axis coordinate of c, so that we can uniquely recover the actual value of c based on r in the signature (elliptic curves are symmetric with respect to the x-axis, and each x corresponds to two possible y-values). Another bit is used to verify that the r value has not overflowed, because the range of coordinates of points on the elliptic curve is 0 to P, but in the signature operation we convert the x-coordinate of c to a scalar, which reduces the range to 0 to N, and therefore overflow truncation may occur.
Do you remember the values of P and N? P refers to the prime numbers in the prime number field, and N refers to the order of the secp256k1 elliptic curve, which is smaller than P.
P = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f
N = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141
ECDSA Verification
Verification is the reverse operation of signing. The steps are as follows:
- Use a hash function to hash the information and get the message digest m.
- Check if the signature values r and s are in the range [1, n-1]. If not, the signature is invalid.
- Calculate a = m * s⁻¹ and b = r * s⁻¹, where s⁻¹ is the multiplicative inverse of s.
- Calculate c = g * a + pubkey * b. If c equals the identity element, verification fails.
- If c's x-coordinate equals r, then the signature is valid; otherwise, it is invalid.
The implementation code is as follows:
import pabtc.secp256k1
def verify(pubkey: pabtc.secp256k1.Pt, m: pabtc.secp256k1.Fr, r: pabtc.secp256k1.Fr, s: pabtc.secp256k1.Fr) -> bool:
# https://www.secg.org/sec1-v2.pdf
# 4.1.4 Verifying Operation
a = m / s
b = r / s
R = pabtc.secp256k1.G * a + pubkey * b
assert R != pabtc.secp256k1.I
return r == pabtc.secp256k1.Fr(R.x.x)
ECDSA Public Key Recovery
Given an ECDSA signature (r, s) and a recovery flag v, you can uniquely determine the public key. The steps are as follows:
- Use a hash function to hash the information and get the message digest m.
- Recover c from the recovery flag v.
- Set the public key pubkey = (c * s - g * m) / r.
The implementation code is as follows:
import pabtc.secp256k1
def pubkey(m: pabtc.secp256k1.Fr, r: pabtc.secp256k1.Fr, s: pabtc.secp256k1.Fr, v: int) -> pabtc.secp256k1.Pt:
# https://www.secg.org/sec1-v2.pdf
# 4.1.6 Public Key Recovery Operation
assert v in [0, 1, 2, 3]
if v & 2 == 0:
x = pabtc.secp256k1.Fq(r.x)
else:
x = pabtc.secp256k1.Fq(r.x + pabtc.secp256k1.N)
z = x * x * x + pabtc.secp256k1.A * x + pabtc.secp256k1.B
y = z ** ((pabtc.secp256k1.P + 1) // 4)
if v & 1 != y.x & 1:
y = -y
R = pabtc.secp256k1.Pt(x, y)
return (R * s - pabtc.secp256k1.G * m) / r
Again, please note that all the code snippets are publicly available on GitHub for your reference and use. If you have any questions or need further assistance, feel free to ask!
Exercise
Q: Suppose a message is hashed to 0x72a963cdfb01bc37cd283106875ff1f07f02bc9ad6121b75c3d17629df128d4e. Please use the private key 0x01 to sign it, verify it and recover the public key.
A:
import pabtc
prikey = pabtc.secp256k1.Fr(1)
pubkey = pabtc.secp256k1.G * prikey
m = pabtc.secp256k1.Fr(0x72a963cdfb01bc37cd283106875ff1f07f02bc9ad6121b75c3d17629df128d4e)
r, s, v = pabtc.ecdsa.sign(prikey, m)
assert pabtc.ecdsa.verify(pubkey, m, r, s)
assert pabtc.ecdsa.pubkey(m, r, s, v) == pubkey