前言

无意翻到 Let's Encrypt (下称 LE )已支持 OCSP Must-Staple 扩展的消息
再加上之前也想升级双证书
于是又有了重新折腾一次的动力。
记录如下——

特别提示:
如果仅是希望使现有证书支持 OCSP Must-Staple ,
用原私钥重新生成 CSR 申请证书即可。
详见 CSR 一节。

准备

服务器环境

推荐使用 nginx-autoinstall 脚本进行安装,
原 Repo 仅适用于 Debian 8
其它发行版需要稍作修改。

编译并安装完可以使用如下工具测试相关特性:

工具选择

CA 当然选择免费且开源的 LE
工具方面则依旧选用 ACME-Tiny
官方列表中亦罗列不少,
包括最近很流行的国人作品 ACME.sh
若选用其它工具则签发流程会有所变化(相应环节会稍作说明)。

密钥强度及算法选择

RFC5480 ,对 ECDSA 证书的推荐组合如下:

| Min Bits of Security | ECDSA Key Size | Message Digest Algorithms | Curves    |
|:--------------------:|:--------------:|:-------------------------:|:---------:|
| 80                   | 192            | SHA-256                   | secp192r1 |
| 112                  | 224            | SHA-256                   | secp224r1 |
| 128                  | 256            | SHA-256                   | secp256r1 |
| 192                  | 384            | SHA-384                   | secp384r1 |
| 256                  | 512            | SHA-512                   | secp521r1 |

secp521r1/NIST P-521 曲线从 Chrome v46 的某几个子版本开始逐渐被全平台移除
而目前 LE 目前仅支持 secp256r1/NIST P-256/ANSI X9.62 prime256v1secp384r1/NIST P384 的 ECDSA 密钥;
考虑到中间证书 Let's Encrypt Authority X3 仍是 RSA-2048 证书,
选择 SHA256ECDSA-256r1 的组合足以兼顾安全和效率,
待明年 LE 提供了 ECDSA 中间证书再换到强度更高的也不迟;
而对于未来我们更期待着 curve25519 曲线的普及( OpenSSL 1.1.0 和 Chrome 50+ 已支持)。

ECDSA 证书的兼容性请参考此文

而对于 RSA 证书,通常亦有加密强度的对照关系如下:

| Symmetric | ECDSA | RSA   |
|:---------:|:-----:|:-----:|
| 80        | 163   | 1024  |
| 112       | 233   | 2240  |
| 128       | 283   | 3072  |
| 192       | 384   | 7680  |
| 256       | 512   | 15360 |

所以对于 RSA 证书我们依据 LE 中间证书选择 SHA-256RSA-2048 的组合即可。

多证书支持

NginX 1.11.0+ 已支持同时提供多个证书,
流行的使用方式是同时提供 ECDSA 和 RSA 证书,
以兼顾性能与兼容性。

此外——
NginX-CT 模块作者也于 7 月增加了对多 ssl_ct_static_scts 指令的支持
OpenSSL 1.0.1d+ 也解决了多证书时开启 ssl_stapling 指令的缺陷
至此本文所需的大部分特性都已支持多证书。

遗憾的是 ssl_stapling_file 指令目前仍会遇到问题,
原因是 LE 的 OCSP 服务器并不支持多证书的合并响应
而 NginX 也并不认为该指令应被多次使用 😂 。
我已向 LE 提了一个 Issue
在问题解决之前我们只能使用传统的 Staple 方式。

已有 Nginx 补丁可允许 ssl_stapling_file 多指令(对应于每个证书),
OCSP Stapling 一节。

申请

账户私钥

LE 的 ACME 自助签发协议需要一个账户私钥来验明身份,
而我们所选用的 ACME-Tiny 工具使用 PEM 格式的密钥。

官方客户端生成的是 JWK 格式,
可使用此脚本来进行转换。

首先来为 LE 账户准备一个私钥——
理论上可以复用你之前所使用的 RSA 或 ECDSA 私钥,
但实践中仍建议为 LE 账户使用一个独立私钥;
若对此不确定,建议重新生成一个,
LE 对相同域名证书除了签发次数之外并未作账户数量上的限制

我们假设相关文件置于 /etc/nginx/ssl 之下:

为便于未来证书续期等操作我们刻意没有设置口令( Passphrase )。

cd /etc/nginx/ssl
openssl ecparam -name secp384r1 -genkey -noout -out ecdsa/account-privkey.pem
chmod 400 account-privkey.pem

如此我们得到了一个 P-384 ECDSA 私钥。
若你和我一样曾经使用过一个 RSA 的,
请放宽心,该证书仅用于身份校验——等于你拥有两个 LE 账户而已。

另外由于 ACME-Tiny 并不会要求我们如官方客户端那样使用邮箱注册,
所以也不要“奢求”会有证书过期提醒
我们可以用 Cronjob 来保证续期。

需要注意的是如果你用旧私钥申请到的证书还未过期的话,
请不要急着随意处置你的这些私钥,
个人建议也无需急着 Revoke 证书,
静待其过期即可(三个月而已)。

域名私钥

由于最终与证书相关的文件为数不少,
我们为不同域名建立不同的目录便于管理:

quchao.com 为例。

mkdir -p sites/quchao.com/{ecdsa,rsa}
cd sites/quchao.com

不建议复用 LE 账户私钥作为域名私钥来申请证书,
推荐为每个独立域名生成三组( ECDSA + RSA )私钥,
其中两组作为候补,并不同时在线上服务。

候补私钥的作用和数量请参考后文中 HPKP 一节。

生成三个 ECDSA 私钥:

其中一组备份我们使用 P-384 曲线,
以备未来升级安全级别更高的证书。
但据此文介绍,
P-256 曲线在 OpenSSL 1.0.1L 之后有大幅针对性优化。

openssl ecparam -name prime256v1 -genkey | openssl ec -aes256 -out account-privkey.pem
openssl ecparam -name prime256v1 -genkey | openssl ec -aes256 -out private.standby1.pem
openssl ecparam -name secp384r1 -genkey | openssl ec -aes256 -out private.stabdby2.pem

生成时会被要求设置口令( Passphrase ),
设置口令后配合 NginX 的 ssl_password_file 指令使用,
但需注意口令文件的权限。

如为避免在使用(如 Web 服务启动)时被要求输入可改用如下命令生成私钥:

openssl ecparam -name prime256v1 -genkey -noout -out account-privkey.pem
openssl ecparam -name prime256v1 -genkey -noout -out private.standby1.pem
openssl ecparam -name secp384r1 -genkey -noout -out private.stabdby2.pem

检查一下私钥信息:

openssl ec -in ecdsa/privkey.pem -text -noout 

再生成三个 RSA 私钥:

其中一组备份我们计划使用 4096 位的长度,
以备未来升级安全级别更高的证书,
但也会显著影响性能

openssl genrsa -aes256 -out rsa/privkey.pem 2048
openssl genrsa -aes256 -out rsa/private.standby1.pem 2048
openssl genrsa -aes256 -out rsa/private.standby2.pem 4096

同样地,
为避免在使用时被要求输入口令,
去除上述命令中的 -aes256 参数。

检查一下私钥信息:

openssl rsa -in rsa/privkey.pem -text -noout

调整权限:

chmod 400 {ecdsa,rsa}/private*.pem

CSR

接下来生成 CSR ( Certificate Signing Request ) 。

公共配置

先准备一份公共的配置文件,
生成 CSR 时它能为我们省去不少参数:

touch /etc/nginx/ssl/openssl.cnf

内容如下:

[req]
distinguished_name = req_distinguished_name
req_extensions     = v3_req

[req_distinguished_name]

[v3_req]
basicConstraints   = CA:FALSE
keyUsage           = nonRepudiation, digitalSignature, keyEncipherment
subjectAltName     = @alt_names
1.3.6.1.5.5.7.1.24 = DER:30:03:02:01:05

[alt_names]

OCSP Must-Staple

上节配置中的 1.3.6.1.5.5.7.1.24 = DER:30:03:02:01:05 这行将会向 CA 申请俗称 OCSP Must-Staple 的 OID (Object Identifier) 证书扩展,
顾名思义服务端必须对证书启用 OCSP Stapling 功能(且需保证响应成功率),
它使客户端能够正确对待无 OCSP 响应的情形——无响应即证书被 Revoked 。

篇首我们有提及目前 LE 签发的证书已支持该特性,
著名的 SSL Labs 的 Dev 版支持了对这个扩展的检测,
而且目前几乎所有现代浏览器都支持了该特性。

除了证书的 OID 扩展方式之外,
服务器还可以通过增加响应头的方式来实现该特性,
但缺点是首次访问时不会启用该特性。

再次提醒开启该特性之后,
服务器务必同时开启 OCSP Stapling 功能,
否则将被客户端视为“证书过期”而强制拒绝。
详见 OCSP Stapling 一节。

生成 CSR

接下来为所有私钥都生成一份对应的 CSR ,
是否为候补私钥也生成 CSR 请自行决定。

以 ECDSA 私钥为例:

由于 LE 目前仅提供 DV 型证书,
所以 -subj 参数中仅有 CN 字段(即 CommonName )有意义,
其余字段是便于未来向其它 CA 申请 EV 证书所用;
另如之前在密钥长度及算法选择一节所讨论——
我们对 ECDSA-384r1RSA-4096 这两个强度更高的候补证书使用 SHA-384 哈希算法,
适时将 -sha256 替换成 -sha384 即可;
更多需求请据自身需求进行调整。
特别提示 OSX 用户请勿使用自带的 OpenSSL 0.9.8zh
否则只能使用早几年已被证明不够安全的 SHA-1 哈希算法。
请至少升级为 0.9.8o 版本以上来默认启用 SHA-256

openssl req -new -sha256 \
    -key ecdsa/privkey.pem \
    -out ecdsa/csr.pem \
    -subj "/C=CN/ST=Guangdong/L=Shenzhen/O=WatChiu/CN=quchao.com" \
    -config <(cat ../openssl.cnf <(printf "DNS.1 = quchao.com\nDNS.2 = www.quchao.com"))

生成完毕后,检查看看我们的申请是否包含了 OCSP Must-Staple 扩展:

openssl req -in ecdsa/csr.pem -noout -text | grep '1.3.6.1.5.5.7.1.24'

别忘了为 RSA 私钥也生成 CSR 。

调整权限:

chmod 600 {ecdsa,rsa}/csr*.pem

格式转换

若此前已生成过 CSR 或使用过其它 LE 的申请工具,
那么可能会涉及到格式的转换工作。(据本文操作则可略过)
例如 LE 官方客户端 CertBot 起初就只识别 DER (或称 ASN.1 )格式。
当然更建议你加上 OCSP Must-Staple 用现有私钥重新生成 CSR 。

PEM 转为 DER

openssl req -in ecdsa/csr.pem -inform PEM -out ecdsa/csr.der -outform DER

反之:

openssl req -in ecdsa/csr.der -inform DER -out ecdsa/csr.pem -outform PEM

Host Challenge

LE 所使用的 ACME 协议支持http-01dns-01 两种质询( Challenge )方式
然而 ACME-Tiny 只支持前者,
若你中意后者请选用其它工具(如 ACME.sh )。

请准备一个 ACME-Tiny 有权写入而 Web 服务有权读取目录:

mkdir -p /var/www/challenges/

将如下规则另存为一个配置文件,
然后在 NginX 的相关 server 段中引入它:

location ^~ /.well-known/acme-challenge/ {
    alias /var/www/challenges/;
    try_files $uri =404;
}

叶证书

先将 ACME-Tiny 脚本存到本地:

mkdir -p /etc/nginx/ssl/tools
curl -o /etc/nginx/ssl/tools/acme_tiny.py -L https://github.com/diafygi/acme-tiny/raw/master/acme_tiny.py

正式申请之前我们可以利用 Staging 环境进行测试,
可尝试编辑 acme_tiny.py 中的 API 域名如下:

DEFAULT_CA = "https://acme-staging.api.letsencrypt.org"
#DEFAULT_CA = "https://acme-v01.api.letsencrypt.org"

使用 Staging 环境,相应中间证书和根证书需要换成 Fake LE Intermediate X1Fake LE Root X1

以 ECDSA 证书为例:

python /etc/nginx/ssl/tools/acme_tiny.py --account-key /etc/nginx/ssl/account-privkey.pem --csr ecdsa/csr.pem --acme-dir /var/www/challenges/ > ecdsa/signed.pem

若执行时报错 expecting an rsa key
则是因为你使用了 ECDSA 的账户私钥,
因为 ACME-Tiny 官方目前仅支持 RSA 账户私钥。
建议改用 drdaemanFork 版,
但使用其最新的提交 d19090b 需依赖 Python 3+
(原作者 diafygi 的设计初衷是保证这个名为 Tiny 的程序少于 200 行,所以 Fork 版利用新语法做了“精简”)
Python 2 的用户可改用哈希为 e740b59 的提交。

若无报错,
则得到了叶证书( Leaf Cert ,又称 End-Entity Cert ) signed.pem
可尝试验证其信息:

openssl x509 -in ecdsa/signed.pem -text -serial -fingerprint -noout

别忘了继续申请 RSA 证书。

调整权限:

chmod 600 {ecdsa,rsa}/signed.pem

若最终利用 Staging 环境的证书验证成功,
务必把 API 切回来重新申请正式的证书供线上服务使用。

证书链

根据 NginX 文档的介绍,
我们还需为 ssl_certificate 指令准备一个证书链,
顺序是从叶证书起逐个连接中间证书;
此证书链无需包含根证书。

从申请到的证书信息中我们得知证书由 Let’s Encrypt Authority X3 签发,
这里我们也能看到信任链及相关证书地址,
并确信 X3 便是 LE 目前所启用的中间证书。

由于 LE 的根证书 ISRG Root X1 还未被广泛信任,
因此我们选择由 IdenTrust 交叉签名的中间证书;
好消息是 macOS 10.12.1 、 Firefox 50 已先后将其加入了信任列表,
相信不久的将来我们就可以直接使用 LE 自己的证书链了。

先将中间证书下载下来:

mkdir -p /etc/nginx/ssl/intermediates
curl -o /etc/nginx/ssl/intermediates/lets-encrypt-x3-cross-signed.pem -L https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem
chmod 600 ../intermediates/lets-encrypt-x3-cross-signed.pem

依次合并叶证书和中间证书,以 ECDSA 证书为例:

cat ecdsa/signed.pem /etc/nginx/ssl/intermediates/lets-encrypt-x3-cross-signed.pem > ecdsa/chained.pem

别忘了为 RSA 证书也单独合并一份。

调整权限:

chmod 600 {ecdsa,rsa}/chained.pem

配置

基本准备就绪,
可修改目标站点的 NginX 配置来使用证书:

其它配置推荐参考 NginX Boilerplate Configs 项目。

# RSA cert
ssl_certificate     ssl/sites/quchao.com/rsa/chained.pem;
ssl_certificate_key ssl/sites/quchao.com/rsa/privkey.pem;

# ECDSA cert
ssl_certificate     ssl/sites/quchao.com/ecdsa/chained.pem;
ssl_certificate_key ssl/sites/quchao.com/ecdsa/privkey.pem;

至此我们完成了最基本的双证书配置。

测试

ECDHE-ECDSA-AES128-SHA256 来测试 ECDSA 证书:

由于我们利用 SNI 扩展使单 IP 服务了多个站点,
因此还需加上 -servername quchao.com 参数。

time openssl s_client -connect quchao.com:443 -servername quchao.com -status -tlsextdebug -tls1_2 -cipher ECDHE-ECDSA-AES128-GCM-SHA256 </dev/null

再以 ECDHE-RSA-AES128-GCM-SHA256 来测试 RSA 证书:

time openssl s_client -connect quchao.com:443 -servername quchao.com -status -tlsextdebug -tls1_2 -cipher ECDHE-RSA-AES128-GCM-SHA256 </dev/null

通过粗略地多次本地 time 测试,
我们能大致看出使用 ECDSA 证书的连接建立时间要稍短于 RSA 证书。

更精准的性能测试仍需从各类客户端和网络环境进行结果采集。

ECDSA: 0.02s user 0.01s system 40% cpu 0.075 total
RSA:   0.02s user 0.00s system 30% cpu 0.089 total

若测试不符合预期,建议先调整加密套件配置。

升级 HTTP 请求

既已完成了 HTTPS 的配置,
我们何不将所有 HTTP 的请求全部“升级”过去?

请修改目标站点的 NginX 配置:

# make sure they're in the same server{}
#listen 80;
#listen 443 ssl spdy http2;

# http://$server_name:80
if ($ssl_protocol = "") {
    return 301 https://$server_name$request_uri;
}

# http://$server_name:443
error_page 497 https://$server_name$request_uri;

其中 497 错误码仅当一个 server 段同时支持 80443 时,
客户端尝试通过 HTTP 协议访问 443 端口产生。

除此之外,
我们还可以使用 CSP 的 upgrade-insecure-requests 指令来升级页面中的其它非安全请求,
由于 CSP 又是另一个庞大话题,此处不表。

A+ 之路

SSL Labs 是一个流行的针对服务端、客户端 SSL 相关特性的在线评估工具,
相较于 TestSSL.sh 这类命令行工具自有其便捷、友好等优势。
它会给所测站点打出分数,
因此你常会在各种论坛、部落格上看到用户展示自己站点的得分,
也因此你能找到一些满分攻略

随着安全要求的不断提升,
其评分规则也是长期缓慢变化中的;
此章取 A+ 之题是尽量去追求更高的分数。
当然站点的安全并不能唯分数论(木桶原理),
事实上一些 Alexa 排名靠前的站点为了获得更高的客户端兼容性,
不得不做某些妥协(比如依旧使用 SSL 3 )。

此文介绍了有关 SSL 安全配置的诸多方面,
建议阅读并采纳,
这里只介绍部分多证书情况下所涉及的配置——

加密套件

选择

加密套件( Cipher Suite )的优选本身就是一门学问,
为获得安全及兼容性的平衡,
站点可根据访客的设备占比来进行微调。

双证书的配置下它更是决定了 ECDSA 和 RSA 证书的优先程度。
由于我的部落格主要只有自己访问 😂 ,
我选择 ECDSA 先于 RSA 的配置:

较激进,完全丢弃了 3DES 可用于兼容老客户端的算法。

ssl_ciphers 'EECDH+CHACHA20 EECDH+CHACHA20-draft EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+ECDSA+SHA EECDH+aRSA+SHA EDH+aRSA AES128-SHA  !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS !RC4 !SEED !CAMELLIA';

当然,还需确保 TLSv1 SSLv3 请禁用)握手时强制使用服务端套件配置:

ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;

性能测试

不少文章都会提到 ChaCha20-Poly1305 在 ARM 平台的加解密速度优势;
同时也会提到支持 AES-NI 扩展的设备拥有不可比拟的 AES 系列算法硬件加速,
涉及的 Cipher 包括 AES-128/256-GCMAES-128/256-CBC

据前文配置,
我们设想 ChaCha20-Poly1305AESGCM 等系列 Cipher 将被优先使用。
我们先看看它们包含了哪些 Cipher (及顺序如何):

v1.0.2 版本不打 Patch 无法支持 ChaCha20-Poly1305
详见前文

openssl ciphers -V 'EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+ECDSA+AESGCM:EECDH+aRSA+AESGCM' | column -t

结果略。
顺序符合预期,
那么性能呢:

openssl speed -elapsed -evp chacha20-poly1305
openssl speed -elapsed -evp aes-128-gcm
openssl speed -elapsed -evp aes-256-gcm

在我支持并开启 AES-NI 扩展的服务器上测试结果如下(仅供参考):

type                16 bytes    64 bytes    256 bytes   1024 bytes  8192 bytes
chacha20-poly1305   56670.68k   205242.45k  414880.43k  754832.38k  1098667.35k
aes-128-gcm         321414.89k  798483.80k  1608541.87k 2040961.71k 2333619.54k
aes-256-gcm         274336.83k  708359.85k  1378406.91k 1640803.33k 1685820.76k

当然这仅能反映服务端性能,
对 ARM 平台的诸多 Android 设备还请另作测试。

安全测试

Mozilla 推出的 CipherScan 工具可以用来便捷地调试该配置,
Checkout 后执行:

依赖 Bash v4 的一些特性,
macOS 用户可通过 brew install bash 升级。

python analyze.py -t quchao.com

扫描结果会基于安全和性能考虑给出一些调整建议。

更多有关加密套件(及其它)的推荐配置可参考:

正向安全

正向安全( Forward Secrecy ) 是指服务端和客户端为每个会话协商一个永不重用的密钥( 即 Session Key ),
并在会话结束后将其销毁;
而实际的私钥仅用于 Diffie-Hellman 握手签名,
倘若该私钥不幸被盗,
此前的所有会话内容也都将保证安全(因为会话密钥已被销毁)。

建议
除了需在加密套件中避免使用 RSA 密钥交换之外(优先使用 ECDHE ,DHE 可做兜底),
为了避免服务端使用 OpenSSL 默认的 1024 位长度密钥与客户端进行交换,
需要生成等 2048 位及以上的 DHE Parameter

openssl dhparam 2048 > dhparam.pem

调整权限:

chmod 400 dhparam.pem

然后在 NginX 目标站点的配置中添加:

ssl_dhparam ssl/sites/quchao.com/dhparam.pem;

更多有关其它服务端的正向安全配置可参考:

HSTS

HSTSHTTP Strict Transport Security )响应头是用来告诉客户端强制对该站点使用安全传输,
若以 HTTP 方式请求会被客户端重定向到 HTTPS 协议,
并将这种策略以指定时间缓存下来。

以 Chrome 为例——
客户端会对这类站点直接进行 307 Internal Redirect 跳转,
这样的纯客户端跳转无疑比走一次服务端 30x 跳转要高效、安全得多。

启用方法是在 NginX 目标站点的配置中添加:

add_header Strict-Transport-Security "max-age=31536000; includeSubdomains; preload" always;

然而对于从未访问过站点的新访客,
始终有可能在 HSTS 发挥功用之前以不安全的 HTTP 方式请求站点。
于是 Chrome 创建了 HSTS 的预载列表Preload List ),
且目前已内置在几乎所有主流浏览器中。

可到 HSTS Preload 将域名提交到预载列表。
但提交前请关注其几点要求
其中尤其需要注意的是——

  • 需监听同主机上的 http 请求并使用 Location重定向至 https ;
  • 必须启用 preloadincludeSubDomains 指令;
  • 标示缓存时间的 max-age 指令取值必须大于 10886400 秒,即 18 周。

HPKP

HPKPHTTP Public Key Pinning )扩展是这样一项 HTTP 的安全特性:
它通过在响应头中输出事先计算好的 SPKI 指纹,
告知客户端所访问的服务关联了哪些特定的公钥,
支持该特性的客户端便可以拒绝继续请求,
并将错误的指纹进行上报,
以达到降低中间人攻击的风险。

策略

它是一项(对站点管理者来说)相当“危险”的特性,
因为一般使用时会设定一个较长的缓存时间,
一旦站点证书链中的公钥指纹与客户端所缓存的无一符合,
用户将被拒之门外直到缓存过期。
因此就需要我们——

  • 设置合理的缓存时长(短于证书过期时限);
  • 选择证书链中合适的节点计算指纹;
  • 除了实际使用的证书所含公钥之外,另准备若干公钥以备轮换;
  • 设置上报和告警,以便及时响应;

对于需要准备几个备份公钥指纹并没有强制规定( RFC7469 只建议至少一个),
Scott Helme 在其博文中分析了多种搭配。
本文选择备份两组——
其中一组使用更高强度便于及时升级;
而对于选取证书链中的哪一级来计算指纹文中亦有诸多介绍。

个人认为备份一组已足够,
毕竟我们同时备份了 ECDSA 和 RSA 私钥;
同时我选择 Pin 叶证书(而非中间证书甚至根证书)的指纹,
因为 LE 的开放性既便于我随时拿出备份私钥申请证书,
同时也利于他人申请同名证书后利用 DNS 劫持、“中间人”等手段绕过 HPKP ,
因此若选择 Pin LE 的中间证书(不建议)请务必额外选择一家 CA 。

获取指纹

SPKI 指纹可从私钥、 CSR 和证书等多种源头解出公钥哈希后 Base64 编码获得,
本文以 CSR 为例:

若在前文没有为备份私钥生成 CSR ,
此时可以私钥为对象来执行操作
注意输出公钥时 openssl 子命令仍有 ecrsa 之分。

从 ECDSA 证书 CSR 解出 SPKI 指纹:

目前该特性仅支持 SHA-256

openssl req -in ecdsa/csr.pem -pubkey -noout | openssl ec -pubin -inform pem -outform der | openssl dgst -sha256 -binary | openssl enc -base64

从 RSA 证书 CSR 解出 SPKI 指纹:

openssl req -in rsa/csr.pem -pubkey -noout | openssl rsa -pubin -inform pem -outform der | openssl dgst -sha256 -binary | openssl enc -base64

别忘了为备份证书也生成指纹。

特别提醒
计算完指纹之后,
请将之前生成好的 ECDSA 和 RSA 各两组私钥及 CSR 备份分开置于不同的地方安全保管,
加密且离线保存最佳。

配置

至此我们已得到若干 SPKI 指纹,
接下来在 NginX 目标站点的配置中添加:

其中——
pin-sha256 指令数量与私钥(含备用)数量一致。
report-uri 指令用以指定 HPKP 上报服务,
我在使用 Scott Helme 所开发的 Report-URI 服务,
该服务亦能用来计算指纹。
更多指令释义请参考这里
最后建议在启用 Public-Key-Pins 头之前先用 Public-Key-Pins-Report-Only 收集问题,
两者亦能长期并行使用。

add_header Public-Key-Pins 'pin-sha256="{SPKI_FP_1}"; pin-sha256="{SPKI_FP_2}"; max-age=2592000; includeSubDomains; report-uri="https://test.report-uri.io/r/default/hpkp/enforce"' always;

OCSP Stapling

之前另有博文详述,
请移步《在 NginX 上为 Let's Encrypt 签发的证书配置 OCSP Stapling》。

前文说述,
目前还无法启用 ssl_stapling_file 指令,
待 LE 支持后我会继续更新。

ssl_stapling_file 的多指令补丁见这里
稍后见上述博文更新。

CT

CTCertificate Transparency ) 是 Google 发起的项目,
是对现有的 SSL 认证体系的一个扩展——
它向其增加了日志、监控和审计三个功能来确保证书链的有效。

CA 或证书拥有者向 CT 日志服务器提交数据之后便开始接受审计和监控,
任何人都能通过这些日志来实时判断目标站点的证书链真伪,
真正实现了其名所示的“透明度”。

尽管目前只有 Chrome 实现了对 Signed Certificate Timestamp (下称 SCT )的校验
对于尚未启用 CT 特性的 EV 证书站点,
Chrome 已不会为其显示 Green Address Bar (俗称“小绿条”)了;
对于咱们这些 LE DV 证书用户来说虽然影响不大,
但部署成本也不高,
试试无妨。

需要注意的是:
2018 年 4 月是 Chrome 全面强制(公开)证书接入 CT 日志的截止时间
否则届时将出现安全提示影响站点访问。

服务端支持

据官方介绍,CT 支持三种 SCT 分发方式:

  • X.509v3 扩展;
  • TLS 扩展;
  • OCSP Stapling ;

鉴于 LE 并未支持方式 1 和 3 ,(但即将于 2017 年 10 月支持方式 1
我们选择 TLS 扩展的方式,
即握手阶段服务端向客户端返回类型为 signed_certificate_timestamp 的扩展字段。

NginX 用户可源码编译 nginx-ct 模块。
(若你在前文中选用了 nginx-autoinstall 来编译 NginX ,直接勾选相关选项即可)

Apache 则有官方模块 mod_ssl_ct
其它 Web 服务端请自行搜索。

日志服务器

接下来请选择若干目标日志服务器
可惜的是并非所有服务器都信任了 LE 的根证书,
因此请优先选择 Operator: Google 的(基本都信任 LE ,除了 SkyDiver 和已冻结的 Aviator ),
其次也请将 Chrome inclusion status: Included 状态纳入考虑。

测试请使用 TestTube 服务器,
但相应地中间证书和根证书需要显性指定为 LE Staging 环境证书。

获取 SCT

接着请编译 ct-submit 工具,
它以 Go 编写,
请 Checkout 下来后源码编译之:

亦有其它语言的实现版本,如 PHPPython 等。

cd /etc/nginx/ssl/tools/ct-submit
go build
cp ct-submit-1.1.2 /usr/local/bin/ct-submit
chmod +x /usr/local/bin/ct-submit

ssl_ct_static_scts 指令接纳目录为值,
而且我们也希望将证书提交到多个日志服务器,
因此建议为 ECDSA 和 RSA 证书分别建立目录用来管理 .sct 文件:

cd /etc/nginx/ssl/sites/quchao.com
mkdir -p {ecdsa,rsa}/sct

正式提交证书链(根证书可省略)到日志服务器:

以专门接收 LE 证书日志的 ct.googleapis.com/icarus 服务器和 ECDSA 证书为例。

ct-submit ct.googleapis.com/icarus < ecdsa/chained.pem > ecdsa/sct/google-icarus.sct

别忘了也为 RSA 证书获取 SCT 。
同个证书可提交至多个不同日志服务器,
备份证书无需提交。

调整权限:

chmod 600 {ecdsa,rsa}/sct/*.sct

提交后的日志可通过 crt.sh 来检索,
Facebook 最近也提供了自己的监控工具供你订阅相关站点的 CT 信息。

配置

至此我们已得到若干 SCT 文件,
接下来在 NginX 目标站点的配置中添加:

多证书情形下请注意务必将不同证书的配置归为一处。
下方示例中的 ssl_certificatessl_certificate_key 指令于前文中已添加。

ssl_ct on;

# RSA cert
ssl_certificate     ssl/sites/quchao.com/rsa/chained.pem;
ssl_certificate_key ssl/sites/quchao.com/rsa/privkey.pem;
ssl_ct_static_scts  ssl/sites/quchao.com/rsa/sct/;

# ECDSA cert
ssl_certificate     ssl/sites/quchao.com/ecdsa/chained.pem;
ssl_certificate_key ssl/sites/quchao.com/ecdsa/privkey.pem;
ssl_ct_static_scts  ssl/sites/quchao.com/ecdsa/sct/;

至此我们配置完毕,
可以通过 Chrome 的 NetLog 或 DevTools 的 Security 面板来确认是否开启,
亦可以使用我们多次提到的 SSL Labs

测试及监控

我的多个站点证书的续期和提交 CT 日志服务器等操作均是每月首日自动完成,
因此确保 SCT 信息正确有效就显得尤为重要。
除了可以利用工具测试及定期校验之外,
一个新增的 HTTP 头 Expect-CT 也能满足这个需求——
它将在出现问题时进行上报(如 SCT 信息与证书不匹配,抑或根本不支持 CT )。

开启 Expect-CT 依旧简单,
在 NginX 目标站点的配置中添加:

add_header Expect-CT 'enforce; max-age=86400; report-uri="https://test.report-uri.io/r/default/ct/enforce"' always;

这样即宣称该站点支持 CT 并让 Chrome 做强制校验。
若想在强制开启前先做测试(仅作上报不做拦截),
enforce 指令去掉即可。

注意 max-age 指令控制着客户端对 CT 校验策略的缓存秒数,
对 LE 这类有效期限短需要定期续期的证书请勿将该值设置过长,
以免证书续期后 SCT 信息未及时同步更新而导致站点无法访问。

另外,我仍旧使用 Report-URI 服务来收集日志。

DNS CAA 记录

CAACertification Authority Authorization )是一种新增的 DNS 记录类型,
你可以用它来“宣称”哪些 CA 有权签发目标域名的证书;
而 CA 也可以利用它来拒绝那些非法的证书签发请求。

目前支持该特性的 CA 并不多,
正好 LE 就是其一;
相应的,当前支持 CAA 记录的 DNS 服务也不多;
而 SSL Labs 的 Dev 版本已支持了该特性的检测。

由于 还未支持该特性,
所以本站暂时尚未予以部署

包括 CloudFlare 在内的多家 DNS 服务提供商已予以支持。
如果你有兴趣,
可以尝试 SSL Mate 出品的这款工具

其它

文件结构

由于涉及的文件较多,
梳理其大致结构如下:

请据个人情况调整,特别注意多个私钥相关权限。

.
|____intermediates                          中间证书
| |____lets-encrypt-x3-cross-signed.pem
| |____lets-encrypt-x4-cross-signed.pem
| |____letsencryptauthorityx3.pem
| |____letsencryptauthorityx4.pem
|____roots                                  根证书
| |____DST_Root_CA_X3.pem
| |____isrgrootx1.pem
|____tools                                  工具
| |____acme_tiny.py
| |____ct-submit
| |____renew_cert.sh
| |____update_ocsp_cache.sh
|____sites                                  按站点存放的相关文件
| |____quchao.com
| | |____ecdsa                              ECDSA 证书相关
| | | |____sct                              SCT 文件
| | | | |____ct_google-icarus.sct
| | | | |____ct_google-pilot.sct
| | | |____chained.pem
| | | |____csr.pem
| | | |____dhparam.pem
| | | |____ocsp.reply.old
| | | |____ocsp.resp                        OCSP 响应缓存(暂不支持多证书)
| | | |____privkey.pem
| | | |____signed.pem
| | |____rsa                                RSA 证书相关
| | | |____sct                              
| | | | |____ct_google-icarus.sct
| | | | |____ct_google-pilot.sct
| | | |____chained.pem
| | | |____csr.pem
| | | |____dhparam.pem
| | | |____ocsp.reply.old
| | | |____ocsp.resp                        
| | | |____privkey.pem
| | | |____signed.pem
|____account-privkey.pem                    LE 账户私钥
|____ca-bundle.pem                          证书链 Bundle
|____openssl.cnf                            专门用于生成的 CSR 的通用配置

实用工具或信息

除了文中已提及的工具,这里再推荐一些:

更新历史

标签: Security, NginX

添加新评论