如何在Java中检索/计算X509证书的Thumbprint?

47
我有一个Java客户端正在调用一个Web服务操作,该操作需要一个证书“指纹”作为参数。我相信这个指纹是一种SHA1哈希,以十六进制字符串格式表示证书的公钥,但我不确定。
.NET框架似乎包含了一种简单的方法来获取这个值(X509Certificate2.Thumbprint属性)。在Windows中查看一个.cer文件的属性也会显示指纹,它看起来像:
a6 9c fd b0 58 0d a4 ee ae 9a 47 75 24 c3 0b 9f 5d b6 1c 77

所以我的问题是:如果我有一个java.security.cert.X509Certificate的实例,有谁知道如何在Java中检索或计算此指纹字符串?


这个Java演示程序(URLConnection)连接到https url并打印/计算各种指纹,包括pin-sha256、SKI和Thumbprints:https://github.com/ecki/JavaCryptoTest/blob/master/src/main/java/net/eckenfels/test/ssl/UrlInspect.java - eckes
7个回答

85

证书的DER编码的SHA-1哈希值是.NET使用X509Certificate2.Thumbprint获取的内容。

如MSDN的备注所述:

指纹是使用SHA1算法动态生成的,不存在于证书中。由于指纹是证书的唯一值,因此通常用于在证书存储中查找特定证书。

Java标准库不直接提供指纹,但您可以按照以下方式获取:

DatatypeConverter.printHexBinary(
        MessageDigest.getInstance("SHA-1").digest(
                cert.getEncoded())).toLowerCase();

这里有一个完整的使用方便获取的PEM文件的示例:

  1. Create stackoverflow.crt.pem:

    -----BEGIN CERTIFICATE-----
    MIIHHjCCBgagAwIBAgIQDhG71w1UtxDQxvVAtrUspDANBgkqhkiG9w0BAQsFADBw
    MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
    d3cuZGlnaWNlcnQuY29tMS8wLQYDVQQDEyZEaWdpQ2VydCBTSEEyIEhpZ2ggQXNz
    dXJhbmNlIFNlcnZlciBDQTAeFw0xNjA1MjEwMDAwMDBaFw0xOTA4MTQxMjAwMDBa
    MGoxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJOWTERMA8GA1UEBxMITmV3IFlvcmsx
    HTAbBgNVBAoTFFN0YWNrIEV4Y2hhbmdlLCBJbmMuMRwwGgYDVQQDDBMqLnN0YWNr
    ZXhjaGFuZ2UuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr0YD
    zscT5i6T2FaRsTGNCiLB8OtPXu8N9iAyuaROh/nS0kRRsN8wUMk1TmgZhPuYM6oF
    S377V8W2LqhLBMrPXi7lnhvKt2DFWCyw38RrDbEsM5dzVGErmhux3F0QqcTI92zj
    VW61DmE7NSQLiR4yonVpTpdAaO4jSPJxn8d+4p1sIlU2JGSk8LZSWFqaROc7KtXt
    lWP4HahNRZtdwvL5dIEGGNWx+7B+XVAfY1ygc/UisldkA+a3D2+3WAtXgFZRZZ/1
    CWFjKWJNMAI6ZBAtlbgSNgRYxdcdleIhPLCzkzWysfltfiBmsmgz6VCoFR4KgJo8
    Gd3MeTWojBthM10SLwIDAQABo4IDuDCCA7QwHwYDVR0jBBgwFoAUUWj/kK8CB3U8
    zNllZGKiErhZcjswHQYDVR0OBBYEFFrBQmPCYhOznZSEqjIeF8tto4Z7MIIB6AYD
    VR0RBIIB3zCCAduCEyouc3RhY2tleGNoYW5nZS5jb22CEXN0YWNrb3ZlcmZsb3cu
    Y29tghMqLnN0YWNrb3ZlcmZsb3cuY29tgg1zdGFja2F1dGguY29tggtzc3RhdGlj
    Lm5ldIINKi5zc3RhdGljLm5ldIIPc2VydmVyZmF1bHQuY29tghEqLnNlcnZlcmZh
    dWx0LmNvbYINc3VwZXJ1c2VyLmNvbYIPKi5zdXBlcnVzZXIuY29tgg1zdGFja2Fw
    cHMuY29tghRvcGVuaWQuc3RhY2thdXRoLmNvbYIRc3RhY2tleGNoYW5nZS5jb22C
    GCoubWV0YS5zdGFja2V4Y2hhbmdlLmNvbYIWbWV0YS5zdGFja2V4Y2hhbmdlLmNv
    bYIQbWF0aG92ZXJmbG93Lm5ldIISKi5tYXRob3ZlcmZsb3cubmV0gg1hc2t1YnVu
    dHUuY29tgg8qLmFza3VidW50dS5jb22CEXN0YWNrc25pcHBldHMubmV0ghIqLmJs
    b2dvdmVyZmxvdy5jb22CEGJsb2dvdmVyZmxvdy5jb22CGCoubWV0YS5zdGFja292
    ZXJmbG93LmNvbYIVKi5zdGFja292ZXJmbG93LmVtYWlsghNzdGFja292ZXJmbG93
    LmVtYWlsMA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYB
    BQUHAwIwdQYDVR0fBG4wbDA0oDKgMIYuaHR0cDovL2NybDMuZGlnaWNlcnQuY29t
    L3NoYTItaGEtc2VydmVyLWc1LmNybDA0oDKgMIYuaHR0cDovL2NybDQuZGlnaWNl
    cnQuY29tL3NoYTItaGEtc2VydmVyLWc1LmNybDBMBgNVHSAERTBDMDcGCWCGSAGG
    /WwBATAqMCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5jb20vQ1BT
    MAgGBmeBDAECAjCBgwYIKwYBBQUHAQEEdzB1MCQGCCsGAQUFBzABhhhodHRwOi8v
    b2NzcC5kaWdpY2VydC5jb20wTQYIKwYBBQUHMAKGQWh0dHA6Ly9jYWNlcnRzLmRp
    Z2ljZXJ0LmNvbS9EaWdpQ2VydFNIQTJIaWdoQXNzdXJhbmNlU2VydmVyQ0EuY3J0
    MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggEBAAzJAMGSdKoX1frdqNlN
    iXu8Gcbsm/DxWMXpcTXlZn8s+/qQQoc+/3o0CK3C8/j9n5DmsYa88P6Ntt5ysDs+
    b0ynXFva4CAEyKaoPM4SIpOjwfWBRSUOqAIkQO2/LhKBwT/EnpaIHIKGnI0UdXLQ
    oDfkMDg6mgJsEBsKdKF5EfEX7iU3NO5xVJPJE8/R0btLAdYwxB9S6fSpCXGe2HqQ
    D101O/7/4MWNdFSbfdDSFcn5oEm+idimrqiNrF5knmuJy4qPBkL7thNuGK6rvYCF
    ZJM03ZEZhkQmn2jG/7LgjfwZmvfcITeADCpylf88bL+lf+vxe6cCl9CyqWgBDpsI
    xpE=
    -----END CERTIFICATE-----
    
  2. Create X509.java:

    import javax.xml.bind.DatatypeConverter;
    import java.io.FileInputStream;
    import java.io.FileNotFoundException;
    import java.security.MessageDigest;
    import java.security.NoSuchAlgorithmException;
    import java.security.cert.CertificateEncodingException;
    import java.security.cert.CertificateException;
    import java.security.cert.CertificateFactory;
    import java.security.cert.X509Certificate;
    
    public final class X509 {
        public static void main(String[] args)
                throws FileNotFoundException, CertificateException, NoSuchAlgorithmException {
            FileInputStream is = new FileInputStream(args[0]);
            CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
            X509Certificate cert = (X509Certificate) certificateFactory.generateCertificate(is);
            String thumbprint = getThumbprint(cert);
            System.out.println(thumbprint);
        }
    
        private static String getThumbprint(X509Certificate cert)
                throws NoSuchAlgorithmException, CertificateEncodingException {
            MessageDigest md = MessageDigest.getInstance("SHA-1");
            byte[] der = cert.getEncoded();
            md.update(der);
            byte[] digest = md.digest();
            String digestHex = DatatypeConverter.printHexBinary(digest);
            return digestHex.toLowerCase();
        }
    }
    
  3. Compile the program with Java 8:

    javac X509.java
    

    Or Java 9 - due to modular JDK/JPMS - DataTypeConverter is not in java.base, but java.xml.bind, so you need to explicitly depend on it during your build:

    javac --add-modules java.xml.bind X509.java
    

    Otherwise, on Java 9, you get this when you try to build it:

    X509.java:3: error: package javax.xml.bind is not visible
            import javax.xml.bind.DatatypeConverter;
            ^
            (package javax.xml.bind is declared in module java.xml.bind, which is not in the module graph)
            1 error
    
  4. Run it with Java 8:

    java X509 stackoverflow.crt.pem
    

    In Java 9 - due to modular JDK/JPMS - DataTypeConverter is not in java.base, but java.xml.bind, so you need to explicitly depend on it when running your program:

    java --add-modules java.xml.bind X509 stackoverflow.crt.pem
    

    Otherwise, on Java 9, you get this when you try to run it:

    Exception in thread "main" java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter
        at X509.getThumbPrint(X509.java:29)
        at X509.main(X509.java:19)
        Caused by: java.lang.ClassNotFoundException: javax.xml.bind.DatatypeConverter
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:582)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:185)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:496)
        ... 2 more
    
  5. Get the expected output:

    47adb03649a2eb18f63ffa29790818349a99cab7
    

6
谢谢您的回答!我在Google上搜索发现,拇指印通常用作证书的唯一标识符,因此它似乎不是特定于.NET的。我调用的Web服务正在使用它来在其存储中查找证书。 - Matt Z
Web服务必须是.NET服务器。我没有看到其他服务器使用指纹来存储客户端证书。.NET提供了许多与安全相关的扩展,您可能会遇到其他问题。除非您的客户端需要跨平台,否则在.NET中编写客户端将更容易。 - ZZ Coder
10
指纹不仅限于.Net。尝试使用SSH连接到之前未连接过的服务器吗?你将看到它的指纹。证书存储也会列出指纹。 - Henrik
10
"为什么你需要用Java呢?" - 他可能正在将证书固定在那里。这比信任数百个CA、下属CA和DNS更安全。 - jww
自从Java 17版本开始,您可以使用java.util.HexFormat来替代DataTypeConverter(这还允许自定义十六进制字符串的格式)。 - undefined

52

使用Apache Commons Codec,您可以执行以下操作:

DigestUtils.sha1Hex(cert.getEncoded())

6

使用Google的Guava编写的一行代码

Hashing.sha256().hashBytes(cert.getEncoded()).toString();

1
为了完整性 - 通常使用SHA-1(Hashing.sha1())作为指纹(如其他答案中所示)。 - Andreas Haufler
在Guava中,sha1已被弃用。 - Nagaraj Alagusundaram

2
您可以使用openssl命令生成缩略指纹,例如如果您有证书的pem格式文件(file.txt)
那么:
cat file.txt | openssl x509 -sha1 -fingerprint - 这将生成相同的缩略指纹

5
那对于Java有什么帮助? - Hiro2k

0

短例子,不使用任何库。

object MessageDigestUtil {

    private val hexCode = "0123456789ABCDEF".toCharArray()

    fun sha1(input: ByteArray): ByteArray {
        return digest(input,"SHA-1")
    }

    @Throws(NoSuchAlgorithmException::class)
    fun getFingerprint(publicKey: PublicKey): String? {
        return printHexBinary(sha1(publicKey.encoded))
    }

    private fun printHexBinary(data: ByteArray): String {
        val r = StringBuilder(data.size * 2)
        for (b in data) {
            r.append(hexCode[b.toInt() shr 4 and 0xF])
            r.append(hexCode[(b and 0xF).toInt()])
        }
        return r.toString()
    }

    @Throws(NoSuchAlgorithmException::class)
    private fun digest(input: ByteArray, algorithm: String): ByteArray {
        val digest = MessageDigest.getInstance(algorithm)
        digest.update(input)
        return digest.digest()
    }
}

0
如果你在解决如何为MSGraph API生成正确的base64编码的指纹时遇到了困难,那么你来对地方了。根据这份文档,你可以生成正确的x5t头参数。
import java.io.FileInputStream;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.security.cert.CertificateFactory;
import java.util.Base64;

X509Certificate cert = getPublicKey();

String thumbprint = getThumbprint(cert); // base64 encoded thumbprint
    
X509Certificate getPublicKey() {
    try (FileInputStream inStream = new FileInputStream("{your .crt or .cer file here}")) {

        CertificateFactory cf = CertificateFactory.getInstance("X.509");
        X509Certificate cert = (X509Certificate) cf.generateCertificate(inStream);

        return cert;
    } catch (Exception e) {
        System.out.println("no file!");
        return null;
    }
}

String getThumbprint(X509Certificate cert) {
    try {
        return Base64.getEncoder().encodeToString(getHash(cert));
    } catch (Exception e) {
        return null;
    }
}

static byte[] getHash(X509Certificate cert) throws NoSuchAlgorithmException, CertificateEncodingException {
    final MessageDigest md = MessageDigest.getInstance("SHA-1");
    md.update(cert.getEncoded());
    return md.digest();
}

-10

这里有一个更简单的方法:

using System.Security.Cryptography.X509Certificates;    

X509Certificate2 xcert = new X509Certificate2("C:\some_cert.cerpub");
string certSubject = xcert.Subject;
string certThumbprint =  xcert.Thumbprint;   

3
不是Java :) 我知道在.NET中很容易入门。 - Matt Z

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接