11

I am currently trying to derive a Bitcoin uncompressed ECDSA public key from a compressed one.

According to this link on the Bitcoin wiki, it is possible to do so... But how?

To give you more details: as of now I have compressed keys (33-bytes-long) gathered on the bitcoin network.

They are of the following format: <1-byte-long prefix><32-bytes-long X>. From there, I would like to obtain an uncompressed key (65-bytes-long) whose format is: <1-byte-long prefix><32-bytes-long X><32-bytes-long Y>

According to this other link on the Bitcoin wiki, it should be as easy as solving the equation:

Y^2 = X^3 + 7

However, I cannot seem to get there. My value for Y is simply far-off. Here is my code (the value for the public key come from the Bitcoin wiki example):

import binascii
from decimal import *

expected_uncompressed_key_hex = '0450863AD64A87AE8A2FE83C1AF1A8403CB53F53E486D8511DAD8A04887E5B23522CD470243453A299FA9E77237716103ABC11A1DF38855ED6F2EE187E9C582BA6'
expected_y_hex = expected_uncompressed_key_hex[-64:]
expected_y_dec = int(expected_y_hex, 16)
x_hex = expected_uncompressed_key_hex[2:66]
if expected_y_dec % 2 == 0:
    prefix = "02"
else:
    prefix = "03"

artificial_compressed_key = prefix + x_hex

getcontext().prec = 500
test_dec = Decimal(int(x_hex, 16))
y_square_dec = test_dec**3 + 7
if prefix == "02":
    y_dec = - Decimal(y_square_dec).sqrt()
else:
    y_dec = Decimal(y_square_dec).sqrt()

computed_y_hex = hex(int(y_dec))
computed_uncompressed_key = "04" + x + computed_y_hex

For information, my outputs are:

computed_y_hex = '0X2D29684BD207BF6D809F7D0EB78E4FD61C3C6700E88AB100D1075EFA8F8FD893080F35E6C7AC2E2214F8F4D088342951'
expected_y_hex = '2CD470243453A299FA9E77237716103ABC11A1DF38855ED6F2EE187E9C582BA6'

Thank you for your help!

Clara-sininen
  • 181
  • 1
  • 9

4 Answers4

8

You need to calculate in the field Z_p, which mostly means that you have to reduce your number to the remainder after dividing with p after each calculation. Calculating this is called taking the modulo and is written as % p in python.

Exponentiating in this field can be done more effectively than the naive way of just multiplying and reducing many times. This is called modular exponentiation. Python's built-in exponentation function pow(n,e,p) can take care of this.

The remaining problem is to find the square root. Luckily secp256k1 is chosen in a special way (p%4=3), so that taking square roots is easy: A square root of x is x^((p+1)/4)%p.

So a simplified version of your code becomes:

import binascii

p_hex = 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F'
p = int(p_hex, 16)
compressed_key_hex = '0250863AD64A87AE8A2FE83C1AF1A8403CB53F53E486D8511DAD8A04887E5B2352'
x_hex = compressed_key_hex[2:66]
x = int(x_hex, 16)
prefix = compressed_key_hex[0:2]

y_square = (pow(x, 3, p)  + 7) % p
y_square_square_root = pow(y_square, (p+1)/4, p)
if (prefix == "02" and y_square_square_root & 1) or (prefix == "03" and not y_square_square_root & 1):
    y = (-y_square_square_root) % p
else:
    y = y_square_square_root

computed_y_hex = format(y, '064x')
computed_uncompressed_key = "04" + x_hex + computed_y_hex

print computed_uncompressed_key
Rasmus Faber
  • 45,972
  • 20
  • 136
  • 184
  • Your prose uses `(p MINUS 1) / 4` but your code uses `(p PLUS 1) / 4`. Having not seen that formula before I don't know which one to correct :). – bartonjs Apr 27 '17 at 15:02
  • @bartonjs: Thanks for catching that. I have fixed it (the code was correct). – Rasmus Faber Apr 28 '17 at 08:16
  • Hello @RasmusFaber,I would like to thank you for your clear, clean code and your explanations. However, I'm afraid it doesn't work when I implement it. I merely changed the syntax from python 2 to 3, but the value I get for y is: `xacc68af70eb1c42c7e2fb7364ad544b527c3926b32ad2cea6af8cea8907b734` when I expect `2CD470243453A299FA9E77237716103ABC11A1DF38855ED6F2EE187E9C582BA6`. Any idea what's going on? Thanks again! – Clara-sininen May 03 '17 at 09:57
  • @Clara-sininen : I don't know exactly what you changed, but this works fine: https://repl.it/HeAZ/0 – Rasmus Faber May 03 '17 at 13:49
  • @RasmusFaber You were right, I just wrote `pow(y_square, (p+1)/4, p)` instead of `pow(y_square, (p+1)//4, p)` ... In any case, it works perfectly now. Thank you so much for your help and patience :) – Clara-sininen May 08 '17 at 09:09
  • @RasmusFaber I found a couple of bugs in your answer. When creating `y`, you should consider whether `y_square_square_root` is odd or even before getting the complementary coordinate, so it should be: `if prefix == "02" and y_square_square_root & 1 or prefix == "03" and not y_square_square_root & 1`. Furthermore, `y` could end up being 1 character short if `hex()` is used, to make sure that the final formatting is right better use `computed_y_hex = format(y, '064x')`. – sr-gi Mar 12 '18 at 12:14
  • @sr-gi Thanks! Is this better? – Rasmus Faber Mar 12 '18 at 12:51
  • Remove the [2:66] from the format (since it is already formatted without the 0x) and it will be perfect! And thank you btw!! Coding such a function for my Bitcoin tools library has been in my todo list for a while. – sr-gi Mar 12 '18 at 12:58
4

Here a sample code without any 3rd party python libs:

def pow_mod(x, y, z):
    "Calculate (x ** y) % z efficiently."
    number = 1
    while y:
        if y & 1:
            number = number * x % z
        y >>= 1
        x = x * x % z
    return number

# prime p = 2^256 - 2^32 - 2^9 - 2^8 - 2^7 - 2^6 - 2^4 - 1
p = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f

# bitcoin's compressed public key of private key 55255657523dd1c65a77d3cb53fcd050bf7fc2c11bb0bb6edabdbd41ea51f641
compressed_key = '0314fc03b8df87cd7b872996810db8458d61da8448e531569c8517b469a119d267'

y_parity = int(compressed_key[:2]) - 2
x = int(compressed_key[2:], 16)

a = (pow_mod(x, 3, p) + 7) % p
y = pow_mod(a, (p+1)//4, p)

if y % 2 != y_parity:
    y = -y % p

uncompressed_key = '04{:x}{:x}'.format(x, y)
print(uncompressed_key) 
# should get 0414fc03b8df87cd7b872996810db8458d61da8448e531569c8517b469a119d267be5645686309c6e6736dbd93940707cc9143d3cf29f1b877ff340e2cb2d259cf

refer to bitcoin talk: https://bitcointalk.org/index.php?topic=644919.0

chutium
  • 357
  • 3
  • 9
3

The field of the elliptic curve is not over the field of real numbers. It's over a finite field modulo some prime.

For Secp256k1 the prime p = 2^256 - 2^32 - 2^9 - 2^8 - 2^7 - 2^6 - 2^4 - 1.

Thus: y^2= (x^3) + 7 (mod p)

There's no direct way to solve the equation, you would need to use Cipolla's algorithm: https://en.wikipedia.org/wiki/Cipolla%27s_algorithm

David Davidson
  • 168
  • 1
  • 11
0

I know that this question has been answered and I actually benefited from this answer, so thank you. The problem is that I found these answers 3 times while looking for the same solution in C# and I don't really code in python :). So for anybody trying to solve this here is a C# solution, have fun! :) (It uses BouncyCastle Library).

using System;
using System.Collections.Generic;
using System.Linq;
using MoreLinq;
using NBitcoin;
using Org.BouncyCastle.Asn1.X9;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Math;
using Org.BouncyCastle.Math.EC;

namespace BitcoinPublicKeyDecompression
{
    public class Program
    {
        public static void Main()
        {
            const string cPubKey = "0250863ad64a87ae8a2fe83c1af1a8403cb53f53e486d8511dad8a04887e5b2352";
            var uPubKey = cPubKey.ToHexByteArray().BitcoinDecompressPublicKey().ToHexString();
            var expectedUPubKey = new PubKey(cPubKey).Decompress().ToString();

            Console.WriteLine($"Public Key:\n\n{cPubKey}\n\nhas been {(uPubKey == expectedUPubKey ? "correctly" : "incorrectly")} decompressed to:\n\n{uPubKey}");

            Console.WriteLine("\nPress any key to quit...");
            Console.ReadKey();
        }
    }

    public static class Extensions
    {
        public static readonly byte[] EmptyByteArray = new byte[0];

        public static byte[] BitcoinDecompressPublicKey(this byte[] bPubC)
        {
            var ecPubKey = bPubC.BitcoinCompressedPublicKeyToECPublicKey();
            return ecPubKey.ToBitcoinUncompressedPublicKey();
        }

        public static ECPublicKeyParameters BitcoinCompressedPublicKeyToECPublicKey(this byte[] bPubC)
        {
            var pubKey = bPubC.Skip(1).ToArray();

            var curve = ECNamedCurveTable.GetByName("secp256k1");
            var domainParams = new ECDomainParameters(curve.Curve, curve.G, curve.N, curve.H, curve.GetSeed());

            var yParity = new BigInteger(bPubC.Take(1).ToArray()).Subtract(BigInteger.Two);
            var x = new BigInteger(1, pubKey);
            var p = ((FpCurve)curve.Curve).Q;
            var a = x.ModPow(new BigInteger("3"), p).Add(new BigInteger("7")).Mod(p);
            var y = a.ModPow(p.Add(BigInteger.One).FloorDivide(new BigInteger("4")), p);

            if (!y.Mod(BigInteger.Two).Equals(yParity))
                y = y.Negate().Mod(p);

            var q = curve.Curve.CreatePoint(x, y);
            return new ECPublicKeyParameters(q, domainParams);
        }

        public static byte[] ToBitcoinUncompressedPublicKey(this AsymmetricKeyParameter ecPublicKey)
        {
            var publicKey = ((ECPublicKeyParameters)ecPublicKey).Q;
            var xs = publicKey.AffineXCoord.ToBigInteger().ToByteArrayUnsigned().PadStart(32);
            var ys = publicKey.AffineYCoord.ToBigInteger().ToByteArrayUnsigned().PadStart(32);
            return new byte[] { 0x04 }.ConcatMany(xs, ys).ToArray();
        }

        public static BigInteger FloorDivide(this BigInteger a, BigInteger b)
        {
            if (a.CompareTo(BigInteger.Zero) > 0 ^ b.CompareTo(BigInteger.Zero) < 0 && !a.Mod(b).Equals(BigInteger.Zero))
                return a.Divide(b).Subtract(BigInteger.One);

            return a.Divide(b);
        }

        public static byte[] ToHexByteArray(this string str)
        {
            byte[] bytes;
            if (string.IsNullOrEmpty(str))
                bytes = EmptyByteArray;
            else
            {
                var string_length = str.Length;
                var character_index = str.StartsWith("0x", StringComparison.Ordinal) ? 2 : 0;
                var number_of_characters = string_length - character_index;
                var add_leading_zero = false;

                if (0 != number_of_characters % 2)
                {
                    add_leading_zero = true;
                    number_of_characters += 1;
                }

                bytes = new byte[number_of_characters / 2];

                var write_index = 0;
                if (add_leading_zero)
                {
                    bytes[write_index++] = CharacterToByte(str[character_index], character_index);
                    character_index += 1;
                }

                for (var read_index = character_index; read_index < str.Length; read_index += 2)
                {
                    var upper = CharacterToByte(str[read_index], read_index, 4);
                    var lower = CharacterToByte(str[read_index + 1], read_index + 1);

                    bytes[write_index++] = (byte)(upper | lower);
                }
            }

            return bytes;
        }

        public static byte CharacterToByte(char character, int index, int shift = 0)
        {
            var value = (byte)character;
            if (0x40 < value && 0x47 > value || 0x60 < value && 0x67 > value)
            {
                if (0x40 != (0x40 & value))
                    return value;
                if (0x20 == (0x20 & value))
                    value = (byte)((value + 0xA - 0x61) << shift);
                else
                    value = (byte)((value + 0xA - 0x41) << shift);
            }
            else if (0x29 < value && 0x40 > value)
                value = (byte)((value - 0x30) << shift);
            else
                throw new InvalidOperationException($"Character '{character}' at index '{index}' is not valid alphanumeric character.");

            return value;
        }

        public static string ToHexString(this byte[] value, bool prefix = false)
        {
            var strPrex = prefix ? "0x" : "";
            return strPrex + string.Concat(value.Select(b => b.ToString("x2")).ToArray());
        }

        public static IEnumerable<T> ConcatMany<T>(this IEnumerable<T> enumerable, params IEnumerable<T>[] enums)
        {
            return enumerable.Concat(enums.SelectMany(x => x));
        }
    } 
}

Result:

enter image description here

rvnlord
  • 2,728
  • 3
  • 14
  • 22