1. 概述

数字证书在建立受信任的安全在线通信中至关重要。我们通常使用它们确保客户端与Web服务器之间交换的数据保持安全。

本教程将探讨如何使用Java判断给定证书是自签名还是由受信任的证书颁发机构(CA)签名的。

然而,由于证书和安全概念的多样性,没有一刀切的解决方案。我们通常需要根据特定场景和需求选择最佳方案。

2. 自签名证书 vs CA签名证书

首先,我们来看自签名证书和CA签名证书的区别。

简单来说,自签名证书由同一实体生成和签名。 虽然它提供加密功能,但无法通过独立机构验证信任。换句话说,它不涉及任何第三方证书颁发机构(CA)。

因此,当用户的浏览器遇到自签名证书时,可能会发出安全警告,因为无法独立验证证书的真实性。

我们通常在私有网络和测试环境中使用它们。

另一方面,CA签名证书由受信任的证书颁发机构签名。 大多数Web浏览器和操作系统都认可并接受这些CA。

此外,CA签名证书证明持有证书的实体是该域名的合法所有者,帮助用户确信他们正在与真实服务器通信,而不是中间人。

现在,让我们看看如何使用Java判断证书是自签名还是CA签名的。

3. 检查证书是否为自签名

开始前,先生成示例中使用的证书。生成自签名证书最简单的方式是使用keytool工具:

keytool -genkey -keyalg RSA -alias selfsigned -keystore keystore.jks -validity 365 -keysize 2048

这里,我们创建了一个别名为selfsigned的自签名证书,并将其存储在keystore.jks密钥库中。

3.1. 比较颁发者和主体值

如前所述,自签名证书由同一实体生成和签名。

证书的颁发者部分代表证书的签名者。自签名证书的主体(颁发给)和颁发者(颁发者)值相同。 换句话说,要判断是否为自签名证书,我们比较其主体和颁发者信息。

此外,Java API提供了java.security.cert.X509Certificate类来处理证书。使用这个类,我们可以与X.509证书交互并执行各种检查和验证。

让我们检查主体和颁发者是否匹配。通过从X509Certificate对象提取相关字段并检查是否匹配:

@Test
void whenCertificateIsSelfSigned_thenSubjectIsEqualToIssuer() throws Exception {
    X509Certificate certificate = (X509Certificate) keyStore.getCertificate("selfsigned");
    assertEquals(certificate.getSubjectDN(), certificate.getIssuerDN());
}

3.2. 验证签名

另一种检查自签名证书的方法是使用其自己的公钥进行验证。

让我们使用*verify()*方法检查自签名证书:

@Test
void whenCertificateIsSelfSigned_thenItCanBeVerifiedWithItsOwnPublicKey() throws Exception {
    X509Certificate certificate = (X509Certificate) keyStore.getCertificate("selfsigned");
    assertDoesNotThrow(() -> certificate.verify(certificate.getPublicKey()));
}

但如果传入CA签名证书,该方法会抛出异常。

4. 检查证书是否为CA签名

要成为CA签名证书,必须属于一个可追溯到受信任根CA的信任链。 简单来说,证书链包含从根证书到用户证书的证书列表。链中的每个证书都签名下一个证书。

谈到信任链,有不同的证书类型:

  • 根证书
  • 中间证书
  • 终端实体证书

此外,我们使用层次结构中的根证书和中间证书来颁发和验证终端实体证书。

本教程中,我们将使用从Baeldung网站获取的证书

Baeldung证书

如果终端实体证书是证书链的一部分,检查CA签名证书的复杂性会增加。这种情况下,我们可能需要检查整个链以确定是否为CA签名证书。证书的颁发者可能不是直接根CA,而是由根CA签名的中间CA。

现在,如果我们检查证书层次结构,可以看到证书属于证书链:

Baeldung证书层次结构

  • Baltimore CyberTrust Root – 根CA
  • Cloudflare Inc ECC CA-3 – 中间CA
  • sni.cloudflaressl.com – 终端实体(在Baeldung网站使用)

4.1. 使用信任库

我们可以创建自己的信任库来检查终端实体证书是否由我们信任的证书之一签名。

设置信任库时,通常包含根CA证书以及构建信任链所需的任何中间CA证书。这样,我们的应用程序可以有效验证其他方提供的证书。

使用信任库的一个优点是可以决定信任哪些CA证书。

在我们的示例中,Baltimore CyberTrust Root证书签发了中间Cloudflare证书,后者又签发了我们的终端实体证书。

现在,让我们将两者都添加到信任库:

keytool -importcert -file cloudflare.cer -keystore truststore.jks -alias cloudflare
keytool -importcert -file root.cer -keystore truststore.jks -alias root

接下来,要检查是否信任给定的终端实体证书,需要找到根证书。让我们创建*getRootCertificate()*方法来搜索根证书:

public static X509Certificate getRootCertificate(X509Certificate endEntityCertificate, KeyStore trustStore)
        throws Exception {
    X509Certificate issuerCertificate = findIssuerCertificate(endEntityCertificate, trustStore);
    if (issuerCertificate != null) {
        if (isRoot(issuerCertificate)) {
            return issuerCertificate;
        } else {
            return getRootCertificate(issuerCertificate, trustStore);
        }
    }
    return null;
}

private static boolean isRoot(X509Certificate certificate) {
    try {
        certificate.verify(certificate.getPublicKey());
        return certificate.getKeyUsage() != null && certificate.getKeyUsage()[5];
    } catch (Exception e) {
        return false;
    }
}

首先,我们尝试在信任库中定位提供证书的颁发者:

static X509Certificate findIssuerCertificate(X509Certificate certificate, KeyStore trustStore)
        throws KeyStoreException {
    Enumeration<String> aliases = trustStore.aliases();
    while (aliases.hasMoreElements()) {
        String alias = aliases.nextElement();
        Certificate cert = trustStore.getCertificate(alias);
        if (cert instanceof X509Certificate) {
            X509Certificate x509Cert = (X509Certificate) cert;
            if (x509Cert.getSubjectX500Principal().equals(certificate.getIssuerX500Principal())) {
                return x509Cert;
            }
        }
    }
    return null;
}

然后,如果找到匹配项,我们验证该证书是否为自签名CA证书。验证成功则到达根证书,否则继续搜索。

最后,测试我们的方法是否正常工作:

@Test
void whenCertificateIsCASigned_thenRootCanBeFoundInTruststore() throws Exception {
    X509Certificate endEntityCertificate = (X509Certificate) keyStore.getCertificate("baeldung");
    X509Certificate rootCertificate = getRootCertificate(endEntityCertificate, trustStore);
    assertNotNull(rootCertificate);
}

如果使用自签名证书执行相同测试,由于信任库不包含它,我们将找不到根证书。

5. 检查证书是否为CA证书

当只处理根证书或中间证书时,可能需要执行额外检查。

注意:根证书也是自签名证书 但根证书与用户自签名证书的区别在于前者启用了keyCertSign标志(因为它可用于签名其他证书)。

我们可以通过检查密钥用法来识别根或中间证书:

@Test
void whenCertificateIsCA_thenItCanBeUsedToSignOtherCertificates() throws Exception {
    X509Certificate certificate = (X509Certificate) keyStore.getCertificate("cloudflare");
    assertTrue(certificate.getKeyUsage()[5]);
}

此外,可以检查基本约束扩展。

基本约束扩展是X.509证书中的一个字段,提供证书预期用途的信息,并指示它代表证书颁发机构(CA)还是终端实体。

如果基本约束扩展不存在,*getBasicConstraints()*方法返回-1:

@Test
void whenCertificateIsCA_thenBasicConstrainsReturnsZeroOrGreaterThanZero() throws Exception {
    X509Certificate certificate = (X509Certificate) keyStore.getCertificate("cloudflare");
    assertNotEquals(-1, certificate.getBasicConstraints());
}

6. 总结

本文中,我们学习了如何检查证书是自签名还是CA签名的。

总结来说,自签名证书具有相同的主体和颁发者组件,并且可以使用其自己的公钥进行验证。

另一方面,CA签名证书通常属于证书链。要验证它们,需要创建包含受信任根证书和中间证书的信任库,并检查终端实体证书的根是否匹配其中一个受信任证书。

最后,如果处理根证书或中间证书,可以通过检查它们是否用于签名其他证书来识别。

完整代码示例可在GitHub上找到。


原始标题:Check if Certificate Is Self-Signed or CA-Signed With Java