BCECPublicKey 到指纹

BCECPublicKey to fingerprint

当连接到 "new" SSH 服务器时,使用命令行,将显示指纹:

The authenticity of host 'test.com (0.0.0.0)' can't be established.
ECDSA key fingerprint is SHA256:566gJgmcB43EXimrT0exEKfxSd3xc7RBS6EPx1XZwYc.
Are you sure you want to continue connecting (yes/no)?

我了解到指纹是 public 密钥的 SHA256 哈希值的 Base64 字符串。

我知道如何用 RSAPublicKey:

生成这个指纹
    RSAPublicKey publicKey = ...;

    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    DataOutputStream dataOutputStream = new DataOutputStream(byteArrayOutputStream);

    dataOutputStream.writeInt("ssh-rsa".getBytes().length);
    dataOutputStream.write("ssh-rsa".getBytes());
    dataOutputStream.writeInt(publicKey.getPublicExponent().toByteArray().length);
    dataOutputStream.write(publicKey.getPublicExponent().toByteArray());
    dataOutputStream.writeInt(publicKey.getModulus().toByteArray().length);
    dataOutputStream.write(publicKey.getModulus().toByteArray());

    MessageDigest digest = MessageDigest.getInstance("SHA256");
    byte[] result = digest.digest(byteArrayOutputStream.toByteArray());

    String fingerprint = Base64.getEncoder().encodeToString(result);

但是我怎样才能用 BCECPublicKey 做到这一点?

更新
我发现 BCECPublicKeyRSAPublicKey 完全不同。我从来不知道 SSH 服务器 public 密钥是 ECDSA 而客户端 public 密钥是 RSA。

字节的结构方式也大不相同。 RSA public 密钥以 header(ssh-rsa) 开头。 header 长度可以从前 4 个字节(readInt())读取。但是,当我使用 ECDSA 执行此操作时,长度太长以表示 header...

补充回答
要复制粘贴的内容:

    BCECPublicKey publicKey = ...;

    byte[] point = SubjectPublicKeyInfo.getInstance(ASN1Sequence.getInstance(publicKey.getEncoded())).getPublicKeyData().getOctets();

    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    DataOutputStream dataOutputStream = new DataOutputStream(byteArrayOutputStream);

    dataOutputStream.writeInt("ecdsa-sha2-nistp256".getBytes().length);
    dataOutputStream.write("ecdsa-sha2-nistp256".getBytes());
    dataOutputStream.writeInt("nistp256".getBytes().length);
    dataOutputStream.write("nistp256".getBytes());
    dataOutputStream.writeInt(point.length);
    dataOutputStream.write(point);

    MessageDigest digest = MessageDigest.getInstance("SHA256");
    byte[] result = digest.digest(byteArrayOutputStream.toByteArray());

    String fingerprint = Base64.getEncoder().encodeToString(result);

OpenSSH public密钥格式(以及它所基于的 SSH 有线格式)确实以类型开头,但对于 ECDSA,类型包括曲线ID。例如,我的一个测试系统有一个 ecdsa/p256 键,如下所示:

$ awk '{print }' <id_ecdsa.pub |openssl base64 -d -A |xxd
0000000: 0000 0013 6563 6473 612d 7368 6132 2d6e  ....ecdsa-sha2-n
0000010: 6973 7470 3235 3600 0000 086e 6973 7470  istp256....nistp
0000020: 3235 3600 0000 4104 8141 9c28 53e7 532e  256...A..A.(S.S.
0000030: 8c4b 9781 c6a5 1820 f41a bc95 4e62 13a9  .K..... ....Nb..
0000040: 8356 a517 be55 6ebc fbf4 de74 e216 8f17  .V...Un....t....
0000050: 6222 011c 5920 a3fc caed c880 4298 46d5  b"..Y ......B.F.
0000060: dd39 396e d72d 1e40                      .99n.-.@

包括:
4 字节 00000013 bigendian int = 19: 类型的长度
19 字节 'ecdsa-sha2-nistp256' 类型
4 字节 00000008 bigendian int = 8: curveid 的长度
8 字节 'nistp256' curveid(冗余,但这是有线格式)
4 字节 00000041 bigendian int = 65: pub 点的长度
65 字节开始 04:X9.62 格式的发布点,在 SEC1 中复制更方便,这是 1 字节 04=未压缩,N 字节 X 坐标,N 字节 Y 坐标,其中 N 是表示所需的(固定)大小曲线的基础字段为无符号。

这些主要在 rfc5656 section 3.1 和 6.1 中的 curveid 中定义。 RFC 允许压缩点格式,但 OpenSSH 不使用该选项。

BCECPublicKey.getEncoded()(像所有 Java PublicKey classes)returns 所谓的 X.509(实际上是 SubjectPublicKeyInfo,SPKI)编码,用于 EC 包括 X9.62 未压缩格式的public 点,但您需要一些解析来提取它。既然你有 BC,那么使用它最简单:

byte[] point = SubjectPublicKeyInfo.getInstance(ASN1Sequence.getInstance(encoded)).getPublicKeyData().getOctets();

或者 .getW().getQ() 将点作为 JCE 或 BC class,您可以从其中任何一个获得(仿射)X 和 Y 坐标 BigInteger 分别。 ECFieldElement 依次产生 BigInteger,并且每个 BigInteger 都可以转换为 可变大小 字节数组,然后您必须向左填充或左截断到正确的大小。

结果就是要散列的数据。如果您不知道,只有 OpenSSH 6.8 版以上使用 base64(sha256(pubkey)) 作为指纹(默认情况下)。在此之前它是带冒号的十六进制(md5(pubkey)),并且较新的版本可以使用旧指纹以实现兼容性(请参阅 ssh_config 中的选项 FingerprintHash for ssh 和标志 -Essh-keygen).

需要说明的是,这只是 OpenSSH 指纹。密钥指纹也用于 PGP 和 X.509/PKIX(SSL/TLS、CMS/SMIME 等)领域,它们完全不同。