从本机 java 中的字节数组私钥生成 EC public 密钥 (7+)

Generate EC public key from byte array private key in native java (7+)

我正在尝试学习一些密码编码,并生成了一个当前保存在字节数组中的 32 字节私钥 (byte[] privatekey)。我知道 public 密钥是使用 secp256k1 命名的椭圆曲线参数和公式 publickey = G * privatekey 生成的,其中 G 是椭圆曲线上的某个点(ECPoint ?),但我无法将命名参数规范和公式转换为 public 键的实际编码。我知道自 java 7 以来,java.security.*java.security.spec.* 包中包含 类 可以用短代码执行此操作,但我找不到一个很好的例子来展示如何在不使用第三方库的情况下执行此操作。

This bitcoin stackexchange link has all the theoretical answer and great python and C# code, but nothing in Java.

Edit/Update:我试图用下面的代码得到我需要的东西:

String secp256k1_G_uncompressed_string = "0479BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8";
byte[] secp256k1_G_uncompressed_bytes = DatatypeConverter.parseHexBinary(secp256k1_G_uncompressed_string);
String privatekeystring = "1184CD2CDD640CA42CFC3A091C51D549B2F016D454B2774019C2B2D2E08529FD";
byte[] privatekeybytes = DatatypeConverter.parseHexBinary(privatekeystring);
BigInteger secp256k1_G_num = new BigInteger(1, secp256k1_G_uncompressed_bytes);
BigInteger privatekey_num = new BigInteger(1, privatekeybytes);
BigInteger curvepoint = secp256k1_G_num.multiply(privatekey_num);
byte[] publickeybytes = curvepoint.toByteArray();
System.out.println(DatatypeConverter.printHexBinary(privatekeybytes));
System.out.println(DatatypeConverter.printHexBinary(publickeybytes));

正确编码生成的public密钥是:

04d0988bfa799f7d7ef9ab3de97ef481cd0f75d2367ad456607647edde665d6f6 fbdd594388756a7beaf73b4822bc22d36e9bda7db82df2b8b623673eefc0b7495

但是正在生成的 public 密钥是这样的:

4E6801418BB6EF9F462F69830F82EB51BB9224219B9D89C8C34FB746297F59779D8B986194181BD7AB99DC7E3086914EA13C4B37E05716CADCA0AE391CE81C4B85E0F09E8628F0F81692B5D08D0D8B9E20615A5D23DE0F591D02C650554BB1D8

椭圆曲线点不是整数。将点 (G) 的编码表示放在 BigInteger 中并尝试将其用作整数是不正确的。椭圆曲线点乘不是整数乘法,远没有BigInteger.multiply那么简单。它是用左边的标量写的,例如kG 不是 Gk.

将比特币 Q 上给出的标准(或至少是传统的)算法转换为 Java 对于任何 Java 程序员来说确实应该是一个相当简单的练习。

Scalar Multiplication of Point over elliptic Curve contains (in the answer) a correct implementation for P192 aka secp192r1; it can be converted to secp256k1 by replacing p and a with the values from the spec (SEC2 from https://www.secg.org or X9.62 if you have it) or any existing implementation -- including Java (see below) -- and discarding the P192-specific test data. Actually you mostly need to change p; the Koblitz curves were chosen to have a=0. Elliptic Curve Multiplication Function 包含一个 not-quite-correct 实现,据说是针对 secp256k1 但实际上并不包含任何曲线的常量。

since java 7, there are classes included in the java.security.* and java.security.spec.* packages to do this in short code

不是真的。首先,Java 加密将您在 java.securityjavax.crypto 中看到的 类 与实现代码隔离开来,后者在完全不同的 类 中(主要(仍然)在sun.*com.sun.*) 在一个或多个 'providers' 中,它们是单独的 jar 并且在技术上是可选的;可以在不更改代码中的调用的情况下删除、添加或更改提供程序,尽管大多数人不会这样做。自 java 5(称为 1.5)以来就存在用于 EC 加密的 JCA 'facade' 类,但标准构建中没有包含实施 EC 算法的提供程序;要使用它们,您必须添加一个 third-party 提供程序。从 java 7 开始,包含一个标准的 SunEC 提供商。但是,JCA(对于所有算法,不仅仅是 EC)在生成后将私有密钥和 public 密钥严格分开,特别是它无法访问 EC 内部存在的 private-to-public 派生逻辑。

它确实包括几个标准曲线的参数,包括 secp256k1,您可以使用它来避免从规范中复制它们的工作。似乎没有直接访问此数据的方法,但您可以通过生成随机数密钥并丢弃它来间接访问此数据。或者,由于您已经有一个私钥,您可以创建 Java 使用的编码 (PKCS8) 并读入,生成相同的曲线参数和一个可用的密钥。一般来说,构建像 PKCS8 这样的 ASN.1 DER 编码相当复杂,但是 对于 EC 它被简化了,因为 (1) 每个人都使用 'named' 形式将曲线编码为单个OID 和 (2) 标准指定了对给定曲线长度固定的私有值的编码;因此,给定 EC 曲线的 PKCS8 编码由固定前缀和后跟私钥值组成。示例片段:

    KeyPairGenerator kg = KeyPairGenerator.getInstance ("EC");
    kg.initialize (new ECGenParameterSpec ("secp256k1"));
    ECParameterSpec p = ((ECPublicKey) kg.generateKeyPair().getPublic()).getParams();
    System.out.println ("p=(dec)" + ((ECFieldFp) p.getCurve().getField()).getP() );
    ECPoint G = p.getGenerator(); 
    System.out.format ("Gx=(hex)%032x%n", G.getAffineX());
    System.out.format ("Gy=(hex)%032x%n", G.getAffineY());
    //
    byte[] privatekey_enc = DatatypeConverter.parseHexBinary(
            "303E020100301006072A8648CE3D020106052B8104000A042730250201010420"+
            "1184CD2CDD640CA42CFC3A091C51D549B2F016D454B2774019C2B2D2E08529FD");
    // note fixed prefix for PKCS8-EC-secp256k1 plus your private value
    KeyFactory kf = KeyFactory.getInstance("EC");
    PrivateKey k1 = kf.generatePrivate(new PKCS8EncodedKeySpec(privatekey_enc));
    ECParameterSpec p2 = ((ECPrivateKey) k1).getParams();
    System.out.println ("again p=(dec)" + ((ECFieldFp) p2.getCurve().getField()).getP() );

产生输出:

p=(dec)115792089237316195423570985008687907853269984665640564039457584007908834671663
Gx=(hex)79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798
Gy=(hex)483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8
again p=(dec)115792089237316195423570985008687907853269984665640564039457584007908834671663

请注意基点 G 的那些坐标符合您的预期。我混合显示了十进制和十六进制只是为了展示可能性;这不会影响计算机中的实际数字。

已添加以回应评论:

变量 p 和 p2 是 ECParameterSpec objects,其中包含 EC 曲线的参数(基础场、曲线系数、基点又名生成器、顺序和余因子;内部 'name' 尽管 API 没有公开它)。我打印的标记为 'p' 的值是调用 getP which returns one 曲线参数的结果,即基础素数的模数字段,因此您需要在链接 post 中显示的计算中使用的值 mod(p)modInverse(p)modPow(,p)。由于此 p(或 P)是曲线的参数,因此该曲线上的所有键都相同;请注意,我打印的两个值是相同的,即使它们来自不同的键。实际上有两种为密码学标准化的椭圆曲线:素数域上的曲线,表示为 Fp,以及特征二的扩展域上的曲线,表示为 F2m。 secp256k1 是第一种,这就是为什么在调用 getP() 之前转换为 ECFieldFp 的原因。

是的,我的固定前缀包含 headers 和标识私钥 (PKCS8) 编码的字段,用于 EC 和 secp256k1,并且该前缀对于所有 EC secp256k1 私钥都是相同的。 p 值如上所述,而不是私钥或 public 密钥。是的,如果你有 public 点,你可以将它与 ECParameterSpec 组合成一个 ECPublicKeySpec 并转换它并使用它 - 或者你可以将点编码附加到类似但不同的固定获取 X509EncodedKeySpec 的前缀,这是编码 Java 用于 public 键并转换 that 而无需提前 ECParameterSpec - - 但据我了解,你的整个问题是你还没有 public 点并且想要导出它,这需要链接的 post 中显示的点乘计算。