【问题标题】:Generate openssl keypair using java使用 java 生成 openssl 密钥对
【发布时间】:2020-07-23 12:31:23
【问题描述】:

我需要在 java 中生成 openssl 密钥对来模拟以下内容:

openssl ecparam -name prime256v1 -genkey -noout -out prime256v1.key

openssl pkcs8 -topk8 -in prime256v1.key -out prime256v1-priv.pem -nocrypt

openssl ec -in prime256v1-priv.pem -pubout -out prime256v1-pub.pem

我的java程序如下:

public static void main(String args[]) throws Exception{

    Security.addProvider(new BouncyCastleProvider());
    KeyPairGenerator g = KeyPairGenerator.getInstance("ECDSA", "BC");
    ECGenParameterSpec spec = new ECGenParameterSpec("secp256r1");
    g.initialize(spec);
    KeyPair keyPair = g.generateKeyPair();

    byte[] publicKeyBytes = keyPair.getPublic().getEncoded();
    String publicKeyContent = Base64.encode(publicKeyBytes);
    String publicKeyFormatted = "-----BEGIN PUBLIC KEY-----" + System.lineSeparator();
    for (final String row:
            Splitter
                    .fixedLength(64)
                    .split(publicKeyContent)
            )
    {
        publicKeyFormatted += row + System.lineSeparator();
    }
    publicKeyFormatted += "-----END PUBLIC KEY-----";
    BufferedWriter writer = new BufferedWriter(new FileWriter("publickey.pem"));
    writer.write(publicKeyFormatted);
    writer.close();

    byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
    String privateKeyContent = Base64.encode(privateKeyBytes);
    String privateKeyFormatted = "-----BEGIN PRIVATE KEY-----" + System.lineSeparator();
    for (final String row:
            Splitter
                    .fixedLength(64)
                    .split(privateKeyContent)
            )
    {
        privateKeyFormatted += row + System.lineSeparator();
    }
    privateKeyFormatted += "-----END PRIVATE KEY-----";
    BufferedWriter writer2 = new BufferedWriter(new FileWriter("privatekey.pem"));
    writer2.write(privateKeyFormatted);
    writer2.close();
}

上述代码有效,但生成的私钥似乎比我在顶部提到的通过命令行实用程序生成的私钥长。

带有命令行的私钥:

-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgGuyf3+/6+rnDKw0D
WbxVyggwNL0jlTVAzGm6cpl3ji2hRANCAAQ7zLtxLLvl6LJHJAlYAZr4hAc09fZn
bAniYIeKVqVBdKIvb5R445PFiUDFcfyneeX/resPXJHMEm/vAxfQeMqL
-----END PRIVATE KEY-----

使用 java 的私钥:

-----BEGIN PRIVATE KEY-----
MIGTAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBHkwdwIBAQQgYFPrkmxnwjVBgpUV
B02/luLD1rt9
UWZHj62YdhwYQESgCgYIKoZIzj0DAQehRANCAATZp7Jl8KXXApA
hvv9qeQtX5LbHQkrCdx3DfkUC
GgCUMSJWKxs7yJPNKtFZnFUTFZfyEF76fdEzky
zIon5H04MX
-----END PRIVATE KEY-----

即使我在这里删除了 2 额外的行,即便如此,这似乎是一个更大的关键。

带有命令行的公钥:

-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEO8y7cSy75eiyRyQJWAGa+IQHNPX2
Z2wJ4mCHilalQXSiL2+UeOOTxYlAxXH8p3nl/63rD1yRzBJv7wMX0HjKiw==
-----END PUBLIC KEY-----

使用 Java 的公钥:

-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2aeyZfCl1wKQIb7/ankLV+S2x0JK
wncdw35FAhoA
lDEiVisbO8iTzSrRWZxVExWX8hBe+n3RM5MsyKJ+R9ODFw==
-----END PUBLIC KEY-----

所以,我的第一个问题是关于私钥的长度。似乎更长了。 我的第二个问题是我似乎没有正确拆分生成的密钥字节。肯定有比预期更多的行。如何纠正?

【问题讨论】:

  • JcaPEMWriter 帮了我大忙。

标签: java security openssl cryptography bouncycastle


【解决方案1】:

...私钥长度...似乎更长

是,或者准确地说,表示/包含私钥的结构更长。 Java 包含可选 -- 和不必要(冗余)在 PKCS8 -- 'parameters' 字段中的算法特定数据,即在SEC1 appendix C.4 中定义的 ECPrivateKey,而 OpenSSL 没有。这将在读回时被忽略。两个结构中的实际键值都是正确的大小。

我没有正确拆分生成的密钥字节

而是拆分(base64)对(密钥结构的)字节进行编码的字符。

查看Base64.encode 之前 Splitter 的输出。我打赌你会发现它在每 76 个 base64 字符之后已经包含换行符,这与一些人认为更常见(或更重要?)或至少比 PEM 更新的 MIME 标准(RFC 1521 et seq)一致。 (虽然 XML 和 JWX 甚至更新,现在很常见,而且根本不插入换行符。)因此您的 Splitter 需要:

  • 第一行的前 64 个字符
  • 第一行的剩余 12 个字符、一个换行符和第二行的 51 个字符
  • 第二行的剩余 25 个字符、一个换行符和(最多)第三行的 38 个字符

尽管根据 PEM 标准 (RFC 1421),OpenSSL 写入 PEM 文件每 64 个字符(最后一行除外)带有正文换行符,但它始终能够读取文件,包含 4 到 76 个字符的任意倍数,与 MIME 一致。自 2016 年 1.1.0 以来的最新版本,现已被广泛采用,可以读取多达数百个字符的行。因此,如果您的文件将由(任何使用)OpenSSL 库读取,您可以只编写 split-at-76 版本而无需任何进一步的更改,除非确保有一个换行符终止 last 行。其他软件可能有所不同;如果您需要安全或严格遵守,请先删除您的Base64.encode 输出中的换行符,然后以正确的间距64 重新添加它们。请参阅the recently published respecification

PS:如果您使用 Java 将此密钥放入 PKCS12 keystore(这需要您拥有/获取/创建证书),openssl 命令行 可以读取直接,然后将 (1) 私钥转换为 PEM,(2) 将证书转换为 PEM,您可以从中提取 PEM 中的公钥。

【讨论】: