Java 11 Curve25519 实现不像信号库

Java 11 Curve25519 Implementation doesn't behave as Signal's libary

在Java11中引入了curve25519内置实现。因为我不知道这一点,直到最近才发现它,所以我使用的是 a library from Signal。这是我切换到 Java 11 的实现之前的代码:

private final Curve25519 CURVE_25519 = Curve25519.getInstance(Curve25519.JAVA);

public Curve25519KeyPair calculateRandomKeyPair() {
    return CURVE_25519.generateKeyPair();
}

public byte[] calculateSharedSecret(byte[] publicKey, byte[] privateKey) {
    return CURVE_25519.calculateAgreement(publicKey, privateKey);
}

现在这是我的代码:

private final String XDH = "XDH";
private final String CURVE = "X25519";

@SneakyThrows
public KeyPair calculateRandomKeyPair() {
    return KeyPairGenerator.getInstance(CURVE).generateKeyPair();
}

@SneakyThrows
public byte[] calculateSharedSecret(byte[] publicKeyBytes, XECPrivateKey privateKey) {
    var paramSpec = new NamedParameterSpec(CURVE);
    var keyFactory = KeyFactory.getInstance(XDH);

    var publicKeySpec = new XECPublicKeySpec(paramSpec, new BigInteger(publicKeyBytes));
    var publicKey = keyFactory.generatePublic(publicKeySpec);

    var keyAgreement = KeyAgreement.getInstance(XDH);
    keyAgreement.init(privateKey);
    keyAgreement.doPhase(publicKey, true);
    return keyAgreement.generateSecret();
}

显然,第二个实现不起作用,而第一个实现起作用。最初,我以为我做错了什么,所以我阅读了文档并检查了类似的答案,但是,因为我没有找到任何有用的东西,所以我决定进一步挖掘并尝试检查 Signal 的库和 Java 是否生成给定私有密钥的相同 public 密钥。为此,我写了这个片段:

import org.whispersystems.curve25519.Curve25519;
import sun.security.ec.XECOperations;
import sun.security.ec.XECParameters;

import java.security.InvalidAlgorithmParameterException;
import java.security.spec.NamedParameterSpec;
import java.util.Arrays;

private static boolean generateJava11KeyPair() throws InvalidAlgorithmParameterException {
    var signalKeyPair = Curve25519.getInstance(Curve25519.JAVA).generateKeyPair();
    
    var signalPublicKey = signalKeyPair.getPublicKey();
    var params = XECParameters.get(InvalidAlgorithmParameterException::new, NamedParameterSpec.X25519);
    var ops = new XECOperations(params);
    var javaPublicKey = ops.computePublic(signalKeyPair.getPrivateKey().clone()).toByteArray();
    
    return Arrays.equals(signalPublicKey, javaPublicKey);
}

(在 Java 的实现之后用于计算 public 键的代码是从 sun.security.ec.XDHKeyPairGenerator 中提取的)

此方法打印 false,这意味着这两个实现实际上并不以相同的方式运行。此时,我想知道这是 Java 错误还是我遗漏了什么。提前致谢。

Bernstein 等人为 X25519(和 X448)密钥 public 和私有密钥定义的编码是无符号固定长度小端,而表示 return 由 BigInteger.toByteArray() 并被 ctor BigInteger(byte[]) 接受的是二进制补码可变长度大端。由于 255 位四舍五入为 32 字节,备用位始终为零(对于 XDH),因此可以忽略符号差异,但其他因素很重要。

JCA 确实使接口 class XECPrivateKey return 和相应的 Spec 接受这些形式,但是对于 XECPublicKey[Spec] 它使用 BigInteger。它确实对“X509”和“PKCS8”编码(分别)使用 Bernstein 格式(分别)return 由 Key.getEncoded() 编辑并被 KeyFactory 接受,但那些具有元数据仅 XDH(或仅 Bernstein XDH-and-EdDSA)系统如 X3DH 不使用。

AFAICS 你的选择是

  • byte-reverse and (zero-)pad when needed the JCA public values in your code, or
  • 使用 Key.getEncoded() 并解析算法特定部分或相反构建算法通用结构以作为 X509EncodedKeySpec 传递给 KeyFactory.getInstance("Xblah")

第二种方法过去曾被问及其他算法:'traditional'(X9 风格)EC——尤其是用于比特币和相关硬币的 secp256k1,通常只使用 raw-X9/SECG没有元数据的数据——和 RSA,其中一些系统使用原始 PKCS1 格式(这里更常见的是私钥而不是 publickey);如果你愿意,我可以找到一些近似的副本来说明该方法。