证书链简介

说明:除非特别指明,本文所述之证书,特指X.509 V3证书。

一. 什么是证书链?

当客户端发起https请求时,web服务端会返回https证书,客户端将对证书进行验证,验证通过之后客户端和服务端之间才能继续进行通信。

严格来说,web服务端所返回的https证书不是一个单一的证书,而是一组有序的证书,称为证书链。证书链由终端证书开始,然后是签署颁发该终端证书的中间CA证书,再然后是签署颁发前一个中间CA证书的另一个中间CA证书…中间CA证书的数量可以是0个到多个,一直链接下去,在证书链的末端,是根证书。根证书是一个自签名证书,可以在证书链中省略,即服务端可以选择在证书链中包含或者不包含根证书。

二. 如何判定一个证书是否为另外一个证书的签发者?

如何判定一个证书是否为另一个证书的签发者呢?在代码层面,可以在当前证书上执行verify方法验证是否已使用与指定的签发者的公钥相对应的私钥签署了此证书:

public abstract void verify(PublicKey key)

其原理为,当前证书由签发者的私钥执行加密得到数字签名,因此当前证书的数字签名只能通过签发者的公钥解密。

在实际构建证书链时,没有必要执行verify方法,因为执行verify方法的代价稍高。我们可以通过另外一种简单,易于执行的方式,依靠证书的主体标识符扩展属性subjectKeyIdentifier和发布者标识符扩展属性authorityKeyIdentifier来构建证书链,这非常便捷,也是一种人肉构建证书链的方法。当然,当证书链构建完成后,还是得执行verify方法来严格验证两个证书之间是否为签发关系。

可以查看当前证书和签发者证书的以下属性:
subjectName: 主体名称
subjectKeyIdentifier: 主体标识符,为扩展属性
issuerName: 发布者名称
authorityKeyIdentifier: 发布者标识符,为扩展属性

当前证书和签发者证书之间满足以下条件:
当前证书的发布者名称 = 签发者的主体名称
当前证书的发布者标识符 = 签发者的主体标识符

这里也许会产生疑问,可不可以更加简单地直接使用证书的主体名称和发布者名称来构建证书链呢?答案是否定的,因为证书的主体名称和发布者名称不受约束,同样的证书名称会被不同的证书使用;而按照规范,主体标识符subjectKeyIdentifier和发布者标识符authorityKeyIdentifier派生自证书的公钥,是根据公钥生成的唯一值,可以准确标识证书的公钥。

三. 证书链的验证

非对称加密技术解决了通信的加密问题,而证书链则用于检查终端证书里的公钥及其它数据是否属于其主题,是否可信任,解决了信任问题,证书链的验证同样依赖于非对称加密技术。

客户端对证书链的验证,从终端证书开始,然后是一系列中间CA证书,最后是根证书。检查是这么做的,用证书链中的下一个证书的公钥来验证当前证书的签名,一直检查到证书链的尾端,如果所有验证都成功通过,那个这个证书就是可信的。在验证根证书时,客户端会根据证书链中的最后一个非根证书在本地的可信任证书存储区域搜寻匹配的根证书,只有本地可信任证书存储区域中包含的根证书,才能通过验证。

从技术上来讲,要完成证书链的认证,服务端返回的证书链必须是有序的,但不必是完整的。证书链除了可以不包含根证书之外,甚至也可以自后向前地省略中间CA证书,直至只包含终端证书。

一方面,证书可以直接在证书的扩展信息中指定签署颁发本证书的下一级证书的获取链接,客户端可以直接根据该链接下载下一级证书,从而由终端证书开始,依次下载一系列的中间证书,最终构建出完整的证书链。

另一方面,客户端本地的可信任证书存储区域除了可以存放根证书以外,也可以存储中间CA证书,客户端可以直接在本地搜寻与终端证书匹配的中间CA证书,以及与该中间CA证书匹配的下一级中间CA证书,直至根证书,最终构建出完整的证书链。

需要指出的是,证书在扩展信息中指定签署颁发本证书的下一级证书的获取链接是一个可选项;客户端本地的可信任证书存储区域会存放所有可信任的根证书,但通常不会存放太多中间CA证书。因此,要确保证书的有效性,构建出完整的证书链,服务端返回的证书链得遵守一定的规范,即除了包含必不可少的终端证书之外,还得包括必要的中间CA证书序列,至于根证书,绝大多数情况下都可以省略,因为在标准的证书链验证过程中,根证书由客户端在本地获取。

四. 证书链是否具有唯一性?

很容易产生一种误解:证书链是唯一的!即终端证书由确定的唯一指定的中间CA证书签署颁发,中间CA证书由另一个确定的唯一指定的中间CA证书签署颁发…最后一个中间CA证书由确定的唯一的根证书签署颁发。

但实际上,证书链不是唯一的,一个终端证书可以通过多个不同的证书链完成验证。这里可以举例两种情况。

其一,当一个证书链中,某个中间CA证书即将过期时,该中间CA证书的颁发者将颁发一个新的中间CA证书取代之,新旧两个中间CA证书使用相同的主体名称和公钥。那么,在这个中间CA证书过期之前,就会形成两条证书链:

终端证书 > 即将过期的中间CA证书 > 根证书
终端证书 > 新颁发的中间CA证书 > 根证书

显然,这两条证书链都是有效的。

其二,在不同 CA 之间交叉认证的情况下,同一个终端证书也会形成多个证书链,这些证书链甚至可能指向不同的根证书。

例如,机构B是一个新的证书颁发机构,其持有根证书B,其签发了一个证书链:

终端证书 > 中间CA证书1 > 根证书B

这里面临的挑战是:由于机构B是新成立的,他的根证书B并未广泛预装在客户端中,因此,该证书链无法在所有的客户端都通过验证。

解决办法是,机构B与另一个更权威的证书颁发机构A合作,机构A持有根证书A,由根证书A签发中间CA证书2,中间CA证书2与根证书B使用相同的主体名称和公钥,这样就形成了两个证书链:

终端证书 > 中间CA证书1 > 中间CA证书2 > 根证书A
终端证书 > 中间CA证书1 > 根证书B

如此一来,终端证书既可以通过根证书B进行验证,也可以通过根证书A进行验证,从而实现更好的兼容性。

五. 服务端返回的证书链一定会被客户端采用吗?

从前文可知,证书链不具有唯一性,但服务端返回的证书链是唯一的,那么,服务端返回的证书链一定会被客户端采纳吗?答案自然是否定的。

通常,客户端会尝试验证从服务端收到的证书链。但客户端的验证行为取决于实现:一些客户端(大多数浏览器)从证书链中取得终端证书后,可能会优先尝试使用本地可信任证书存储区域中已知的中间CA证书和根证书来重新构建另一个链,必要时甚至依靠证书在扩展信息中指定的签署颁发本证书的下一级证书的获取链接来下载证书。

以下为Nginx服务配置的一个https站点:https://dancen.com。
Nginx为该站点配置了证书:/etc/letsencrypt/live/dancen.com/fullchain.pem

server {
        listen 80;
        listen 443 ssl;
        server_name dancen.com;
        ssl_certificate /etc/letsencrypt/live/dancen.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/dancen.com/privkey.pem;
}

以下通过一段Java程序读取证书fullchain.pem的内容:

File file = new File("/etc/letsencrypt/live/dancen.com/fullchain.pem");
InputStream inputStream = new FileInputStream(file);
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
Collection<? extends Certificate> certs = certFactory.generateCertificates(inputStream);

for(Certificate cert : certs)
{
	X509Certificate x509Cert = (X509Certificate)cert;
	System.out.println(x509Cert.getSubjectX500Principal().getName());
	System.out.println("\t< " + x509Cert.getIssuerX500Principal().getName());
}

inputStream.close();

输出如下,可以看到,服务端配置的证书文件fullchain.pem中包含了一个由3个证书构成的证书链。

CN=dancen.com
	< CN=R3,O=Let's Encrypt,C=US
CN=R3,O=Let's Encrypt,C=US
	< CN=ISRG Root X1,O=Internet Security Research Group,C=US
CN=ISRG Root X1,O=Internet Security Research Group,C=US
	< CN=DST Root CA X3,O=Digital Signature Trust Co.

下面通过Java直接访问该https站点,获取站点的证书链。

connection = MyHttpsUtil.getMyHttpsURLConnection(“https://dancen.com”);
connection.connect();
Certificate[] certs = connection.getServerCertificates();

for(Certificate cert : certs)
{
	X509Certificate x509Cert = (X509Certificate)cert;
	System.out.println(x509Cert.getSubjectX500Principal().getName());
	System.out.println("\t< " + x509Cert.getIssuerX500Principal().getName());
}

connection.disconnect();

通过输出可以看到,Java访问该https站点时得到的证书链就是在Nginx上配置的证书链,该证书链中不包括根证书,我们可以通过以下代码,对最后一个中间CA证书执行getIssuerCert方法补齐根证书。

public class CertChainCompleter
{
	private static final X509TrustManager X509_TRUST_MANAGER;
	
	static
	{
		TrustManagerFactory trustManagerFactory = null;
		X509TrustManager x509TrustManager = null;
		
		try
		{
			trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
			KeyStore keyStore = null;
			trustManagerFactory.init(keyStore);
		}
		catch(Exception e)
		{
			e.printStackTrace();
		}
		
		if(null != trustManagerFactory)
		{
			TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
			
			if(null != trustManagers)
			{
				for(TrustManager trustManager : trustManagers)
				{
					if(null != trustManager && trustManager instanceof X509TrustManager)
					{
						x509TrustManager = (X509TrustManager)trustManager;
						
						break;
					}
				}
			}
		}
		
		X509_TRUST_MANAGER = x509TrustManager;
	}
	
	public static X509Certificate getIssuerCert(X509Certificate x509Certificate)
	{
		X509Certificate rs = null;
		
		if(null != X509_TRUST_MANAGER && null != x509Certificate)
		{
			X509Certificate[] acceptedIssuers = X509_TRUST_MANAGER.getAcceptedIssuers();
			
			if(null != acceptedIssuers)
			{
				for(X509Certificate acceptedIssuer : acceptedIssuers)
				{
					if(null != acceptedIssuer)
					{
						try
						{
							x509Certificate.verify(acceptedIssuer.getPublicKey());
							rs = acceptedIssuer;
							
							break;
						}
						catch(Exception e)
						{
							//TODO nothing
						}
					}
				}
			}
		}
		
		return rs;
	}
}

下面是补齐根证书之后的证书链信息:

[Leaf]:
version: 3
serialNumber: 03fa41664db4ca93cc2d465c616dce0edc65
sigAlgName: SHA256withRSA
subjectName: CN=dancen.com
subjectKeyIdentifier: b93e93d1d78d291928631e4024895f2b57282872
issuerName: CN=R3,O=Let's Encrypt,C=US
authorityKeyIdentifier: [142eb317b75856cbae500940e61faf9d8b14c2c6]
issuerCertUrl: [http://r3.i.lencr.org/]
notBefore: 2021/08/30_10:18:42
notAfter: 2021/11/28_10:18:41
subjectAlternativeName: [dancen.com,www.dancen.com]
revokeAccess:
	ocsp: [http://r3.o.lencr.org]
	crl: []

[Intermediate_1/2]:
version: 3
serialNumber: 912b084acf0c18a753f6d62e25a75f5a
sigAlgName: SHA256withRSA
subjectName: CN=R3,O=Let's Encrypt,C=US
subjectKeyIdentifier: 142eb317b75856cbae500940e61faf9d8b14c2c6
issuerName: CN=ISRG Root X1,O=Internet Security Research Group,C=US
authorityKeyIdentifier: [79b459e67bb6e5e40173800888c81a58f6e99b6e]
issuerCertUrl: [http://x1.i.lencr.org/]
notBefore: 2020/09/04_08:00:00
notAfter: 2025/09/16_00:00:00
subjectAlternativeName: []
revokeAccess:
	ocsp: []
	crl: [http://x1.c.lencr.org/]

[Intermediate_2/2]:
version: 3
serialNumber: 4001772137d4e942b8ee76aa3c640ab7
sigAlgName: SHA256withRSA
subjectName: CN=ISRG Root X1,O=Internet Security Research Group,C=US
subjectKeyIdentifier: 79b459e67bb6e5e40173800888c81a58f6e99b6e
issuerName: CN=DST Root CA X3,O=Digital Signature Trust Co.
authorityKeyIdentifier: [c4a7b1a47b2c71fadbe14b9075ffc41560858910]
issuerCertUrl: [http://apps.identrust.com/roots/dstrootcax3.p7c]
notBefore: 2021/01/21_03:14:03
notAfter: 2024/10/01_02:14:03
subjectAlternativeName: []
revokeAccess:
	ocsp: []
	crl: [http://crl.identrust.com/DSTROOTCAX3CRL.crl]

[Root]:
version: 3
serialNumber: 44afb080d6a327ba893039862ef8406b
sigAlgName: SHA1withRSA
subjectName: CN=DST Root CA X3,O=Digital Signature Trust Co.
subjectKeyIdentifier: c4a7b1a47b2c71fadbe14b9075ffc41560858910
issuerName: CN=DST Root CA X3,O=Digital Signature Trust Co.
authorityKeyIdentifier: []
issuerCertUrl: []
notBefore: 2000/10/01_05:12:19
notAfter: 2021/09/30_22:01:15
subjectAlternativeName: []
revokeAccess:
	ocsp: []
	crl: []

仔细查看根证书,其notAfter值为:2021/09/30_22:01:15。这说明根证书已经过期,该证书链将无法通过客户端的验证,这与我们在国庆时出现的一次线上事故是匹配的。10月1日时,一项服务无法正常使用,经过检测发现,该服务所在服务器与另外一台服务器无法建立https链接,其原因就是证书过期,最终定位到具体过期的证书就是这个Lets Encrypt的根证书:DST Root CA X3。

看到证书过期时,我首先使用浏览器访问了该域名,但是并没有提示证书过期,后来检查发现,Java和浏览器访问该站点使用的是不同的证书链。

Java:dancen.com > R3 > ISRG Root X1 > DST Root CA X3
浏览器:dancen.com > R3 > ISRG Root X1

这里发生了一个怪事,Java和浏览器访问同一个站点使用的是不同的证书链,Java使用的证书链与Nginx服务器配置的的证书链一致,浏览器使用的证书链与Nginx服务器配置的证书链不一致。同时,两个不同的证书链中都包含了一个主体名称为ISRG Root X1的证书,但在Java证书链中,该证书是一个中间证书,而在浏览器证书链中,该证书则是一个根证书。

将两个ISRG Root X1证书的信息导出,仔细比较,可以看到,两个ISRG Root X1证书拥有相同的主体名称和相同的主体标识符subjectKeyIdentifier,即二者使用了相同的主体名称和相同的公钥,可以应用于同一个证书链。但二者其实是两个不同的证书,它们有着不同的序列号serialNumber,在Java证书链中,它是一个中间CA证书,而在浏览器的证书链中,它是一个自签名的根证书。

Java:浏览器:
version: 3version: 3
serialNumber: 4001772137d4e942b8ee76aa3c640ab7serialNumber: 8210cfb0d240e3594463e0bb63828b00
sigAlgName: SHA256withRSAsigAlgName: SHA256withRSA
subjectName: CN=ISRG Root X1,O=Internet Security Research Group,C=USsubjectName: CN=ISRG Root X1,O=Internet Security Research Group,C=US
subjectKeyIdentifier: 79b459e67bb6e5e40173800888c81a58f6e99b6esubjectKeyIdentifier: 79b459e67bb6e5e40173800888c81a58f6e99b6e
issuerName: CN=DST Root CA X3,O=Digital Signature Trust Co.issuerName: CN=ISRG Root X1,O=Internet Security Research Group,C=US
authorityKeyIdentifier: [c4a7b1a47b2c71fadbe14b9075ffc41560858910]authorityKeyIdentifier: []
issuerCertUrl: [http://apps.identrust.com/roots/dstrootcax3.p7c]issuerCertUrl: []
notBefore: 2021/01/21_03:14:03notBefore: 2015/06/04_19:04:38
notAfter: 2024/10/01_02:14:03notAfter: 2035/06/04_19:04:38
subjectAlternativeName: []subjectAlternativeName: []
revokeAccess:revokeAccess:
ocsp: []ocsp: []
crl: [http://crl.identrust.com/DSTROOTCAX3CRL.crl]crl: []

六. 证书验证哪些信息?

以下是一个名为dancen.com的证书的部分信息内容:

version: 3
serialNumber: 03fa41664db4ca93cc2d465c616dce0edc65
sigAlgName: SHA256withRSA
subjectName: CN=dancen.com
subjectKeyIdentifier: b93e93d1d78d291928631e4024895f2b57282872
issuerName: CN=R3,O=Let's Encrypt,C=US
authorityKeyIdentifier: [142eb317b75856cbae500940e61faf9d8b14c2c6]
issuerCertUrl: [http://r3.i.lencr.org/]
notBefore: 2021/08/30_10:18:42
notAfter: 2021/11/28_10:18:41
subjectAlternativeName: [dancen.com,www.dancen.com]
revokeAccess:
	ocsp: [http://r3.o.lencr.org]
	crl: []

对证书进行验证时,需要检查以下内容。

  1. 证书能够被解析,废话!!!

  2. 证书用途正确。
    证书的用途由KeyUsage扩展指定,包括数码签名,密钥加密等等。对于CA证书,其用途自然必须包括签署证书。

  3. 证书与网络地址匹配。
    如果是终端证书,需要检查证书适用的域名、IP等是否与当前域名、IP等匹配,证书的域名等信息由subjectAlternativeName属性指定。subjectAlternativeName属性可以指定多个值,也可以使用通配符*。

例如,subjectAlternativeName的值为[dancen.com,*.dancen.com],代表该证书可以给站点dancen.com及其所有一级子站点使用。如果该证书用于其它站点,将无法通过证书验证。

  1. 证书在有效期时间段内。
    即当前发起网络访问的时间需要在证书的notBefore和notAfter属性值指定的时间范围内。需要指出的是,这里的时间指的是客户端本地时间,因此,这是可以作弊的。一些客户端明明已经连接了网络,却无法访问互联网,就有可能是客户端时间错误的原因。

  2. 证书没有被吊销。
    当证书需要被提前终止时,例如私钥泄露时,可执行证书吊销操作。证书被吊销后,证书不再有效,那么,客户端是如何知道证书是否被吊销的呢?证书的ocsp属性和crl属性提供了两种不同的吊销证书查询入口。

crl:即证书吊销列表文件的下载链接,由于crl文件会不断增大,并且证书颁发者也不会实时地将已吊销的证书信息写入crl文件(有可能几天,甚至几个月才更新一次),因此,crl查询方式是一种头脑简单的设计产物,基本上只供备用,或者作为一种友好的吊销证书展示方式。

ocsp:ocsp查询方式克服了crl方式的一些明显缺点,在ocsp查询方式中,客户端根据当前证书及其发布者的证书的信息生成ocsp查询请求,然后ocsp服务器返回ocsp响应。依靠ocsp,证书颁发者可以近乎实时地返回证书吊销信息,并且消息体的大小也得到了控制。

总体而言,证书的吊销查询是一个尴尬的存在,不论是crl还是ocsp,它们都存在以下问题。
1). 网络性能问题。
由于客户端需要完成证书吊销查询之后才能完成证书验证,进而与服务端进行通信。而证书吊销查询本身属于一个网络IO操作,其时间开销和网络可达性都得不到保证,这严重影响了客户端与服务端通信的性能。

衍生问题:
假设证书吊销查询是必须的,那么吊销查询本身的性能问题将严重影响客户端与服务端通信的性能;一旦证书吊销查询服务器存在任何故障,无法通信,将导致后续客户端无法与服务端通信,造成严重事故。

假设证书吊销查询不是必须的,可以后置进行,以降低性能影响,那么证书吊销查询所带来的安全性将大打折扣。

假设证书吊销查询是必须的,但允许执行失败,例如,证书吊销查询执行成功则执行证书吊销验证,否则不执行证书吊销验证。那么,客户端与服务端的通信性能仍然受到证书吊销查询的影响,并且证书吊销查询所带来的安全性仍然大打折扣,因为客户端很容易屏蔽证书吊销查询。

事实上,由于证书吊销查询带来的性能问题,各种客户端对证书吊销查询的处理方式并不一致。大多数客户端,包括众多的浏览器,为了用户体验,并不总是执行证书吊销查询,它们何时执行证书吊销查询是不可预测的。这就意味着,证书吊销并非一项完全可靠的安全机制。

2). 隐私泄露问题。
由于客户端建立https请求时需要去证书颁发机构执行证书吊销查询,这就意味着客户端的网络访问行为泄露给了证书颁发机构。

  1. 证书由权威机构颁发。
    这便是证书链的验证,当前证书验证通过之后,证书验证将沿着证书链对下一个证书执行验证,下一个证书除了要满足没有过期等要求之外,还必须是上一个证书的签署颁发者。

证书验证操作顺着证书链一直到达链的末端,证书链的末端通常是一个中间证书,客户端将在本地可信任证书存储区域中搜索与该证书匹配的根证书。

根证书提前存储于客户端本地,也只有在客户端本地可信任证书存储区域存在的证书,其本身及其颁发的子证书才能获得信任,通过验证。

需要指出的是,正如前文所述,这里的证书链未必是服务端返回的证书链,客户端完全有可能根据终端证书,结合本地的可信任证书存储区域,以及证书的扩展信息中指定的签署颁发本证书的下一级证书的获取链接来构建一个新的证书链。

  • 3
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
OpenSSL是一个开放源代码的加密工具包,可以用来进行SSL/TLS协议的实现。在使用OpenSSL操作证书时,可以采取以下步骤: 1.生成私钥和证书请求:首先,使用OpenSSL生成一个私钥和一个证书请求(CSR)。私钥用来签署证书,而CSR包含了申请者的公钥信息。 2.自签名证书:接下来,使用生成的私钥和CSR生成一个自签名证书。自签名证书可以用于测试或者临时环境,但在生产环境中,应该使用由受信任的第三方机构签名的证书。 3.签名证书:如果要获得第三方机构签名的证书,需要将CSR发送给证书颁发机构(CA)。CA会对CSR进行验证,并使用CA的私钥签名生成一个证书。这个证书可以用来在客户端与服务器之间建立信任。 4.构建证书证书是由证书和根证书构成的一条验证路径。在OpenSSL中,可以通过将证书和根证书连接在一起来构建证书。同时,还需要将每个证书保存为单独的文件,以便于使用。 5.验证证书:在服务端和客户端之间建立连接时,服务器需要将证书发送给客户端。客户端使用CA的公钥来验证证书的有效性。如果证书中的任何一个证书无效或不受信任,连接将被中断。 通过以上步骤,我们可以使用OpenSSL操作证书。这样可以确保在SSL/TLS连接中建立起合法且可信的通信路径,保证通信的安全性和准确性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值