【问题标题】:How do I get an ECDSA public key from just a Bitcoin signature? ... SEC1 4.1.6 key recovery for curves over (mod p)-fields如何仅从比特币签名中获取 ECDSA 公钥? ... SEC1 4.1.6 (mod p) 域上曲线的密钥恢复
【发布时间】:2013-11-09 00:53:32
【问题描述】:

更新:Git 上提供了部分解决方案

编辑:此版本的编译版本可在https://github.com/makerofthings7/Bitcoin-MessageSignerVerifier 获得

请注意,要验证的消息必须以Bitcoin Signed Message:\n 作为前缀。 Source1Source2

C# 实现中有一些错误,我可能可以通过this Python implementation 进行纠正


实际上提出正确的 Base 58 地址似乎有问题。

我在下面有以下消息、签名和 Base58 地址。我打算从签名中提取密钥,对该密钥进行哈希处理,然后比较 Base58 哈希值。

我的问题是:如何从签名中提取密钥? (在这篇文章的底部编辑I found the c++ code,在 Bouncy Castle / 或 C# 中需要它)

留言

StackOverflow test 123

签名

IB7XjSi9TdBbB3dVUK4+Uzqf2Pqk71XkZ5PUsVUN+2gnb3TaZWJwWW2jt0OjhHc4B++yYYRy1Lg2kl+WaiF+Xsc=

Base58 比特币地址“哈希”

1Kb76YK9a4mhrif766m321AMocNvzeQxqV

由于 Base58 比特币地址只是一个哈希,我不能用它来验证比特币消息。但是,可以从 签名中提取公钥。

编辑: 我要强调的是,我是从签名本身而不是从 Base58 公钥哈希中获取公钥。如果我想要(而且我确实想要),我应该能够将这些公钥位转换为 Base58 哈希。我不需要帮助,我只需要提取公钥位和验证签名的帮助。

问题

  1. 在上面的签名中,这个签名是什么格式的? PKCS10? (答案:不,它是专有的as described here

  2. 如何在 Bouncy Castle 中提取公钥?

  3. 验证签名的正确方法是什么? (假设我已经知道如何将公钥位转换成与上面比特币哈希值相等的哈希值)

先前的研究

This link 描述了如何使用 ECDSA 曲线,下面的代码将允许我将公钥转换为 BC 对象,但我不确定如何从签名中获取点 Q

在下面的示例中,Q 是硬编码值

  Org.BouncyCastle.Asn1.X9.X9ECParameters ecp = Org.BouncyCastle.Asn1.Sec.SecNamedCurves.GetByName("secp256k1");
  ECDomainParameters params = new ECDomainParameters(ecp.Curve, ecp.G, ecp.N, ecp.H);
  ECPublicKeySpec pubKeySpec = new ECPublicKeySpec(
  ecp .curve.decodePoint(Hex.decode("045894609CCECF9A92533F630DE713A958E96C97CCB8F5ABB5A688A238DEED6DC2D9D0C94EBFB7D526BA6A61764175B99CB6011E2047F9F067293F57F5")), // Q
        params);
  PublicKey  pubKey = f.generatePublic(pubKeySpec);


 var signer = SignerUtilities.GetSigner("ECDSA"); // possibly similar to SHA-1withECDSA
 signer.Init(false, pubKey);
 signer.BlockUpdate(plainTextAsBytes, 0, plainTextAsBytes.Length);
 return signer.VerifySignature(signature);

其他研究:

THIS 是验证消息的比特币来源。

解码签名的Base64后,调用RecoverCompact(hash of message, signature)。我不是 C++ 程序员,所以我假设我需要弄清楚 key.Recover 是如何工作的。那还是key.GetPubKey

这是我认为我在 C# 中需要的 C++ 代码,最好是在充气城堡中......但我会采用任何可行的方法。

// reconstruct public key from a compact signature
// This is only slightly more CPU intensive than just verifying it.
// If this function succeeds, the recovered public key is guaranteed to be valid
// (the signature is a valid signature of the given data for that key)
bool Recover(const uint256 &hash, const unsigned char *p64, int rec)
{
    if (rec<0 || rec>=3)
        return false;
    ECDSA_SIG *sig = ECDSA_SIG_new();
    BN_bin2bn(&p64[0],  32, sig->r);
    BN_bin2bn(&p64[32], 32, sig->s);
    bool ret = ECDSA_SIG_recover_key_GFp(pkey, sig, (unsigned char*)&hash, sizeof(hash), rec, 0) == 1;
    ECDSA_SIG_free(sig);
    return ret;
}

...ECDSA_SIG_recover_key_GFp is here的代码

比特币中的自定义签名格式

This answer says there are 4 possible 可以产生签名的公钥,并且这被编码在较新的签名中。

【问题讨论】:

  • @zimdanen ,正确,我不想从 Base58 位硬币地址(哈希)获取信息。签名不是散列,但也包含足够的信息让我找出公钥,然后将该密钥转换为散列。然后我可以将计算的哈希值与我拥有的值进行比较。
  • 没问题。我添加了一个说明以防止其他快速阅读器这样做。
  • 签名验证的目的不是根据您已经知道的公钥验证签名(并且信任属于签名者)吗?如果您要根据同时给出的公钥验证签名(而不是检查签名者的已知公钥),这似乎违背了签名的目的。
  • @Bruno,是的,这一点被考虑在内。嵌入的微型公钥在散列时应该等于面向用户的压缩公钥散列。因此,如果签名是有效的,并且提取的公钥在散列到 Base58 形式时是完整的,那么 sig 是有效的并且您期望它来自谁。
  • 嗨@makerofthings7,我对这个问题缺乏关注感到震惊。我正在为你准备一个正确的答案。

标签: c# python openssl bouncycastle elliptic-curve


【解决方案1】:

恐怕您的示例数据存在一些问题。首先,您的样本 Q 的长度为 61 个字节,但比特币公钥(使用 secp256k1 曲线)的未压缩形式应该是 65 个字节。您提供的 Q 没有正确验证消息,但我计算的 Q 似乎可以验证它。

我编写了计算字符串“StackOverflow test 123”的正确公钥并使用 ECDsaSigner 验证它的代码。但是,此公钥的哈希值是 1HRDe7G7tn925iNxQaeD7R2ZkZiKowN8NW 而不是 1Kb76YK9a4mhrif766m321AMocNvzeQxqV

能否请您验证您的数据是否正确,并可能提供消息字符串的确切哈希值,以便我们尝试调试,不正确的哈希值可能会使事情变得非常糟糕。我使用的代码如下:

using System;
using System.Text;
using System.Security.Cryptography;

using Org.BouncyCastle.Math;
using Org.BouncyCastle.Math.EC;
using Org.BouncyCastle.Asn1.X9;
using Org.BouncyCastle.Crypto.Signers;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Utilities.Encoders;

public class Bitcoin
{
  public static ECPoint Recover(byte[] hash, byte[] sigBytes, int rec)
  {
    BigInteger r = new BigInteger(1, sigBytes, 0, 32);
    BigInteger s = new BigInteger(1, sigBytes, 32, 32);
    BigInteger[] sig = new BigInteger[]{ r, s };
    ECPoint Q = ECDSA_SIG_recover_key_GFp(sig, hash, rec, true);
    return Q;
  }

  public static ECPoint ECDSA_SIG_recover_key_GFp(BigInteger[] sig, byte[] hash, int recid, bool check)
  {
    X9ECParameters ecParams = Org.BouncyCastle.Asn1.Sec.SecNamedCurves.GetByName("secp256k1");
    int i = recid / 2;

    Console.WriteLine("r: "+ToHex(sig[0].ToByteArrayUnsigned()));
    Console.WriteLine("s: "+ToHex(sig[1].ToByteArrayUnsigned()));

    BigInteger order = ecParams.N;
    BigInteger field = (ecParams.Curve as FpCurve).Q;
    BigInteger x = order.Multiply(new BigInteger(i.ToString())).Add(sig[0]);
    if (x.CompareTo(field) >= 0) throw new Exception("X too large");

    Console.WriteLine("Order: "+ToHex(order.ToByteArrayUnsigned()));
    Console.WriteLine("Field: "+ToHex(field.ToByteArrayUnsigned()));

    byte[] compressedPoint = new Byte[x.ToByteArrayUnsigned().Length+1];
    compressedPoint[0] = (byte) (0x02+(recid%2));
    Buffer.BlockCopy(x.ToByteArrayUnsigned(), 0, compressedPoint, 1, compressedPoint.Length-1);
    ECPoint R = ecParams.Curve.DecodePoint(compressedPoint);

    Console.WriteLine("R: "+ToHex(R.GetEncoded()));

    if (check)
    {
      ECPoint O = R.Multiply(order);
      if (!O.IsInfinity) throw new Exception("Check failed");
    }

    int n = (ecParams.Curve as FpCurve).Q.ToByteArrayUnsigned().Length*8;
    BigInteger e = new BigInteger(1, hash);
    if (8*hash.Length > n)
    {
      e = e.ShiftRight(8-(n & 7));
    }
    e = BigInteger.Zero.Subtract(e).Mod(order);
    BigInteger rr = sig[0].ModInverse(order);
    BigInteger sor = sig[1].Multiply(rr).Mod(order);
    BigInteger eor = e.Multiply(rr).Mod(order);
    ECPoint Q = ecParams.G.Multiply(eor).Add(R.Multiply(sor));

    Console.WriteLine("n: "+n);
    Console.WriteLine("e: "+ToHex(e.ToByteArrayUnsigned()));
    Console.WriteLine("rr: "+ToHex(rr.ToByteArrayUnsigned()));
    Console.WriteLine("sor: "+ToHex(sor.ToByteArrayUnsigned()));
    Console.WriteLine("eor: "+ToHex(eor.ToByteArrayUnsigned()));
    Console.WriteLine("Q: "+ToHex(Q.GetEncoded()));

    return Q;
  }

  public static bool VerifySignature(byte[] pubkey, byte[] hash, byte[] sigBytes)
  {
    X9ECParameters ecParams = Org.BouncyCastle.Asn1.Sec.SecNamedCurves.GetByName("secp256k1");
    ECDomainParameters domainParameters = new ECDomainParameters(ecParams.Curve,
                                                                 ecParams.G, ecParams.N, ecParams.H,
                                                                 ecParams.GetSeed());

    BigInteger r = new BigInteger(1, sigBytes, 0, 32);
    BigInteger s = new BigInteger(1, sigBytes, 32, 32);
    ECPublicKeyParameters publicKey = new ECPublicKeyParameters(ecParams.Curve.DecodePoint(pubkey), domainParameters);

    ECDsaSigner signer = new ECDsaSigner();
    signer.Init(false, publicKey);
    return signer.VerifySignature(hash, r, s);
  }



  public static void Main()
  {
    string msg = "StackOverflow test 123";
    string sig = "IB7XjSi9TdBbB3dVUK4+Uzqf2Pqk71XkZ5PUsVUN+2gnb3TaZWJwWW2jt0OjhHc4B++yYYRy1Lg2kl+WaiF+Xsc=";
    string pubkey = "045894609CCECF9A92533F630DE713A958E96C97CCB8F5ABB5A688A238DEED6DC2D9D0C94EBFB7D526BA6A61764175B99CB6011E2047F9F067293F57F5";

    SHA256Managed sha256 = new SHA256Managed();
    byte[] hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(msg), 0, Encoding.UTF8.GetByteCount(msg));
    Console.WriteLine("Hash: "+ToHex(hash));

    byte[] tmpBytes = Convert.FromBase64String(sig);
    byte[] sigBytes = new byte[tmpBytes.Length-1];
    Buffer.BlockCopy(tmpBytes, 1, sigBytes, 0, sigBytes.Length);

    int rec = (tmpBytes[0] - 27) & ~4;
    Console.WriteLine("Rec {0}", rec);

    ECPoint Q = Recover(hash, sigBytes, rec);
    string qstr = ToHex(Q.GetEncoded());
    Console.WriteLine("Q is same as supplied: "+qstr.Equals(pubkey));

    Console.WriteLine("Signature verified correctly: "+VerifySignature(Q.GetEncoded(), hash, sigBytes));
  }

  public static string ToHex(byte[] data)
  {
    return BitConverter.ToString(data).Replace("-","");
  }
}

编辑 我看到这仍然没有被评论或接受,所以我编写了一个完整的测试来生成一个私钥和一个公钥,然后使用私钥生成一个有效的签名。之后,它从签名和散列中恢复公钥,并使用该公钥来验证消息的签名。请看下面,如果还有问题请告诉我。

  public static void FullSignatureTest(byte[] hash)
  {
    X9ECParameters ecParams = Org.BouncyCastle.Asn1.Sec.SecNamedCurves.GetByName("secp256k1");
    ECDomainParameters domainParameters = new ECDomainParameters(ecParams.Curve,
                                                                 ecParams.G, ecParams.N, ecParams.H,
                                                                 ecParams.GetSeed());
    ECKeyGenerationParameters keyGenParams =
      new ECKeyGenerationParameters(domainParameters, new SecureRandom());

    AsymmetricCipherKeyPair keyPair;
    ECKeyPairGenerator generator = new ECKeyPairGenerator();
    generator.Init(keyGenParams);
    keyPair = generator.GenerateKeyPair();

    ECPrivateKeyParameters privateKey = (ECPrivateKeyParameters) keyPair.Private;
    ECPublicKeyParameters publicKey = (ECPublicKeyParameters) keyPair.Public;

    Console.WriteLine("Generated private key: " + ToHex(privateKey.D.ToByteArrayUnsigned()));
    Console.WriteLine("Generated public key: " + ToHex(publicKey.Q.GetEncoded()));

    ECDsaSigner signer = new ECDsaSigner();
    signer.Init(true, privateKey);
    BigInteger[] sig = signer.GenerateSignature(hash);

    int recid = -1;
    for (int rec=0; rec<4; rec++) {
      try
      {
        ECPoint Q = ECDSA_SIG_recover_key_GFp(sig, hash, rec, true);
        if (ToHex(publicKey.Q.GetEncoded()).Equals(ToHex(Q.GetEncoded())))
        {
          recid = rec;
          break;
        }
      }
      catch (Exception)
      {
        continue;
      }
    }
    if (recid < 0) throw new Exception("Did not find proper recid");

    byte[] fullSigBytes = new byte[65];
    fullSigBytes[0] = (byte) (27+recid);
    Buffer.BlockCopy(sig[0].ToByteArrayUnsigned(), 0, fullSigBytes, 1, 32);
    Buffer.BlockCopy(sig[1].ToByteArrayUnsigned(), 0, fullSigBytes, 33, 32);

    Console.WriteLine("Generated full signature: " + Convert.ToBase64String(fullSigBytes));

    byte[] sigBytes = new byte[64];
    Buffer.BlockCopy(sig[0].ToByteArrayUnsigned(), 0, sigBytes, 0, 32);
    Buffer.BlockCopy(sig[1].ToByteArrayUnsigned(), 0, sigBytes, 32, 32);

    ECPoint genQ = ECDSA_SIG_recover_key_GFp(sig, hash, recid, false);
    Console.WriteLine("Generated signature verifies: " + VerifySignature(genQ.GetEncoded(), hash, sigBytes));
  }

【讨论】:

  • 很好的观察结果,但这并不能回答问题?
  • @AdamMcArthur 我理解最新的问题是:“我的问题是:如何从签名中提取密钥?(编辑我在这篇文章的底部找到了 c++ 代码,需要它Bouncy Castle / or C#)”我基本上将相关的 C++ 代码转换为 BouncyCastle 和 C#,BouncyCastle 对加密参数使用了许多不同的名称,而不是示例 OpenSSL 代码。你在回答哪个问题?
  • 抱歉,我没有看到您的“编辑”。虽然我一直在浏览比特币源代码,但我发现实际上不可能从签名中检索公钥?
  • @AdamMcArthur 如链接中所述,确实可以从 MESSAGE SIGNATURE 中检索公钥,它包含关于哪个点是正确公钥的 2 位信息。在我的代码的后编辑部分中,我还展示了如何计算 2 位信息,直接从比特币来源翻译。
  • @AdamMcArthur 有关更多信息,请参阅源代码github.com/bitcoin/bitcoin/blob/… 以及“CKey::Sign”和“CKey::SignCompact”以及“CPubKey::Verify”和“CPubKey::”之间的区别验证紧凑”。 Compact 版本是支持密钥恢复的消息的特殊签名,签名和验证版本是在网络上广泛使用的常用 DER 编码签名。
【解决方案2】:

在引用 BitcoinJ 之后,这些代码示例中的一些似乎缺少正确的消息准备、双 SHA256 散列,以及输入到地址计算的已恢复公共点的可能压缩编码。

以下代码应该只需要 BouncyCastle(您可能需要来自 github 的最新版本,不确定)。它从 BitcoinJ 中借用了一些东西,并且做得足以让小示例正常工作,请参阅内联 cmets 了解消息大小限制。

它只计算 RIPEMD-160 哈希值,我使用http://gobittest.appspot.com/Address 检查结果的最终地址(不幸的是,该网站似乎不支持输入公钥的压缩编码)。

    public static void CheckSignedMessage(string message, string sig64)
    {
        byte[] sigBytes = Convert.FromBase64String(sig64);
        byte[] msgBytes = FormatMessageForSigning(message);

        int first = (sigBytes[0] - 27);
        bool comp = (first & 4) != 0;
        int rec = first & 3;

        BigInteger[] sig = ParseSig(sigBytes, 1);
        byte[] msgHash = DigestUtilities.CalculateDigest("SHA-256", DigestUtilities.CalculateDigest("SHA-256", msgBytes));

        ECPoint Q = Recover(msgHash, sig, rec, true);

        byte[] qEnc = Q.GetEncoded(comp);
        Console.WriteLine("Q: " + Hex.ToHexString(qEnc));

        byte[] qHash = DigestUtilities.CalculateDigest("RIPEMD-160", DigestUtilities.CalculateDigest("SHA-256", qEnc));
        Console.WriteLine("RIPEMD-160(SHA-256(Q)): " + Hex.ToHexString(qHash));

        Console.WriteLine("Signature verified correctly: " + VerifySignature(Q, msgHash, sig));
    }

    public static BigInteger[] ParseSig(byte[] sigBytes, int sigOff)
    {
        BigInteger r = new BigInteger(1, sigBytes, sigOff, 32);
        BigInteger s = new BigInteger(1, sigBytes, sigOff + 32, 32);
        return new BigInteger[] { r, s };
    }

    public static ECPoint Recover(byte[] hash, BigInteger[] sig, int recid, bool check)
    {
        X9ECParameters x9 = SecNamedCurves.GetByName("secp256k1");

        BigInteger r = sig[0], s = sig[1];

        FpCurve curve = x9.Curve as FpCurve;
        BigInteger order = x9.N;

        BigInteger x = r;
        if ((recid & 2) != 0)
        {
            x = x.Add(order);
        }

        if (x.CompareTo(curve.Q) >= 0) throw new Exception("X too large");

        byte[] xEnc = X9IntegerConverter.IntegerToBytes(x, X9IntegerConverter.GetByteLength(curve));

        byte[] compEncoding = new byte[xEnc.Length + 1];
        compEncoding[0] = (byte)(0x02 + (recid & 1));
        xEnc.CopyTo(compEncoding, 1);
        ECPoint R = x9.Curve.DecodePoint(compEncoding);

        if (check)
        {
            //EC_POINT_mul(group, O, NULL, R, order, ctx))
            ECPoint O = R.Multiply(order);
            if (!O.IsInfinity) throw new Exception("Check failed");
        }

        BigInteger e = CalculateE(order, hash);

        BigInteger rInv = r.ModInverse(order);
        BigInteger srInv = s.Multiply(rInv).Mod(order);
        BigInteger erInv = e.Multiply(rInv).Mod(order);

        return ECAlgorithms.SumOfTwoMultiplies(R, srInv, x9.G.Negate(), erInv);
    }

    public static bool VerifySignature(ECPoint Q, byte[] hash, BigInteger[] sig)
    {
        X9ECParameters x9 = SecNamedCurves.GetByName("secp256k1");
        ECDomainParameters ec = new ECDomainParameters(x9.Curve, x9.G, x9.N, x9.H, x9.GetSeed());
        ECPublicKeyParameters publicKey = new ECPublicKeyParameters(Q, ec);
        return VerifySignature(publicKey, hash, sig);
    }

    public static bool VerifySignature(ECPublicKeyParameters publicKey, byte[] hash, BigInteger[] sig)
    {
        ECDsaSigner signer = new ECDsaSigner();
        signer.Init(false, publicKey);
        return signer.VerifySignature(hash, sig[0], sig[1]);
    }

    private static BigInteger CalculateE(
        BigInteger n,
        byte[] message)
    {
        int messageBitLength = message.Length * 8;
        BigInteger trunc = new BigInteger(1, message);

        if (n.BitLength < messageBitLength)
        {
            trunc = trunc.ShiftRight(messageBitLength - n.BitLength);
        }

        return trunc;
    }

    public static byte[] FormatMessageForSigning(String message)
    {
        MemoryStream bos = new MemoryStream();
        bos.WriteByte((byte)BITCOIN_SIGNED_MESSAGE_HEADER_BYTES.Length);
        bos.Write(BITCOIN_SIGNED_MESSAGE_HEADER_BYTES, 0, BITCOIN_SIGNED_MESSAGE_HEADER_BYTES.Length);
        byte[] messageBytes = Encoding.UTF8.GetBytes(message);

        //VarInt size = new VarInt(messageBytes.length);
        //bos.write(size.encode());
        // HACK only works for short messages (< 253 bytes)
        bos.WriteByte((byte)messageBytes.Length);

        bos.Write(messageBytes, 0, messageBytes.Length);
        return bos.ToArray();
    }

问题中初始数据的示例输出:

Q: 0283437893b491218348bf5ff149325e47eb628ce36f73a1a927ae6cb6021c7ac4
RIPEMD-160(SHA-256(Q)): cbe57ebe20ad59518d14926f8ab47fecc984af49
Signature verified correctly: True

如果我们将 RIPEMD-160 值插入地址检查器,它会返回

1Kb76YK9a4mhrif766m321AMocNvzeQxqV

如问题中所述。

【讨论】:

  • 这段代码非常棒,与 QT 客户端完美配合。我在创建能够通过此代码验证的签名时遇到问题。你能帮我解决这个问题吗?
猜你喜欢
  • 2013-07-14
  • 2022-10-20
  • 2012-09-10
  • 2014-01-17
  • 2020-03-04
  • 1970-01-01
  • 2017-04-02
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多