如何在Java中获取EC证书的公钥信息(无需调用OpenSSL)
问题:Java中无需调用OpenSSL打印EC证书公钥信息
已知可在Java中读取PEM证书并解析得到ECPublicKey对象,但不知道如何从该对象的ECParameter和ECPoint数据中提取出OpenSSL输出里的公钥格式(带04前缀、冒号分隔的十六进制字节串,对应256位EC密钥的未压缩公钥)。
OpenSSL打印的公钥示例:
pub: <<
04:7a:ef:a5:53:5a:f7:a4:15:a9:ca:5a:63:6b:c9: <<
f7:d3:1a:22:c0:71:df:01:dc:ff:62:c8:aa:97:f6: <<
66:52:a4:31:08:d2:a5:5b:d5:6e:54:25:fc:89:d3: <<
0c:c2:95:1c:94:a6:96:e7:e8:b2:5e:fc:f7:48:5d: <<
42:3a:ca:81:79 <<
核心原理
EC公钥的未压缩格式遵循X.509规范:以字节04开头,随后依次拼接公钥点的x坐标和y坐标的字节数组(256位密钥对应每个坐标32字节)。从ECPublicKey的getW()方法可获取ECPoint对象,从中提取x、y坐标即可生成目标公钥串。
实现步骤
- 从
ECPublicKey获取ECPoint公钥点; - 提取x、y坐标的
BigInteger值,转换为固定32字节的数组(补前导零); - 拼接
04前缀、x字节数组、y字节数组,得到完整未压缩公钥字节数组; - 将字节数组格式化为OpenSSL风格的冒号分隔十六进制字符串,按每行16字节拆分。
代码示例
import java.math.BigInteger; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.security.interfaces.ECPublicKey; import java.security.spec.ECPoint; import java.io.ByteArrayInputStream; public class ECPubKeyPrinter { public static void main(String[] args) throws Exception { // 输入PEM证书内容 String pemCert = "-----BEGIN CERTIFICATE-----\n" + "MIIBzDCCAXGgAwIBAgIBATAKBggqhkjOPQQDAjBcMQswCQYDVQQGEwJVUzELMAkG\n" + "A1UECBMCQ0ExCzAJBgNVBAcTAlBBMQswCQYDVQQKEwJCQzEMMAoGA1UECxMDVkNG\n" + "MRgwFgYDVQQDEw9uby1rdS10ZXN0LWhvc3QwIBcNMjQwMjIzMTc1ODAwWhgPMjEy\n" + "NDAyMjMxNzU4MDBaMFwxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTELMAkGA1UE\n" + "BxMCUEExCzAJBgNVBAoTAkJDMQwwCgYDVQQLEwNWQ0YxGDAWBgNVBAMTD25vLWt1\n" + "LXRlc3QtaG9zdDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHrvpVNa96QVqcpa\n" + "Y2vJ99MaIsBx3wHc/2LIqpf2ZlKkMQjSpVvVblQl/InTDMKVHJSmlufosl7890hd\n" + "QjrKgXmjIjAgMAkGA1UdEwQCMAAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwCgYIKoZI\n" + "zj0EAwIDSQAwRgIhAOHnrVfm+K1Q0g5ds4tiMZyV00jWybqJIIlPyB4dhqemAiEA\n" + "pMJa4NY0J+3c1wVNQ6l3C5AVJsg8ld/EL11TaochQ+U=\n" + "-----END CERTIFICATE-----"; // 解析PEM证书 CertificateFactory cf = CertificateFactory.getInstance("X.509"); X509Certificate cert = (X509Certificate) cf.generateCertificate( new ByteArrayInputStream(pemCert.getBytes()) ); // 验证并获取EC公钥 if (!(cert.getPublicKey() instanceof ECPublicKey)) { throw new IllegalArgumentException("证书中的公钥不是EC类型"); } ECPublicKey ecPubKey = (ECPublicKey) cert.getPublicKey(); ECPoint ecPoint = ecPubKey.getW(); // 提取x、y坐标并转换为固定32字节数组 byte[] xBytes = toFixedLengthBytes(ecPoint.getAffineX(), 32); byte[] yBytes = toFixedLengthBytes(ecPoint.getAffineY(), 32); // 拼接未压缩格式公钥 byte[] uncompressedPub = new byte[65]; uncompressedPub[0] = 0x04; System.arraycopy(xBytes, 0, uncompressedPub, 1, 32); System.arraycopy(yBytes, 0, uncompressedPub, 33, 32); // 格式化为OpenSSL风格输出 String formattedPub = formatToOpenSSLStyle(uncompressedPub); System.out.println("pub: <<"); System.out.println(formattedPub); } // 将BigInteger转换为固定长度字节数组,补前导零 private static byte[] toFixedLengthBytes(BigInteger num, int length) { byte[] rawBytes = num.toByteArray(); int startIdx = rawBytes[0] == 0 ? 1 : 0; int actualLen = rawBytes.length - startIdx; if (actualLen > length) { throw new IllegalArgumentException("坐标长度超过指定限制"); } byte[] result = new byte[length]; System.arraycopy(rawBytes, startIdx, result, length - actualLen, actualLen); return result; } // 格式化为OpenSSL风格的十六进制字符串 private static String formatToOpenSSLStyle(byte[] data) { StringBuilder sb = new StringBuilder(); for (int i = 0; i < data.length; i++) { if (i > 0) sb.append(':'); sb.append(String.format("%02X", data[i])); // 每16字节换行并添加标记 if ((i + 1) % 16 == 0 && i != data.length - 1) { sb.append(": <<\n "); } } sb.append(" <<"); return sb.toString(); } }
代码说明
toFixedLengthBytes:处理BigInteger.toByteArray()的输出,确保生成固定长度的字节数组(256位密钥对应32字节),补前导零以符合X.509规范;formatToOpenSSLStyle:将字节数组转换为冒号分隔的十六进制字符串,按每行16字节拆分,完全模拟OpenSSL的输出格式;- 核心逻辑:通过
ECPublicKey.getW()获取公钥点,提取坐标后拼接成未压缩格式的公钥字节数组。
内容的提问来源于stack exchange,提问作者Mark Boon
相关产品推荐
相关产品推荐

