当前位置: 首页 > 知识库问答 >
问题:

如何从比特币签名中获取ECDSA公钥?... SEC1 4.1.6(mod p)字段曲线的密钥恢复

林冥夜
2023-03-14

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

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

请注意,要验证的消息必须以< code >比特币签名消息:\n为前缀。源1源2

C#实现中有一些错误,我可能可以从这个Python实现中纠正

它似乎在实际提供正确的基58地址方面存在问题。

我有下面的消息、签名和Base58地址。我打算从签名中提取密钥,散列该密钥,并比较Base58散列。

我的问题是:如何从签名中提取密钥?(编辑我在这篇文章的底部找到了c代码,需要它在Bouncy Castle/或C#)

消息

StackOverflow test 123

签名

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

Base58 比特币地址“哈希”

1Kb76YK9a4mhrif766m321AMocNvzeQxqV

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

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

问题

>

  • 在上面的签名中,这个签名是什么格式的?PKCS10?(答案:不,它是专有的,如下所述)

    如何提取Bouncy Castle中的公钥?

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

    先前的研究

    此链接描述了如何使用ECDSA曲线,下面的代码将允许我将公钥转换为BC对象,但我不确定如何从签名中获取点< code>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);
    

    额外研究:

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

    解码签名的Base64后,调用RecoverCompact(消息散列,签名)。我不是C程序员,所以我假设我需要弄清楚key.Recover是如何工作的。那或key.GetPubKey

    这是我认为我在C#中需要的C代码,理想情况下是在弹力城堡中...但我会接受任何有用的东西。

    // reconstruct html" target="_blank">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代码在这里

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

    这个答案说有4个可能的公钥可以生成签名,并且这是在较新的签名中编码的。

  • 共有2个答案

    白浩气
    2023-03-14

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

    我编写了计算字符串“StackOverflow test 123”的正确公钥的代码,并使用ECDsaSigner验证它。但是,此公钥的哈希是< code > 1 hrde 7g 7 TN 925 inxqaed 7 r 2 zkzikown 8 NW ,而不是< code > 1kb 76 yk 9 a 4 mhrif 766m 321 amocnvzeqxqv 。

    请您验证您的数据是否正确,并可能给出消息字符串的确切哈希值,以便我们尝试html" target="_blank">调试,不正确的哈希值可能会把事情搞得一团糟。我使用的代码如下:

    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));
      }
    
    魏彦
    2023-03-14

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

    下面的代码应该只需要BouncyCastle(可能你需要github的最新版本,不确定)。它从BitcoinJ借用了一些东西,并且做得足够多,可以让小例子工作,请参阅内联评论以获取消息大小限制。

    它最多只能计算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
    

    如问题中所述。

     类似资料:
    • 我已经阅读了如何在OpenSSL中从ECDSA私钥获取公钥? 并想做同样的事情,但在Java中与充气城堡。 我也见过Bouncy Castle ESCDA从私钥创建公钥,但这并没有帮助。

    • 从这里我指出了自己如何获得公钥,并使用EC_POINT_bn2point,而不是hex2point,根据OpenSSL源代码,它在内部执行BN_hex2bn。 那么,为什么EC_POINT_bn2point返回null呢?我正在认真考虑重新编译OpenSSL,并放一些调试例程来找出它失败的原因。

    • 问题内容: 目前,我正在使用此方法创建keyPair 还有另一种方法,我可以指定keySize,但不确定如何传递keySpecs? 问题答案: 您的代码已经足够,并且指定“ secp256k1”已经设置了正确的大小。该方法是一种 替代 到; 您呼叫一个或另一个,而不是两个都呼叫。如果调用指定密钥大小的密钥(例如256),则BC提供程序将尝试选择正确大小的默认曲线(对于256,它将是“ prime2

    • 我从服务器获得了以下jwks 我有一个加密的令牌。我想用上面的jwk解密它。如何从上述jwk中获取公钥。我从楼梯上走下来https://auth0.com/docs/quickstart/backend/rails/01-authorization.但上述jwk中缺少x5c(证书)。 我想知道在jwk中没有x5c如何获取公钥。

    • 我正在使用Ruby 2.5.x OpenSSL库研究椭圆曲线。我可以很容易地使用 但是给定一个私钥,我想重新生成公钥。 我知道OpenSSL可以做到这一点,因为命令行允许您这样做,Ruby比特币项目也可以做到这一点。但是Ruby比特币项目使用FFI而不是Ruby提供的接口有自己的OpenSSL接口。 Ruby 2.5.x openssl 库是否没有公开足够的 OpenSSL 接口,以便能够从私钥生

    • 本文向大家介绍Android 获取签名公钥和公钥私钥加解密的方法(推荐),包括了Android 获取签名公钥和公钥私钥加解密的方法(推荐)的使用技巧和注意事项,需要的朋友参考一下 如下所示: 以上这篇Android 获取签名公钥和公钥私钥加解密的方法(推荐)就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持呐喊教程。