BigInteger 中的 .NET 错误?

.NET bug in BigInteger?

我一直在处理一个需要 BigIntegers 的 .NET 项目,我注意到框架的实现提供了看起来不正确的结果。经过数小时的尝试找出我做错了什么之后,我决定在 Java 和 C++(使用 OpenSSL)中测试我的算法,令人震惊的是,在这两种语言中我都得到了预期的结果。

现在我很自然地想知道我做错了什么(因为地球上没有办法这是一个以前没有注意到的错误),我希望有人能帮助我!

这是精简的 C# 代码:

using System;
using System.Numerics;
using System.Globalization;

public class Program
{
    public static void Main()
    {
        var B = BigInteger.Parse("023B61801145A9CB06ADF77493042D166E793B946D1B07B46070E3986A6F036BE", NumberStyles.AllowHexSpecifier);
        var k = BigInteger.Parse("3", NumberStyles.AllowHexSpecifier);
        var x = BigInteger.Parse("09F015DB40A59403E42FBD568AF5774A0A0488A62", NumberStyles.AllowHexSpecifier);
        var g = BigInteger.Parse("7", NumberStyles.AllowHexSpecifier);
        var N = BigInteger.Parse("0894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7", NumberStyles.AllowHexSpecifier);
        var u = BigInteger.Parse("0AC06F615645BEA9B3D6D887C30D28D71B079B598", NumberStyles.AllowHexSpecifier);
        var a = BigInteger.Parse("0D4515CA7747787F1DDA9962ACE81E8412D9D20D06251696ACD74735F1F3B9875", NumberStyles.AllowHexSpecifier);

        var S = calc(B, k, x, g, N, u, a);
        Console.WriteLine(S.ToString("X"));
    }

    static BigInteger calc(BigInteger B, BigInteger k, BigInteger x, BigInteger g, BigInteger N, BigInteger u, BigInteger a)
    {
        var val = B - k * BigInteger.ModPow(g, x, N);
        var exponent = a + u * x;
        return BigInteger.ModPow(val, exponent, N);
    }
}

你可以在这里执行:https://dotnetfiddle.net/qXXiBk

Java中的相同代码:

import java.math.BigInteger;

public class Main
{
  public static void main(String[] args)
  {
    BigInteger B = new BigInteger("023B61801145A9CB06ADF77493042D166E793B946D1B07B46070E3986A6F036BE", 16);
    BigInteger k = new BigInteger("3", 16);
    BigInteger x = new BigInteger("09F015DB40A59403E42FBD568AF5774A0A0488A62", 16);
    BigInteger g = new BigInteger("7", 16);
    BigInteger N = new BigInteger("0894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7", 16);
    BigInteger u = new BigInteger("0AC06F615645BEA9B3D6D887C30D28D71B079B598", 16);
    BigInteger a = new BigInteger("0D4515CA7747787F1DDA9962ACE81E8412D9D20D06251696ACD74735F1F3B9875", 16);

    BigInteger S = calc(B, k, x, g, N, u, a);
    System.out.println(S.toString(16));
  }

  private static BigInteger calc(BigInteger B, BigInteger k, BigInteger x, BigInteger g, BigInteger N, BigInteger u, BigInteger a)
  {
    BigInteger value = B.subtract(k.multiply(g.modPow(x, N)));
    BigInteger exponent = a.add(u.multiply(x));

    return value.modPow(exponent, N);
  }
}

你可以在这里执行:https://www.onlinegdb.com/BJXxMiO28

最后是一个使用 OpenSSL 的快速而肮脏的 C++ 实现:

#include <iostream>

 #include <openssl/bn.h>

 class BigInteger
 {
     public:
        BigInteger(char const* hexString, BN_CTX *ctx)
            : bn_{BN_new()}
            , ctx_{ctx}
        {
            BN_hex2bn(&bn_, hexString);
        }

        ~BigInteger()
        {
            BN_free(bn_);
        }

        BigInteger ModPow(BigInteger const& exponent, BigInteger const& modulo) const
        {
            BigInteger ret{"0", ctx_};
             BN_mod_exp(ret.bn_, bn_, exponent.bn_, modulo.bn_, ctx_);
             return ret;
        }

        BigInteger Subtract(BigInteger const& rhs) const
        {
            BigInteger ret{"0", ctx_};
            BN_sub(ret.bn_, bn_, rhs.bn_);
             return ret;
        }

        BigInteger Multiply(BigInteger const& rhs) const
        {
            BigInteger ret{"0", ctx_};
            BN_mul(ret.bn_, bn_, rhs.bn_, ctx_);
             return ret;
        }

        BigInteger Add(BigInteger const& rhs) const
        {
            BigInteger ret{"0", ctx_};
            BN_add(ret.bn_, bn_, rhs.bn_);
             return ret;
        }

        std::string ToString() const
        {
            return BN_bn2hex(bn_);
        }

     private:
     BIGNUM* bn_;
     BN_CTX *ctx_;
 };

BigInteger calc(BigInteger const& B, BigInteger const& k, BigInteger const& x, BigInteger const& g, BigInteger const& N, BigInteger const& u, BigInteger const& a)
{
    BigInteger value = B.Subtract(k.Multiply(g.ModPow(x, N)));
    BigInteger exponent = a.Add(u.Multiply(x));

    return value.ModPow(exponent, N);
}

int main()
{
     BN_CTX *ctx = BN_CTX_new();

    BigInteger B{"023B61801145A9CB06ADF77493042D166E793B946D1B07B46070E3986A6F036BE", ctx};
    BigInteger k{"3", ctx};
    BigInteger x{"09F015DB40A59403E42FBD568AF5774A0A0488A62", ctx};
    BigInteger g{"7", ctx};
    BigInteger N{"0894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7", ctx};
    BigInteger u{"0AC06F615645BEA9B3D6D887C30D28D71B079B598", ctx};
    BigInteger a{"0D4515CA7747787F1DDA9962ACE81E8412D9D20D06251696ACD74735F1F3B9875", ctx};

    auto S = calc(B, k, x, g, N, u, a);

    std::cout << S.ToString();
    BN_CTX_free(ctx);
}

你可以在这里执行:https://godbolt.org/z/PtNGdQ

同样,C++ 和 Java 都同意 218BC3CE2641EFF5F4BB95A2DB931CA62A933C6BA40D3F6E2AD5D5F7D41F0E0A 的答案,只有 C# 说是 98405F6F9C609C9A370E3A17B28CCC5322918ADCE44DE0DE7F995370A9E07253。这是一个真正的阻碍,因为我需要在需要第一个(正确)答案的系统上工作。我真的在这里不知所措,我真诚地希望有人知道我做错了什么。

干杯

Python 也同意答案应该是 218bc3ce2641eff5f4bb95a2db931ca62a933c6ba40d3f6e2ad5d5f7d41f0e0a

问题似乎不是十六进制解析(即使解析值的十进制版本结果也是一样的)。

我认为你的态度是正确的,认为这不可能是 C# 实现中大整数中的错误,但在我看来,这实际上是事实的有力证据(即使我必须说我不是 C# 程序员,只玩了一点)。

我认为你应该提交错误报告。

编辑

正如 Rufo 爵士在评论中正确指出的那样,问题在于 C# 中如何处理负股息的模运算,将代码更改为

var val = (B - k * BigInteger.ModPow(g, x, N) + N*k) % N;

产生了预期的结果。

我会说仍然是一个错误,但是一个设计错误并且不会被修复。

信息

让我们看一下 calc 方法:

当我们比较 C# val.ToString("X") 和 Java val.toString(16) 中的十六进制输出值时,我们将得到不同的输出:

C#:   F4EB82A8CAFDA89F0E2B69C3C4FEF2920913B60DD701C2193C41AE7EC6BC1A38B
Java: -b147d5735025760f1d4963c3b010d6df6ec49f228fe3de6c3be51813943e5c75

但是当我们在 C# val.ToString() 和 Java val.toString(10) 中使用十进制输出值时,我们将得到相同的输出:

C#:   -80186293521643543106092742417459818853945355375849134884320433064971933211765
Java: -80186293521643543106092742417459818853945355375849134884320433064971933211765

此答案基于您无法比较的比较(十六进制输出)。


(将此作为答案发布,因为它不适合评论,但将其设为社区维基):

C# 和 Java 版本之间的差异发生在 calc 中。当我像这样分离出中间值时:

CSharp

BigInteger B = BigInteger.Parse("023B61801145A9CB06ADF77493042D166E793B946D1B07B46070E3986A6F036BE", NumberStyles.AllowHexSpecifier);
BigInteger k = BigInteger.Parse("3", NumberStyles.AllowHexSpecifier);
BigInteger g = BigInteger.Parse("7", NumberStyles.AllowHexSpecifier);
BigInteger x = BigInteger.Parse("09F015DB40A59403E42FBD568AF5774A0A0488A62", NumberStyles.AllowHexSpecifier);
BigInteger N = BigInteger.Parse("0894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7", NumberStyles.AllowHexSpecifier);

Console.WriteLine( "B == " + B.ToString("X") );
Console.WriteLine( "k == " + k.ToString("X") );
Console.WriteLine( "g == " + g.ToString("X") );
Console.WriteLine( "x == " + x.ToString("X") );
Console.WriteLine( "N == " + N.ToString("X") );

Console.WriteLine( "-------" );

BigInteger p = BigInteger.ModPow(g, x, N);
Console.WriteLine( "p == " + p.ToString("X") );

BigInteger m = k * p;
Console.WriteLine( "m == " + m.ToString("X") );

BigInteger d = B - m;
Console.WriteLine("d == " + d.ToString("X"));

Java

BigInteger B = new BigInteger("023B61801145A9CB06ADF77493042D166E793B946D1B07B46070E3986A6F036BE", 16);
BigInteger k = new BigInteger("3", 16);
BigInteger g = new BigInteger("7", 16);
BigInteger x = new BigInteger("09F015DB40A59403E42FBD568AF5774A0A0488A62", 16);
BigInteger N = new BigInteger("0894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7", 16);

System.out.println("B == " + B.toString(16));
System.out.println("k == " + k.toString(16));
System.out.println("g == " + g.toString(16));
System.out.println("x == " + x.toString(16));
System.out.println("N == " + N.toString(16));

System.out.println("-------");

BigInteger p = g.modPow(x, N);
System.out.println("p == " + p.toString(16));

BigInteger m = k.multiply(p);
System.out.println("m == " + m.toString(16));

BigInteger d = B.subtract(m);
System.out.println("d == " + d.toString(16));       

这些给了我这个输出:

CSharp:

B == 23B61801145A9CB06ADF77493042D166E793B946D1B07B46070E3986A6F036BE
k == 3
g == 7
x == 09F015DB40A59403E42FBD568AF5774A0A0488A62
N == 0894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7
-------
p == 46FF4F26CC2AB0EA82B849044AC68D6CC772C8232086C890C0FBC5DE13BA3111
m == 0D4FDED74648012BF8828DB0CE053A84656585869619459B242F3519A3B2E9333
value == F4EB82A8CAFDA89F0E2B69C3C4FEF2920913B60DD701C2193C41AE7EC6BC1A38B

Java:

B == 23b61801145a9cb06adf77493042d166e793b946d1b07b46070e3986a6f036be                                                                                                 
k == 3                                                                                                                                                                
g == 7                                                                                                                                                                
x == 9f015db40a59403e42fbd568af5774a0a0488a62                                                                                                                         
N == 894b645e89e1535bbdad5b8b290650530801b18ebfbf5e8fab3c82872a3e9bb7                                                                                                 
-------                                                                                                                                                               
p == 46ff4f26cc2ab0ea82b849044ac68d6cc772c8232086c890c0fbc5de13ba3111                                                                                                 
m == d4fded74648012bf8828db0ce053a84656585869619459b242f3519a3b2e9333                                                                                                 
d == -b147d5735025760f1d4963c3b010d6df6ec49f228fe3de6c3be51813943e5c75       

所以在 B - m 中发生了一些奇怪的事情,而不是 ModPow 调用。

第 2 部分

让我们通过序列化 BigInteger 值(我确认它们被正确序列化)将这种情况减少到 d = B - m

CSharp

BigInteger B = BigInteger.Parse("023B61801145A9CB06ADF77493042D166E793B946D1B07B46070E3986A6F036BE", NumberStyles.AllowHexSpecifier);
Console.WriteLine( "B == " + B.ToString("X") )

BigInteger m = new BigInteger( new Byte[] { 51, 147, 46, 59, 154, 81, 243, 66, 178, 89, 148, 97, 105, 88, 88, 86, 70, 168, 83, 224, 12, 219, 40, 136, 191, 18, 128, 100, 116, 237, 253, 212, 0 } );
Console.WriteLine( "m == " + m.ToString("X") )

BigInteger d = B - m;
Console.WriteLine( "d == " + d.ToString("X") )

Java:

BigInteger B = new BigInteger("023B61801145A9CB06ADF77493042D166E793B946D1B07B46070E3986A6F036BE", 16);
BigInteger m = new BigInteger( new byte[] { 0, -44, -3, -19, 116, 100, -128, 18, -65, -120, 40, -37, 12, -32, 83, -88, 70, 86, 88, 88, 105, 97, -108, 89, -78, 66, -13, 81, -102, 59, 46, -109, 51 } );

System.out.println("B == " + B.toString(16));
System.out.println("m == " + m.toString(16));

BigInteger d = B.subtract(m);
System.out.println("d == " + d.toString(16));

这表明 C# 和 Java 对 Bm 的值相同,对 d 的值不同:

// C#:
B == 23B61801145A9CB06ADF77493042D166E793B946D1B07B46070E3986A6F036BE
m == 0D4FDED74648012BF8828DB0CE053A84656585869619459B242F3519A3B2E9333
d == F4EB82A8CAFDA89F0E2B69C3C4FEF2920913B60DD701C2193C41AE7EC6BC1A38B

// Java:
B == 23b61801145a9cb06adf77493042d166e793b946d1b07b46070e3986a6f036be                                                                                
m == d4fded74648012bf8828db0ce053a84656585869619459b242f3519a3b2e9333                                                                                
d == -b147d5735025760f1d4963c3b010d6df6ec49f228fe3de6c3be51813943e5c75           

问题是 - F4EB82A8CAFDA89F0E2B69C3C4FEF2920913B60DD701C2193C41AE7EC6BC1A38B 代表与 -b147d5735025760f1d4963c3b010d6df6ec49f228fe3de6c3be51813943e5c75 相同的值吗?