openssl基本密码学操作

 

openssl的基本检查

使用以下命令检测版本,-a可以提供完整数据。

openssl version
openssl version -a

speed test

speed测试是openssl为你跑一下不同算法在你机器上的实际执行速度,这项测试在openssl中是一项非常有指导意义的测试。一方面,他给出了你选择算法的依据,通过实际数据告诉你每个算法能跑多快。另一方面,他可以用来评估不同硬件对算法的加速能力。如果仅仅是给出了选择算法的能力,我们一般都可以得到一个一般性结论,例如chacha20比AES快。但实际上很多CPU带有AESNI指令集,这种情况下AES的执行速度反而会更高。所以运行性能是和执行平台紧密相关的。关于这部分,可以参考Intel对OpenSSL的性能优化

具体的测试方法是

openssl speed

后面可以跟算法,只测试特定的算法集。

我这里跑了一遍全集,挑几个重点算法说一下性能吧。

hash算法

  1. sha256,标杆性hash算法,64字节小数据140M/s,8k大数据353M/s。sha512,170/470。hash算法的内部状态越长,在连续计算时的速度越快。
  2. sha1,251/768。
  3. md5,243/575。(你没看错,md5比sha1还慢)
  4. rmd160比sha256还慢,whirlpool比sha256慢。最快的是ghash,小数据4222/9732。但是见鬼的是,我查不着这是TMD什么算法(openssl list -digest-algorithms的输出里没有)。
  5. 最合适的算法,应该就是sha-512/256了吧。很安全,速度比sha256快,长度也不算太长,还能防御LEA(Length extension attack)。

对称算法

  1. aes-128-cbc,标杆算法,120/125(M/s)。aes-192,93/103。aes-256,86/88。aes的内部状态越长,在连续计算时的速度越慢。这点和hash正好相反。
  2. camellia128,138/167。camellia192,110/128。camellia256,109/124。这是一种大批量数据计算非常优越的算法,AES在计算大批量时性能上升并不快。
  3. des比aes慢的多,只有66M/s。3DES更慢,只有25M/s。
  4. 没有chacha20。
  5. 不考虑chacha20的情况下,最好的算法应该是camellia128。当然,工业标杆是aes-128-cbc。

非对称算法

  1. rsa 1024/2048/3072/4096的sign效率分别是8698/1351/453/206(个/s),verify效率分别是131847/46297/22970/13415。rsa也是随着内部状态上升效率下降的,而且下降非常快。而且verify效率远高于sign。
  2. dsa 1024/2048的sign效率分别是9836/3280,verify的效率分别是10584/3616。
  3. ecdsa 192/224/256/384的sign效率分别是12696/12672/21016/4383,verify效率分别是3200/5630/9994/1019。能很明显看出来,sign效率比verify高。256位的时候由于某种效应性能达峰,后续直接断崖下跌。
  4. ecdh 192/224/256/384的效率为3642/8339/15094/1183。同样能看出这种效应。
  5. rsa和ecc不具有互换性。rsa参数选择建议2048,ecc参数选择建议256。

对称加解密

openssl支持多种对称加密算法,可以直接对文件加解密。在使用前,我们首先列出系统上支持的算法。

openssl enc -ciphers

输出很复杂,不列举。我们直接讲我的机器上分析后的结果。

  1. 第一段是密码算法。在我这里,支持以下算法:aes, bf, blowfish, camellia, cast, chacha20, des, des3, desx, id(ea), rc2, rc4, seed。
  2. 最后一段有可能是模式。在我这里,支持以下模式:ECB,CBC,CFB,OFB,CNT。其中CFB,OFB和CTR(CNT)是可流式的,其余都是块式的。
  3. enc的manpages里明确说了,enc不支持CCM或是GCM这类的authenticated encryption。推荐是使用CMS。

例如我们使用比较流行的chacha20来加密一个文件src,里面可以随便写一句话。

openssl enc -chacha20 < src > dst

注意dst应该会比src大。因为默认情况下,openssl会为密码加一个salt,然后把salt保存到加密结果上去。再从passwd+salt里推导出key和IV(默认sha256)。默认的salt为8bytes,合64bits。key为32bytes,合256bits。IV为16bytes,合128bits。具体情况可以用openssl enc -P -chacha20来打印。

另一点让我比较惊讶的就是,chacha20是一种流式算法。如果你采用-aes-128-ecb的话(这是一种典型的块式算法,已经研究的比较透彻了),输出是长度对16对整加16字节。而chacha20的输出纯粹比输入长16个字节。我觉得很好奇,于是就找了这个源码研究了下。

算法的核心状态机是一个64字节的数组,第一个分组16字节填充固定数据,第二个分组32字节填充key,第三个分组8字节填充nonce,最后8字节填充IV。然后通过一个变形算法,把这个核心状态变成一个out数组,再XOR到目标数据上去。每次输出一个out数组,nonce都会自动增长。

如果他的算法没错的话,chacha20非但是一个流式算法,而且主体算法就是CTR的变形。那么chacha20就会有CTR的几个特性,例如明文-密文对应,加密-解密过程是同一个。而且如果每次nonce不变的话,对CPA的抵抗会有问题。(公司里有个场景正好是这种nonce不能变的)

另外,这个加密过程有几个细节。一个是可以用-a或者-base64开关来获得一个纯文本的结果(当然,代价就是增加空间消耗)。第二个是可以用-k来指定密码,用-kfile来指定密码文件,而不是现场输入。当然,这样做的代价就是可能会记入command history,或者有磁盘记录。最后一个是-z,可以在加密前先做一遍压缩。

相应的,解密指令就是

openssl enc -d -chacha20 < dst

另外,openssl还提供了以算法为基础的写法。例如chacha20的加密指令也可以写成这样。

openssl chacha20 < src > dst

大家举一反三,我就不罗嗦了。

摘要生成

先说一句,本章一般人不需要阅读。性子急的朋友请先看最后一段。

openssl用于摘要的方法主要是dgst。首先老规矩,我们先看有哪些摘要算法。

openssl list -digest-commands

在贝壳这里的机器上,算法基本有这么几类。blake2,gost,md4,md5,rmd160,sha1,sha2。不用说,md4/5,sha1都是不安全的。我查了一下,gost和原生ripemd也是不安全的。blake2,ripemd160,sha2还是安全的。所以推荐算法是blake2,sha2。具体来说算法就是blake2b512,blake2s256,sha224,sha256,sha384,sha512,第一选择是sha256。很可惜,没有sha-512/256。

然后我们就可以用来算hash了。例如

openssl sha256 < src

可以看到输出了吧。

OK,下面要说一个悲伤的事实。为什么我们没听说过人家用这个功能呢?因为linux的coreutils里面,有md5sum和sha256sum。我查了一下,支持blake2,CRC,md5,sha1,sha224,sha256,sha384,sha512。上面数的各种推荐算法,还有最常用的MD5,你都能直接用。不用苦逼的用openssl拼。

所以这一章,其实是废话来着。

RSA的生成和使用

RSA支持的功能比较全面,加解密,签署验证,还有验证还原操作。可以说是用途最广的一个算法族。

公私钥对的生成和管理

生成密钥

openssl genrsa 2048 > rsa.key

在openssl里,in和out经常和stdin和stdout有相同的含义。两者经常可以互换使用。例如上面指令,其实也可以写成openssl genrsa 2048 -out rsa.key。但是如果用stdout写出,会使得openssl无法控制权限(毕竟它不知道你要写文件)。所以,这样生成的密钥,权限为其他人可读。常规请用-out写出,比较安全。

查看密钥

openssl rsa -text -in rsa.key

可以看到很多数据,modulus,publicExponent,privateExponent,prime1,prime2,exponent1,exponent2,coefficient。具体意义可以在这里查看。

可以注意到,除了最基本的p=prime1,q=prime2,n=modulus,e=publicExponent,d=privateExponent外。openssl还额外保存了三个数,exponent1=d mod (p-1),exponent2=d mod (q-1),coefficient=(inverse of q) mod p。为啥我也不明白。关于prime1啦,prime2的详细解释,请看这篇

分离公钥

openssl rsa -pubout < rsa.key > rsa.pub

分离之后可以查看

openssl rsa -text -pubin -in rsa.pub

可以看到,只有modulus和publicExponent了。

另外,你可以把key加密或解密(很多场合下会用到)。方法如下:

openssl rsa -aes128 < rsa.key > rsa.enc
openssl rsa < rsa.enc > rsa.key

很多教程里会告诉你用-des或-3des,根据密码学常识你就知道,这是错的。idea也建议不要用,因此推荐用aes(优先)或者camellia。

加解密

数据加密

openssl rsautl -encrypt -pubin -inkey rsa.pub < src > dst

注意输出长度和位数相等(这里是2048)。

数据解密

openssl rsautl -decrypt -inkey rsa.key < dst > src.new
diff src src.new

注意公钥加密私钥解密。

签署验证

数据签署

openssl rsautl -sign -inkey rsa.key < src > dst

注意输出长度和位数相等(这里是2048)。

数据验证有多种方法,第一种是直接用rsautl

openssl rsautl -verify -pubin -inkey rsa.pub < dst > src.new
diff src src.new

注意私钥签署公钥验证。

另一种是用pkeyutl,注意这里有两种效果。

openssl pkeyutl -verify -pubin -inkey rsa.pub -sigfile sig < src
openssl pkeyutl -verifyrecover -pubin -inkey rsa.pub < sig

这里就是RSA系列sign算法比较特殊的地方。一般的sign都是验证sig和src是否具有对应关系,RSA的verify直接能解出原始数据来,这也算某种意义上的“验证了对应关系”。所以rsautl的verify,在pkeyutl的通配模式里,其实是verifyrecover。

ECC的生成和使用

ECC支持签署,验证和derivation。其中签署用的是ECDSA算法(很遗憾,不是EdDSA),Kx用的是ECDH。

公私钥对的生成和管理

ECC的生成比较特殊。在ECC里,你不止要设定一个长度,而是要选择一条曲线。因此第一步,需要列出所有支持的曲线。

openssl ecparam -list_curves

我这里的数据很长,具体不列了。但是可以看出几个特点。

  1. 不支持25519曲线。
  2. 某几条曲线不支持ECDSA。

随后,你可以选择一个曲线来生成密钥。例如我们选择secp256r1。

openssl ecparam -genkey -name secp256r1 > ecc.key

查看密钥

openssl ec -text < ecc.key

可以看到,ECC的数据就要比RSA简单的多,只有一个priv和一个pub。其余主要是说你用了什么曲线。

分离公钥并查看

openssl ec -pubout < ecc.key > ecc.pub
openssl ec -text -pubin < ecc.pub

其实很多同学看出来了,openssl在处理ECC时和RSA的参数基本是类似的,只是ec和rsa指令的区别而已。对于已经生成好的key而言,我们可以抽离具体的key算法,用一个比较通用的办法来处理公钥提取问题。

openssl pkey -pubout < ecc.key > ecc.pub

pkey指令也可以用于其他方面,例如加解密。具体就不赘述了。

签署和验证

ECC的签署和验证就要借助于pkey指令了,具体来说,是pkeyutl指令。注意,这里的形态和RSA的形态不一样。

openssl pkeyutl -sign -inkey ecc.key < src > sig
openssl pkeyutl -verify -pubin -inkey ecc.pub -sigfile sig < src

这里无法直接解出原始数据,只能验证得到是否正确的结果。ECC是不支持verifyrecover的。

derivation

我们先假定你生成了两对key和pub,随后你可以用这两对key和pub推导出一个双方共同的秘密。

openssl pkeyutl -derive -inkey ecc1.key -peerkey ecc2.pub

我们可以看到,这么生成出来的数据是一堆乱码。所以加上hexdump让输出比较可读。

openssl pkeyutl -derive -inkey ecc1.key -peerkey ecc2.pub -hexdump

我们也可以换一个顺序来生成。

openssl pkeyutl -derive -inkey ecc2.key -peerkey ecc1.pub -hexdump

可以看到,结果并没有差别。

key derivation的时候,双方互相给对方发一个pub。随后利用对方的pub和自己的priv计算出一个共享的s。攻击者虽然有双方的pub,然而无法得到s。当然,如我们在这里说过,Mallory永远可以通过拦截pub的发送过程来发出攻击。

DH的生成和使用

私钥生成

生成dh私钥

openssl dhparam -outform PEM -out dhparam.pem 1024

查看私钥

openssl dhparam -in dhparam.pem -text

注意,双方的p和g都是现场约定的,公钥A可以很快计算生成,因此都无需保存。

DH的私钥一般不用于加密和签署(你看,他连公钥都没有)。DH是一个Kx算法,因此DH的私钥只用于derivation操作。

derivation

根据pkeyutl的manpage,dh的私钥应当支持derivation操作。然而杯具的是,我实际测试openssl pkeyutl -derive -inkey dhparam1.pem -peerkey dhparam2.pem无法执行,不知道是不是因为dhparam都是priv的缘故。但是dhparam里确实没有生成公钥的参数。

无论如何,在nginx里,dhparam是一个重要参数。如果你使用默认的dhparam,会被警告不安全。

DSA的生成和使用

首先,DSA只支持签署和验证。

公私钥对的生成

和RSA非常像,但是有点区别。

openssl dsaparam -genkey 2048 > dsa.key

另外注意,dsaparam参数是不支持加密的。如果要加密,需要写成这个样子。

openssl dsaparam -genkey 2048 | openssl dsa -aes128 > dsa.key

同类,如果要读取内容的话,可以这么做。

openssl dsa -text < dsa.key

剥离公钥这么做。

openssl dsa -pubout < dsa.key > dsa.pub

我们把公钥和私钥分别打出来,可以发现,公钥的要素是pub,P,Q,G。私钥多一项priv。

签署验证

这是另一个奇怪的地方。根据pkeyutl的manpage,DSA的key支持sign(而且只支持sign)。可是我在实验openssl pkeyutl -sign -in src -inkey dsa.key -out sig的时候,又失败了。这个例子是直接抄的manpage,错误提示是Public Key operation error。

---------

 

openssl证书相关

鸣谢和前言

首先感谢Ivan Ristić。本篇很多内容可以从他的这本书里读到。大家如果有钱,可以买来支持一下。没钱但是英文好的话,也可以直接读一下(这本书在网上是公开的)。本文没有直接复制,引用,或是完全写成读书笔记。所以原则上是不受书的版权限制的。不过如果作者有异议的话,我愿意下线这篇文章。

First of all, I wanna say thank you to Ivan Ristić. I got a lot of help from this book. You can buy one if you want, or read it personally (it’s public). This is really a good book.

In this article (I mean the one you are reading currently), I don’t copy or reference directly from that book. So as far as I know, it shouldn’t have any copyright problem. But if the author (Ivan Ristić) have any problem, I’m willing to take this article down.

PKI证书体系结构

公钥体系的问题

在介绍证书体系之前,我们首先先回顾一下公钥体系解决了什么问题,又引入了什么问题。

在公钥体系之前,为了和某人安全的通讯,我们需要预先共享一个公共的秘密x。而且对每个通讯组合,都需要一个独立的x。如果你偷懒,在A-C里复用了A-B的x,那么C就有能力截听甚至篡改A-B通讯。那么,随着通讯组合的上升,独立的秘密x就会暴涨(数学很容易求出,一般期望是C(n, 2),复杂度是O(n^2))。这么大的数据量,存储和使用都很困难。公钥体系引入后,每个人只要保留自己的私钥,而通过公钥和别人握手。Eva即使能听到内容,也无法获得公共的秘密x。在这个模式下,只要每人一个公私钥对就行,数据量大大减少。

然而,这也引入了主动攻击者Mallory可以通过MITM攻击的可能性。因此,不仅需要给予对方公钥,更需要给予对方可信的公钥。这就是公钥体系需要解决的首要问题。

当然,可信给予公钥的最简单方法,就是离线向对方交付。但是你可以想象你要上所有网站,都需要物理的到他们店里,获取一份公钥的情况么?而且还要定期更新。这种模式在大多数场景下都无法工作,只能小范围内用于调试。

私钥-公钥-证书

前面我们已经唠叨了不少了,私钥签署公钥验证,这是基本模式。那么我们能不能通过对公钥进行签署来解决这个问题呢?发送公钥的时候同时发送签名,对方验证签名,就知道这个公钥是否被伪造了。

也行,也不行。如果对方要验证你的公钥,那么他就需要有签署者的公钥。而签署者的公钥又如何给你呢?这时线下模型就不是一个不可能的选项了。毕竟公司千千万,但是签署者不会太多。

但是仅签署公钥并不解决问题。毕竟如果不绑定到身份,那么攻击者也完全可以签署一份出来。好比A向CA申请签署了一个公钥,B也这么干了。C在获得公钥的时候无法分辨谁是谁,因为都是合法的签名。那么怎么办呢?于是在签署里,除了你的公钥,签名,还需要附加你的身份信息(尤其是域名——如果用于https通讯的话)。这样,虽然攻击者能签署出另一本证书,但是由于身份不符,所以无法发起MITM。而整个身份数据,公钥,各种元信息,加上整体签署,合起来就叫证书。

目前PKI方案,基本就是三级签署制。你的系统(或浏览器)里安装RootCA,RootCA签署Intermediate,Intermediate签署你的证书。这三层都是一个上级可以签署多个下级,从而形成一颗树。你可以查看一下Firefox的设置,安全,证书。里面会有所有的预装证书和部分的其他来源证书(如果有的话)。

之所以是三级方案,是因为根证书可能存在系统里长期不换。这种情况下万一泄漏了很麻烦。所以RootCA都是签署出一些中间证书,然后就把RootCA干掉,key放在冷存储里,扔保险柜里安全存放。然后用中间证书签署各种实际证书。这样万一中间证书泄漏,可以很容易的吊销。

证书和证书链

既然证书有签署顺序,验证就有验证顺序。

在大部分系统里,存在着信任根证书域,其中保存着所有的信任根。在使用的时候,对方会提供他的证书。在验证时,你会发现,啊咧,还差着Intermediate呢。

所以,在配置证书时,除了证书本身外,很重要的一点就是配置证书链(cert chain)。证书链里保存了多本证书,一般是从最上面一本根证书到最下面一本自己手里的证书,完整的一个链条。

以PEM格式而言(具体下面会说),证书链的做法就是把自己的Cert放在最上面,然后是Intermediate,最后是rootCA。后面的每一本证书都要能证明前面的证书,根证书可要可不要。分行复制进去,仅此而已。具体可以查看这里

证书的申请-验证-发行

那么,既然有签署机构,我们如何申请证书,这些机构又如何来认证你的身份呢?

首先,你需要找代理机构。是的,这些签署机构一般都不会亲自来办理全套业务,一般都是找代理商做商业运作。等你找到代理商,你需要做的第二件事情就是——交钱。毕竟这么大一堆机构,养人很花钱的呐。

等交了钱了,你就是他们的客户,有权找他们申请证书了。正常来说,申请证书的第一步是——自己生成一份私钥。是的,既然签署者只需要你的公钥,那么私钥他就不需要接触。很多非正规代理商是他们帮你生成的,请干掉这些代理商,换一家靠谱点的。第二步是将公钥和你的身份信息写入一个req文件,可靠的发送给代理商。第三步是代理商验证你的身份和你写在req里的身份是否一致。如果一致,他们会把你这个req发到一个特别的机器上进行签署。这个机器只能用于签署,上面的私钥是无法拷走的。最后,他们会把签署好的crt文件发给你——其实这步不发给你也行,crt文件是可以公开给全世界看的。只要丢到一个所有人都能拿到的地方,那也算是完成了签署。当然,发给你个人更像是一种交付。

可能有人看出来了,这里的核心就是“供应商如何验证你的身份”。一般我们用于通讯的证书都是用在HTTPS上的,有三种验证方式。

  • DV: 只要验证域名。对应的方法就是,给域名whois信息里写的邮箱发邮件,让你确认。或者是让你写入一个TXT的域名记录,以证明对域名有控制权。或者你已经配置了http服务的情况下,给特定的URL放一个特定的文件也行。
  • OV: 验证项目要多一点,要提供工商营业执照。
  • EV: 验证项目比OV还要多一点,一般要求企业提供DUNS。偶尔甚至有要求律师函或电话验证的。

申请流程

构造req

使用以下指令生成req

openssl req -new -key rsa.key -out shell.csr

基本来说,需要你填几个参数。国家,省份,城市,组织名(公司名),Unit名字(部门),Common Name,Email,最后还有一个可选的password。其中Comman Name是最重要的一个参数,这里填写的是你身份有关的数据,例如你申请的证书对应的域名。

生成req之后,你就可以发送给代理商了。当然,如果你要再看一眼,也可以举一反三类比办理。

openssl req -text -in shell.csr -noout

req除了能直接构造外,还能从现有证书直接构造。这对于续签证书很方便。

openssl x509 -x509toreq -in shell.crt -out shell_new.csr -signkey rsa.key

另外,req构造也可以用配置文件来进行。将以下内容存入req.cnf,再执行openssl req -new -config req.cnf -key rsa.key -out shell.csr,其他没有区别。CN对应上面的Common Name,emailAddress对应Email,O对应Organization Name,L对应Locality Name,C对应Country Name,都很好猜。如果你想要看详细解说的话,可以看这份文档

[req]
prompt = no
distinguished_name = dn
req_extensions = ext
input_password = PASSPHRASE

[dn]
CN = www.feistyduck.com
emailAddress = webmaster@feistyduck.com
O = Feisty Duck Ltd
L = London
C = GB

[ext]
subjectAltName = DNS:www.feistyduck.com,DNS:feistyduck.com

注意最后的subjectAltName,这个是用于签署多个域名的。一本证书可以签多个域名,在正常的req -new构造里,是看不到这个选项的。

签署

自签证书

几乎所有讲openssl签署的文章,第一课都是自签证书。自签证书的基本指令大概是这样的:

openssl x509 -req -days 365 -in shell.csr -signkey rsa.key -out shell.crt

开关中,-req指定输入可以是一个csr(否则按照x509的默认假定,会出错)。shell.csr是上面构造出来的那个证书申请。注意-days,这个参数在这里出现(而不是在证书申请中),说明证书有效期是由签署者现场指定的。在req指令中是可以加入-days参数的,但是其意义为,在指定-x509参数的情况下(下文说明),一同指定天数。实际上还是签署时指定有效期。

如果你打算一行中完成整个签署流程,可以这么做。

openssl req -new -x509 -days 365 -key rsa.key -out shell.crt

或者是更简短的形式

openssl req -new -x509 -days 365 -key rsa.key -out shell.crt -subj '/C=CN/L=SH/O=home/CN=*.shell.org/emailAddress=shell@shell.org'

其中-x509指定生成一个自签证书,而不是证书申请。-subj是证书的subject部分,在x509 -text看证书时能看到的。格式照上面抄就行。注意-subj对req也有效。

alternative name

建立一个文件shell.ext,内容如下:

subjectAltName = DNS:*.facebook.com, DNS:facebook.com

然后将这个文件加入到签署指令中去:

openssl x509 -req -days 365 -in shell.csr -signkey rsa.key -out shell.crt -extfile shell.ext

你就可以得到一个带有alternative name的cert:

openssl x509 -in shell.crt -text -noout

注意,extfile指令对req不生效。req要增加alternative name,只能用config文件。

扩展属性

// TODO: 将来有空再写吧

多种证书格式转换

这节我也不是很擅长,所以基本就是抄的cookbook。

证书格式

证书格式比较复杂,常见证书格式有以下几种:

  • DER cert: 二进制格式,DER ASN.1编码。
  • PEM cert: 平文本格式,base64过的DER数据,以—–BEGIN CERTIFICATE—–开头(openssl的很多文件都有类似的boundary)。
  • DER key: 二进制格式,DER ASN.1编码。
  • PEM key: 平文本格式,base64过的DER数据。
  • PKCS7: RFC2315定义,一般叫做.p7b或者p7c文件。
  • PKCS12: 可以同时在文件内存入证书链和私钥,一般叫.p12或者.pfx。

PEM和DER互相转换

不就是base64互转么。基本思路是这样。

openssl x509 -inform PEM -in shell.crt -outform DER -out shell.der

原理很简单,自己看看就行。

PKCS12

这个就比较复杂了。首先给出PEM的cert和key转换为p12的代码。

openssl pkcs12 -export -name shell -out shell.p12 -inkey rsa.key -in shell.crt -certfile shell.crt

上面打包了三个文件,一个key,一个crt,一个上位证书。我的上级证书就是自己(自签的)。导出的时候需要输入一个密码。

反过来就比较麻烦了。如你所见,反过来比较类似于解压。如果要把所有内容塞到一个文件里去,可以这么做。

openssl pkcs12 -in shell.p12 -out shell.pem -nodes

开关-nodes的意思是no-des,不要给key做加密的意思,不是node-s。

如果你要分别解开,需要用-nocerts,-nokeys -clcerts,-nokeys -cacerts三个开关。三次输入密码,回车。

openssl pkcs12 -in shell.p12 -nocerts -out shell.key -nodes
openssl pkcs12 -in shell.p12 -nokeys -clcerts -out shell.crt
openssl pkcs12 -in shell.p12 -nokeys -cacerts -out shell-ca.crt

我所熟知的范围内,需要用到PKCS12转换技巧的地方,只有Debian的SSO方案。那是一个用户端证书方案,是由Debian生成一张证书,然后由用户自己导入浏览器内部的。生成的证书是PEM格式,连同key一起,转换为p12文件,手工导入chrome里。

PKCS7

从来没用过,不花时间了,直接抄指令。

openssl crl2pkcs7 -nocrl -out fd.p7b -certfile fd.crt -certfile fd-chain.crt
openssl pkcs7 -in fd.p7b -print_certs -out fd.pem

算法选择和TLS配置加固

前面讲了一堆算法,可以想见,在TLS中用的不会是某个固定算法,而是在多个算法中选择一个。那么理所当然,为了兼容旧的系统,OpenSSL中可能带有很多旧算法(甚至还在使用)。那么这些旧的算法就可能成为一个漏洞。例如MITM时强制降级到旧的算法,用已知的漏洞去攻击。因此,OpenSSL允许用户自行选择算法族。更大的算法族提供更好的兼容性,但是安全性上就有不足。禁用老式算法可以提供额外的安全保证,但是可能老的系统就无法连接。

查看系统中的所有算法

用这个指令查看系统中所有算法:

openssl ciphers -v

上面的指令可以跟关键字来做查询。当然,如果只是查找的话,我一般习惯是用grep。上面的指令一个关键用途是,对下面给出的算法最佳配置列出当前系统中的可用算法组合。

列出一堆算法,抓几个关键点。Kx是密钥交换(Key eXchange),Au是验证(Authentication),Enc是加密(Encryption),Mac是消息签名。每一个名字,其实都是几种算法的组合。其中还要排除掉一些冲突无法组合或者无需组合的情况。例如AESGCM就没有MAC,因为GCM是AEAD的,带有验证功能。

要知道更细节的意义,可以看cookbook的对应章节。cookbook里也有很多东西没有,例如CHACHA20系列算法,在cookbook里只在下面配置的注释里面提到了。不过大家既然都看过加密相关的章节了,这部分内容想想就应该能明白,所以不赘述了。

算法配置的最佳实践

关于openssl配置的最佳实践,我建议还是读ssllabs的指南nginx的ssl配置。全文读一读。

简单来说,TLS的配置分为两个部分,协议(protocol)和算法(cipher)。协议分为SSLv2/SSLv3和TLS1.0/1.11.2。TLSv1.3最近才发布(1.2发布于2008年,1.3于2018年),所以暂时略去不提。在这些协议中,SSLv2绝对不可用(DROWN attack),SSLv3建议不要用(POODLE attack)。TLS1.0也有弱点(BEAST attack),但是由于客户端仍然广为使用,需要做兼容的同学可以开启。TLSv1.1和TLSv1.2是安全的,但是1.2提供了更多的现代密码算法(主要是AEAD类)。

顺便一提,我看到的1.3最大的好处,一个是握手的轮数减少了。传统TLS握手需要两次往返。1.3只要一轮。去除了一些不安全的算法(Camellia居然也是不安全的?好吧,大家还是用aes128好了)。大家要知道具体细节的话,可以看这里

算法的选择就比较复杂。你首先需要考量对以前版本的兼容性。例如BEAST attack还是RC4 attack。这个问题的细节可以看nginx的ssl配置中,The BEAST attack and RC4一节。也可以看这份文档。个人觉得还是后面这份写的明白点。简单来说,两个坑你总要中一个。趋势上,更多的人中RC4问题。如果你要追求安全,还可以同时关闭TLSv1.0和RC4。当然,缺点就是兼容性问题。

nginx的ssl配置给出了很实用的算法选择指南。简单版本中,算法选择只有四类:EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH。也就是说,Kx算法中只有两个安全的,EECDH和EDH。Enc算法中也只有两个安全的,AESGCM和AES256。

根据我们前面的密码学知识,我们知道EECDH和EDH分别是前向安全的Kx协议中,对应DLP和ECDLP问题的两个实现。最前面的E表示这个实现的参数每次都是临时产生(严格来说只有带E的算法才是前向安全的)。AESGCM是AEAD算法,AES256是非AEAD算法。之所以有后者还是考虑了兼容性问题的。然而如果你在最新的openssl上执行这个设定,你会看到Mac中有个东西跳出来,SHA1。

这是因为这个配置写在2015年,还没有考虑到Google的SHA1碰撞实例。而且即便是Google的例子中,要碰撞一个实例也是非常费力的。所以从实证角度讲,我不觉得有人能在TLS流程中构造出一个可行的Mac碰撞。但是既然有替代品,去掉SHA1的成本很低,没必要继续使用。所以在近代,要用这个配置的话,需要写成这个样子。

ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH:!SHA1';

可用算法总计16种。

复杂版本是这个样子的:EECDH+AESGCM:EDH+AESGCM:ECDHE-RSA-AES128-GCM-SHA256:AES256+EECDH:DHE-RSA-AES128-GCM-SHA256:AES256+EDH:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4。注意,如果你要在bash中执行这个配置,必须使用”,而不是”“。因为有!的存在。

根据我们的密码学知识,我们知道这个配置禁用了几个不安全的算法。DES,MD5,PSK,RC4(上面有说)。但是这个配置还漏了不少东西,SHA1,RSA(as Kx, not aRSA)。然而这个配置却不能简单的干掉SHA1,因为SSLv3的所有Mac算法都是SHA1的。如果你干掉SHA1,等于禁用SSLv3(虽然强烈建议这么做)。至于RSA,是因为不满足前向安全性,好像是今年新出的提示。所以复杂版本至少要长这样:

ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:ECDHE-RSA-AES128-GCM-SHA256:AES256+EECDH:DHE-RSA-AES128-GCM-SHA256:AES256+EDH:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4:!RSA';

我这里是53个组合。如果更进一步关掉SHA1,则只剩下35个组合。

另外需要说明的一点(网上搜到的大多数教程都不会说),在OpenSSL v1.1的某些版本里,支持了CHACHA20。理论来说,你可以将CHACHA20的支持加入到你的配置里去。这个算法更快(尤其是对CPU不支持AESNI的系统,例如很多手机)。

ssl_ciphers 'EECDH+CHACHA20:EDH+CHACHA20:EECDH+AESGCM:EDH+AESGCM:EECDH+AES256:EDH+AES256:!SHA1';

我的版本是1.1.0f,CHACHA20在里面总共有7种算法组合。其中只有三种是新的算法族合用的。所以如果没问题的话,上面的配置应该有19种算法组合,而且CHACHA20优先。

如果你觉得配置OK了,可以去这个网站检测一下自己网站的安全性。如果你的配置正确,上面的ciphers应该不会出现任何ciphers方面的问题。不过整体得分可能并不高,ciphers只是长征的第一步。

其他安全加固

首先是最重要的,SSL一定要升级到最高版本,能多高多高。现在都2018年了,我还能在线上看到heartbleed漏洞。如果还有这种级别的漏洞,就不要提什么安全加固了,OK?

然后,保证在nginx里有这两行配置:

ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;

session cache听起来是一个提升性能的配置。

第三点是打开HSTS。一般浏览器会在输入域名的时候自动补全http://,服务器会在http的80端口上收到请求,然后301到https上继续访问。一般来说这样没什么问题。但是攻击者可以MITM之前的http过程,使其不定向到https,而是定向到攻击者Mallory。Mallory用https向上游访问。在这个过程里,客户根本不知道服务器有TLS保护。

HSTS通过特定配置挫败这个攻击。在用户首次访问网站的时候,HSTS会增加一个Header: Strict-Transport-Security。这个Header指明当前网站多久内不应用http去访问。在nginx里,可以这么配置。

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";

在用户首次访问网站过后,浏览器就会缓存这个设定。再次访问的时候就不会被MITM了。当然,这个防御并不能应付首次攻击。

第四点是增加DH的复杂度。OpenSSL的默认复杂度是1024,我们至少应该增加到2048,推荐4096。因此需要先生成dh参数:

openssl dhparam -out dhparam.pem 4096

随后在nginx中加入这行:

ssl_dhparam /etc/ssl/certs/dhparam.pem;

最后一点就是,有条件的话,开启OCSP stagling。

ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;

这条之所以在最后,是因为这条是性能优化,不是安全加固。

自建CA

所谓自建CA,就是自行建立一个CA根,为下属签署证书。比较简单的自建CA模型往往是CA-cert,复杂的甚至可以做到CA-Intermediate-cert(和目前各大证书代理机构结构相同)。

自建CA,最简单的方法是使用easy-rsa项目。

setup

首先你需要获得一份easy-rsa,我用的是Debian stretch中自带的2.2.2-2。你也可以直接去上游下载一份,我看到的最新版本是3.0.4。获得后,复制文件到一个临时目录,而后的生成之类行为都是基于这个目录的。你需要妥善保管整个目录(主要是保管keys目录,vars文件丢了可以重新编辑),保证不丢失和不泄密。

在完成复制后,首先需要设定一些环境变量。编辑vars文件,你可以看到一个bash脚本,里面有很多环境变量。一般来说,你只需要修改最下面’KEY_COUNTRY’开始的那些内容,包括国家,地区,城市,组织,邮件地址之类的。随后source vars引用文件,使得变量生效。后续所有操作都是假定变量生效的,就不一一重复了。

在引用文件后,你会看到提示。一个是openssl.cnf文件找不到。你可以看一下,目录里有openssl-0.9.6.cnf openssl-0.9.8.cnf openssl-1.0.0.cnf三份文件。随便链接一份到openssl.cnf就行。这种情况下,正常人都会链接openssl-1.0.0.cnf吧。另一个提示是,如果执行./clean-all,会清除全部文件。实际上,由于我们什么都没建立,因此不用担心,大胆执行。执行完成后,可以看到生成了一个keys目录,带有几个文件。后续所有的私钥和证书生成都是在这里完成。

准备的最后一步,是生成一份CA。执行./build-ca,你可以看到keys目录下出现了一份crt和key。其中crt是rootCA,可以用于分发。key需要谨慎保护。如果这份key泄漏了,那么攻击者就可以任意签署证书了。crl并不能解决这个问题,唯一的补救措施是干掉整个CA树,重新生成CA,并为所有人重签署证书。近代PKI体系中为了规避这个问题,采用的是CA-Intermediate-cert结构。

sign

很简单,./build-key common_name。如果是域名,照写域名就行,但是泛域名最好不要直接写进去,在输入CN时修改一下。因为cn同时会作为文件名,文件名里带有*会比较麻烦。如果没问题的话,最后可以看到问你要challenge password。这个东西,根据我查到的资料,应该是在吊销时需要的。然而我吊销时也用不上,貌似是个废物。不要设定就好了。最后是两个y,确认签署。

签署完成后,在keys目录中就可以看到很多新东西。*.old是备份,不用管。test1.*是刚刚生成的证书,crt是证书,csr是req,key是私钥。build-key这个脚本,其实就是自动生成key,根据你的要求生成csr,最后自动用ca签掉你的csr。但是注意,这里生成的crt,头部包含了-text的全部输出。在作为crt使用的时候建议删除,并把ca.crt合并进去(具体解释见后)。

如果你有印象的话,会发现目录里会多出几个东西来,这些基本都是和序列号有关的。PKI要求,同一个CA必须为签署的每本证书配备一个对应的序列号(Serial Number),序列号必须唯一。你用x509指令看一下刚刚生成的crt文件,可以看到sn为1。此时查看serial文件,可以看到内容为02。serial文件就是下一个可用的sn。

理解了serial,index.txt也简单了。这就是一个签署记录表,记录了当前CA签署的每个crt的sn,subject,还有当前状态什么的。有了这些数据,未来才能进行对应的吊销。最后就是01.pem,这个是基于序列号的命名,内容和crt一致。

附带讲解一下,build-key-pass可以生成一份带密码的私钥,build-key-server可以生成服务器端证书,build-key-pkcs12可以生成pkcs12证书。这些比较简单,就不罗嗦了。

sign with req

还记得我们说过标准CA的过程是,签署过程中不应该帮你生成私钥。那么我们来模拟一遍客户自己生成私钥的过程吧。

客户自行生成私钥和req的细节可以看上面,也可以直接使用easy-rsa的build-req脚本。同比,使用./build-req cn,经过类似过程,会生成一些文件。但是你仔细看的话,只有key和csr,并没有crt。这是因为这步是在客户手里做的,csr会被发送给服务方签署。

服务器端也很简单,拿到文件后,用./sign-req cn来签署。把crt发还给客户就行。

revoke

这个也不难,./revoke-full cn就行了。例如我吊销test1,完成后看一下index.txt,01那行被改成了R。openssl crl -in keys/crl.pem -text -noout(其实直接./list-crl就行)看一下,吊销是成功的。再吊销test2,结果一样。

关键是crl的生效方式。吊销颁发给客户的证书,crl需要被放到服务器上读取。吊销服务器证书,crl需要发给所有客户。一般而言,后者在技术上不具有可行性。所以往往会为证书里签上一个CRL URL,把CRL放在某个URL里完事。

但是这又带来另一个问题。随着PKI体系和https的推行,大量的公司签署服务器证书,也有大量公司被吊销证书。客户端获得CRL的时候,需要获得完整文件,庞大的问题造成了严重的性能问题。因此才酝酿了OCSP。OCSP每次只获得一条记录。

intermediate

这是我用的最少的一个功能,一般用easy-rsa的人根本不会搞什么intermediate。

签署没任何困难,./build-inter cn就完了。然后我们用openssl x509 -in keys/int1.crt -text -noout仔细看一下证书,你会发现,这本证书的X509v3 Basic Constraints很有趣,是CA:TRUE。

然后要怎么用呢?

首先,你要再搞一个easy-rsa目录,重新编辑vars文件(内容可以一样,也可以不一样),重新source生效,用clean-all生成基础环境。这些都和setup没区别,但是后面不要build-ca,用./inherit-inter ../easy-rsa/keys/ int1,其中../easy-rsa/keys/是前面你签署证书的keys目录,int1是前面刚刚签署出来的intermediate证书。

完成后,前面一个证书体系签署的intermediate就会变成你新目录的ca,而前面的crt和当前的crt会合并成一个export-ca.crt。这是你的证书链文件,发布的时候使用这本。其他和正常的PKI体系没有任何区别。

证书购买中的一些细节

  • EV没有泛域证书: 虽然是常识,但是还是有人不知道。