在服务器上启用HTTP公钥固定扩展的教程
公钥固定(PublicKeyPinning)是指一个证书链中必须包含一个白名单中的公钥,也就是说只有被列入白名单的证书签发机构(CA)才能为某个域名*.example.com签发证书,而不是你的浏览器中所存储的任何CA都可以为之签发。本文讲述了这种机制的背景知识,并提供了Apache、Lighttpd和NGINX上的配置范例。
HTTP公钥固定扩展
用你使用的银行做个例子,它一直使用CA公司A为其签发证书。但是在当前的证书体系下,CA公司B、CA公司C和NSA的CA都能给你的银行创建证书,而你的浏览器会毫无疑虑的接受它们,因为这些公司都是你所信任的根CA。
如果你的银行实现了HPKP并固定了它们的第一个中级证书(来自CA公司A),那么浏览器将不会接受来自CA公司B和CA公司C的证书,即便它们也有一个有效的信任链。HPKP也允许你的浏览器将这种违例行为报告给该银行,以便银行知道被伪造证书攻击了。
HTTP公钥固定扩展是一个从2011年开始开发的针对HTTP用户代理(即浏览器)的公钥固定标准。它由Google发起,甚至在Chrome中实现的固定机制可以使用一个人工维护的网站公钥固定列表,这个列表包含了固定的几个网站的公钥签名。(LCTT译注:Chrome和FireFox32及以后版本都支持公钥固定机制,并使用内置的人工维护的公钥固定列表数据,这些数据随着浏览器软件的更新而更新,主要包括几个大型站点。目前还只有Chrome38+支持通过HTTP响应头传递公钥固定信息。)
以下是HPKP的几个功能简述:
- HPKP是在HTTP层面设置的,使用Public-Key-Pins(PKP)响应头。
- 该规则的保留周期通过max-age参数设置,单位是秒。
- PKP响应头只能用于正确的安全加密通讯里面。
- 如果出现了多个这样的响应头,则只处理第一个。
- 固定机制可以使用includeSubDomains参数扩展到子域。
- 当接收到一个新的PKP响应头时,它会覆盖之前存储的公钥固定和元数据。
- 公钥固定是用哈希算法生成的,其实是一个“主题公钥信息(SKPI)”指纹。
本文首先会介绍一些HPKP工作的原理,接下来我们会展示给你如何得到需要的指纹并配置到web服务器中。
SPKI指纹-理论
通常来说,对证书进行哈希是一个显而易见的解决方案,但是其实这是错的。不能这样做的原因是CA证书可以不断重新签发:同一个公钥、主题名可以对应多个证书,而这些证书有不同的延展或失效时间。浏览器从下至上地在证书池中构建证书链时,另外一个版本的证书可能就替代匹配了你原本所期望的证书。
举个例子,StartSSL有两个根证书:一个是以SHA1签名的,另外是一个是SHA256。如果你希望固定住StartSSL作为你的CA,那么你该使用哪个证书呢?你也许可以使用这两个,但是如果我不告诉你,你怎么会知道还有一个根证书呢?
相反地,对公钥进行哈希则不会有这个问题:
浏览器假定子证书是固定不动的:它总是证书链的起点。子证书所携带的签名一定是一个有效的签名,它来自其父证书给这个证书专门签发的。这就是说,父证书的公钥相对于子证书来说是固定的。所以可推论公钥链是固定的。
唯一的问题是你不能固定到一个交叉认证的根证书上。举个例子,GoDaddy的根证书是Valicert签名的,这是为了让那些不能识别GoDaddy根证书的老客户可以信任其证书。然而,你不能固定到Valicert上,因为新的客户在证书链上发现了GoDaddy证书就会停止上溯(LCTT译注:所以就找不到固定信息了)。
此外,我们是对SubjectPublicKeyInfo(SPKI)进行哈希而不是对公钥位串。SPKI包括了公钥类型、公钥自身及其相关参数。这很重要,因为如果对公钥进行哈希就有可能导致发生曲解攻击。对于一个Diffie-Hellman公钥而言:如果仅对公钥进行哈希,而不是对完整的SPKI,那么攻击者可以使用同样的公钥而让客户端将其解释为其它组。同样地,这样也有可能强制将一个RSA密钥当成DSA密钥解释等等。
固定在哪里
你应该固定在什么地方?固定你自己的公钥并不是一个最好的办法。你的密钥也许会改变或撤销。你也许会使用多个证书,经常轮换证书的话密钥就改变了。也许由于服务器被入侵而撤销证书。
最容易但是不是太安全的方法是固定第一个中级CA证书。该证书是签名在你的网站证书之上的,所以签发该证书的CA的公钥肯定是在证书链上的。
采用这种方法你可以从同一个CA更新你的证书而不用担心固定信息不对。如果该CA发行了一个不同的根证书,也许你会遇到一些问题,对此并没有太好的解决方案。不过你可以通过如下做法来减轻这种问题的影响:
从一个不同的CA申请一个备用的证书,并固定该备份。
RFC里面说你至少需要做两个固定。一个是当前连接所使用的证书链上的,另外一个是备份的。
另外的固定是对备份公钥的,它可以是来自另外一个给你签发证书的不同CA的SKPI指纹。
在这个问题上还有一种更安全的方法,就是事先创建好至少三个独立的公钥(使用OpenSSL,参见此页了解JavascriptOpenSSL命令生成器),并将其中两个备份到一个安全的地方,离线存储、不要放到网上。
为这三个证书创建SPKI指纹并固定它们,然后仅使用第一个作为当前的证书。当需要时,你可以使用备份密钥之一。不过你需要让CA给你做签名来生成证书对,这可能需要几天,依你的CA的工作情况而定。
对于HPKP来说这没有问题,因为我们使用的是公钥的SPKI哈希,而不是证书。失效或不同的CA签名链并不影响。
如果你按照上述方法生成并安全存储了至少三个独立的密钥,并固定它们,也可以防止你的CA撤销你的网站证书并签发一个假证书时出现问题。
SPKI指纹
可以使用如下的OpenSSL命令来生成SPKI指纹,它出现在RFC草案中:
opensslx509-noout-incertificate.pem-pubkey|\ opensslasn1parse-noout-informpem-outpublic.key; openssldgst-sha256-binarypublic.key|opensslenc-base64
结果:
klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY=
上面输入的certificate.pem文件是本站(https://raymii.org)的证书链中第一个证书。(在写本文时,COMODORSADomainValidationSecureServerCA,序列号2B:2E:6E:EA:D9:75:36:6C:14:8A:6E:DB:A3:7C:8C:07)。
你也需要同样对你的另外两个备份公钥生成指纹。
故障
在写本文时(2015/1),唯一支持HPKP的浏览器(chrome)有一个严重的问题:Chrome并不能够区分HSTS和HPKP响应头中的max-age和includeSubdomains参数。也就是说,如果你的HSTS和HPKP设置了不同的max-age和includeSubdomains参数,它们会互相搞乱。关于这个故障的更多信息参见:https://code.google.com/p/chromium/issues/detail?id=444511。感谢ScottHelme(https://scotthelme.co.uk)发现并告诉我这个Chromium项目的问题。
Web服务器配置
下面你可以看到三个主流Web服务器的配置方法。这只是一个HTTP响应头,绝大多数Web服务器都可以设置它。它只需要设置到HTTPS网站上。
下面的例子固定到COMODORSADomainValidationSecureServerCA及备份的ComodoPositiveSSLCA上,30天失效期,包括所有的子域。
Apache
编辑你的Apache配置文件(如/etc/apache2/sites-enabled/website.conf或/etc/apache2/httpd.conf),并添加下列行到你的VirtualHost中:
#如需要,载入headers模块。 LoadModuleheaders_modulemodules/mod_headers.so HeadersetPublic-Key-Pins"pin-sha256=\"klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY=\";pin-sha256=\"633lt352PKRXbOwf4xSEa1M517scpD3l5f79xMD9r9Q=\";max-age=2592000;includeSubDomains"
Lighttpd
Lighttpd更简单一些,将下列行添加到你的Lighttpd配置文件(如/etc/lighttpd/lighttpd.conf):
server.modules+=("mod_setenv") $HTTP["scheme"]=="https"{ setenv.add-response-header =("Public-Key-Pins"=>"pin-sha256=\"klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY=\";pin-sha256=\"633lt352PKRXbOwf4xSEa1M517scpD3l5f79xMD9r9Q=\";max-age=2592000;includeSubDomains") }
NGINX
NGINX的配置更简短。添加以下行到你的HTTPS配置的server块中:
add_headerPublic-Key-Pins'pin-sha256="klO23nT2ehFDXCfx3eHTDRESMz3asj1muO+4aIdjiuY=";pin-sha256="633lt352PKRXbOwf4xSEa1M517scpD3l5f79xMD9r9Q=";max-age=2592000;includeSubDomains';
报告功能
HPKP报告功能允许浏览器报告任何违例给你。
如果你在响应头中添加了附加的report-uri="http://example.org/hpkp-report"参数,并用该URI处理接收到的数据的话,客户端会在发现违例时发送报告给你。这个报告是以POST方式发送到你指定的report-uri上,并以类似下面的JSON格式:
{ "date-time":"2014-12-26T11:52:10Z", "hostname":"www.example.org", "port":443, "effective-expiration-date":"2014-12-31T12:59:59", "include-subdomains":true, "served-certificate-chain":[ "-----BEGINCERTIFICATE-----\nMIIAuyg[...]tqU0CkVDNx\n-----ENDCERTIFICATE-----" ], "validated-certificate-chain":[ "-----BEGINCERTIFICATE-----\nEBDCCygAwIBA[...]PX4WecNx\n-----ENDCERTIFICATE-----" ], "known-pins":[ "pin-sha256=\"dUezRu9zOECb901Md727xWltNsj0e6qzGk\"", "pin-sha256=\"E9CqVKB9+xZ9INDbd+2eRQozqbQ2yXLYc\"" ] }
非强制,只报告
HPKP也可以设置为非强制的,可以使用Public-Key-Pins-Report-Only来只发送违例报告给你。
这样可以让你在网站不可访问或HPKP配置不正确时不固定,之后你可以将这个响应头改为Public-Key-Pins来强制固定。