I am using the azurerm_key_vault_key
terraform resource to create an secp256k1 key. I can output the X and Y coordinates for this key as strings that look like this (appears to be a base64 url-encoded, but I cannot find any docs that clearly define this)
"x": "ARriqkpHlC1Ia1Tk86EM_bqH_9a88Oh2zMYF3fUUGJw"
"y": "wTYd3CEiwTk1n-lFPdpZ51P4Z0EzlVNXLvJMY-k55pQ"
I need to convert this into a public key in hexadecimal format. I have tried the following in Python:
import ecdsa
import binascii
import base64
def base64url_to_bytes(base64url_string):
padding = '=' * (4 - (len(base64url_string) % 4))
base64_string = base64url_string.replace('-', '+').replace('_', '/') + padding
return base64.b64decode(base64_string)
def compress_point(x, y):
"""Compresses the point (x, y) to a 33-byte compressed public key."""
prefix = "02" if y % 2 == 0 else "03"
return prefix + x
def decompress_point(compressed_key):
"""Decompresses the compressed public key to (x, y)."""
prefix = compressed_key[:2]
x = compressed_key[2:]
y = ecdsa.ellipticcurve.Point(ecdsa.SECP256k1.curve, int(x, 16), int(prefix, 16))
return x, y.y()
x_value = base64url_to_bytes("ARriqkpHlC1Ia1Tk86EM_bqH_9a88Oh2zMYF3fUUGJw")
y_value = base64url_to_bytes("wTYd3CEiwTk1n-lFPdpZ51P4Z0EzlVNXLvJMY-k55pQ")
x, y = int.from_bytes(x_value, byteorder='big'), int.from_bytes(y_value, byteorder='big')
# Compress the point
compressed_key = compress_point(format(x, 'x'), y)
print(compressed_key)
decompressed_x, decompressed_y = decompress_point(compressed_key)
print("Decompressed X:", decompressed_x)
print("Decompressed Y:", decompressed_y)
It compresses, but decompress_point
throws an assertion error, which suggests that I am doing something wrong either in the compression or decompression step.
Here is the full output of the script above:
0211ae2aa4a47942d486b54e4f3a10cfdba87ffd6bcf0e876ccc605ddf514189c
Traceback (most recent call last):
File "get_public_key3.py", line 32, in <module>
decompressed_x, decompressed_y = decompress_point(compressed_key)
File "get_public_key3.py", line 19, in decompress_point
y = ecdsa.ellipticcurve.Point(ecdsa.SECP256k1.curve, int(x, 16), int(prefix, 16))
File "/home/ubuntu/.local/lib/python3.8/site-packages/ecdsa/ellipticcurve.py", line 1090, in __init__
assert self.__curve.contains_point(x, y)
AssertionError
I am not at all familiar with the packages in use here and the code above has mostly been put together from various bits and pieces I could find online.
Could someone provide me with a definitive answer on how to do this?
Base64url is a common encoding, your x and y look formally Base64url encoded. Also, a Base64url decoding provides an EC point valid for secp256k1:
...
x_value = base64url_to_bytes("ARriqkpHlC1Ia1Tk86EM_bqH_9a88Oh2zMYF3fUUGJw")
y_value = base64url_to_bytes("wTYd3CEiwTk1n-lFPdpZ51P4Z0EzlVNXLvJMY-k55pQ")
x, y = int.from_bytes(x_value, byteorder='big'), int.from_bytes(y_value, byteorder='big')
point = ecdsa.ellipticcurve.Point(ecdsa.SECP256k1.curve, x, y)
print(point) # (499815257955367864408742079034045137860289053987516893620084695066060527772,87391995603390372321291482400901178939983763504428780242289494985570894079636)
An invalid EC point would result in an error message. So it is most likely indeed a Base64url encoding.
Converting your Base64url encoded x and y coordinates to a compressed or uncompressed key is easy. You don't even need the ecdsa library.
Note that for secp256k1 x and y must each have a size of 32 bytes in compressed or uncompressed format. If they should have numerically a value, which is smaller, they are to be padded from the front with 0x00 values to 32 bytes. A possible conversion looks as follows:
...
x_value = base64url_to_bytes("ARriqkpHlC1Ia1Tk86EM_bqH_9a88Oh2zMYF3fUUGJw")
y_value = base64url_to_bytes("wTYd3CEiwTk1n-lFPdpZ51P4Z0EzlVNXLvJMY-k55pQ")
# padding with leadng 0x00 values
x_32 = (32 - len(x_value)) * b'\0' + x_value
y_32 = (32 - len(y_value)) * b'\0' + y_value
compressed = (b'\02' if y_32[31] % 2 == 0 else b'\03') + x_32
uncompressed = b'\04' + x_32 + y_32
print(compressed.hex()) # 02011ae2aa4a47942d486b54e4f3a10cfdba87ffd6bcf0e876ccc605ddf514189c
print(uncompressed.hex()) # 04011ae2aa4a47942d486b54e4f3a10cfdba87ffd6bcf0e876ccc605ddf514189cc1361ddc2122c139359fe9453dda59e753f86741339553572ef24c63e939e694
Note that your compress_point()
does not take padding into account (which is however not relevant for your values, since x and y are 32 bytes), nor does it return the hex encoded value with a leading 0 if the leading byte is single-digit (so format(x, 'x')
returns the hex encoded value as 63-digit 11ae2aa4a47942d486b54e4f3a10cfdba87ffd6bcf0e876ccc605ddf514189c
).
For completeness: If x and y are already integers, the conversion to padded bytes objects can be implemented simply as follows:
...
x_32 = x.to_bytes(32, 'big')
y_32 = y.to_bytes(32, 'big')
To convert a compressed key to an uncompressed key, the y value must be reconstructed, which requires modular arrithmetic. For this purpose ecdsa can be used (which implements the necessary math under the hood):
from ecdsa import VerifyingKey, SECP256k1
vk = VerifyingKey.from_string(compressed, curve=SECP256k1)
uncompressed = vk.to_string('uncompressed')
print(uncompressed.hex()) # 04011ae2aa4a47942d486b54e4f3a10cfdba87ffd6bcf0e876ccc605ddf514189cc1361ddc2122c139359fe9453dda59e753f86741339553572ef24c63e939e694
Likewise, of course, converting an uncompressed key to a compressed one is just as possible.
Note that your decompress_point()
applies ecdsa.ellipticcurve.Point()
incorrectly. The constructor requires as 2nd and 3rd parameters x and y coordinates as integers (see 1st code snippet).
For completeness: from_string()
can also handle the raw format, i.e. the concatenation of the 32 bytes x and 32 bytes y coordinate:
...
from ecdsa import VerifyingKey, SECP256k1
raw = x_32 + y_32
vk = VerifyingKey.from_string(raw, curve=SECP256k1)
uncompressed = vk.to_string('uncompressed')
print(uncompressed.hex()) # 04011ae2aa4a47942d486b54e4f3a10cfdba87ffd6bcf0e876ccc605ddf514189cc1361ddc2122c139359fe9453dda59e753f86741339553572ef24c63e939e694