从数字签名到数字证书, All I write is now ……
吖,终于放假了,睡了两天的充满电键者今天勤奋的爬起来码字了,马上春节,正文前先拜个早年,祝各位童鞋新年快乐,心想事成。
最近键者打算在项目中使用基于TLS
的认证体系,来管理服务
之间的相互认证问题,于是开始研究怎么实现并管理自签名的证书,于是牵扯到了键者常年黑箱的数字证书,所以……今天我们来聊聊数字证书吧☺️
作为常年的黑箱攻城狮,终究还是逃不过开箱的时候,也许就像薛定谔说的,不打开箱子,怎么知道箱子里的猫是活着还是死了呢……
虽然,可能打开了也不知道……
快照:数字签名
以前对接微信的开发者接口时,需要对请求的业务进行签名,微信会提供给键者一段字符串,键者每次起调微信的接口时,均需要将所有请求参数及参数值,按照参数名开头字母顺序排序,然后将这段字符串附加在排序后的请求之后,并用微信指定的哈希函数进行一次哈希计算,将哈希结果与其他请求参数一同发给微信的接口。
// 充满真诚的伪代码
req := "更新"
user := "大悦天"
secret := "HelloWorld"
// 准备用于签名的字符串
strToSign := fmt.Sprintf("req=%s&user=%s&secret=%s", req, user, secret)
signedStr := Sha256(strToSign)
// 发送给服务端的内容
strToSend := fmt.Sprintf("req=%s&user=%s&sign=%s", req, user, signedStr)
基于哈希函数不可逆的特性,微信只要将收到的请求参数用相同的规则排序,并使用同一段字符串再做一次哈希,并将哈希结果与键者提供的进行比对,就可以确保请求是否真的是键者发出的。
前文用到的那段字符串
,从职能上看,就是密钥
;而根据这段请求
和密钥
生成哈希结果的过程,就是签名
,而这个哈希结果,自然就是数字签名
。
其实这里还巧妙地实现了在如何避免在信道上传输用户的
密钥
的情况下,验证用户的身份。
用户在参数中表述自己的身份(user
),及请求的详细内容(req
),再用自己的密钥
对所有请求参数进行签名,将签名结果提交给服务端;服务端根据用户身份拿出自己存储的那份密钥
,就可以计算并判定签名是否正确,然后再根据用户的权限设置,选择拒绝或者接受并执行相关的操作。
当然,工程上我们一般还会设置一个随机字串作为盐值,并加入当前的时间戳,以确保哪怕请求的内容一样,每次产生的
数字签名
结果都不一样,防止重放攻击。
简单小结一下:一般来说,数字签名
的核心技术在于哈希函数的不可逆性及足够高的结果唯一性。
漫谈:数字证书
那么通过数字签名
,可以确保通讯双方的安全么?很遗憾,并不能,显而易见的一个问题是,数字签名
可以确保请求方发出的请求没有被中间人篡改,通过恰当的设计还能一定程度上避免回放攻击,然而前文中的每个参数,仍然是公开的,也就是说,它并不能防止内容被窃听。
理论上,通讯安全需要防止三重威胁:窃听风险、篡改风险、冒充风险。
那么这个时候,很自然而然地,就会想到对内容进行加密咯?
相信各位童鞋肯定都对计算机中
对称加密
和不对称加密
两种加密体系的特点有一定了解,键者在本文就不啰嗦了。
我们先来考虑对称加密
的思路,服务端和客户端事前协商一把对称密钥
,客户端对请求内容先进行一次加密,服务端收到请求后,先用对称密钥
对请求进行一次解密,然后再校验数字签名
……
假设服务端仅使用一把对称密钥
,那么所有客户端都知道这把对称密钥
,倒也能完成通讯的需求。可是密钥这东西,知道的人越多,泄漏的可能性就越大,对于管理来说也不是一个可靠的方案,一个客户端的管理不善会波及所有的客户端,这个风险成本太大了,并不是一个可靠的方案。
那如果给每个客户端都分配一把对称密钥
呢?也会有问题,每个客户端发来的消息都是加密过的。也就是说,服务端无法直接从客户端发来的消息中得知,这个客户端是哪个用户,也就无法选择合适的对称密钥
对内容进行解密,除非用所有客户端的对称密钥
“轮询”一遍……
所以根据电视剧的常规套路,这里先出现的
对称加密
一般都是炮灰的节奏?
那么是时候引入传说中的不对称加密
了,加入服务端自己生成好一对不对称密钥
,然后将其中一把自己严加保管(即私钥
),另一个把公开给所有希望与自己通讯的客户端(即公钥
),则所有的客户端都可以通过公钥
加密自己的请求,且该请求只有持有私钥
的服务端才能解开,那么通讯的安全就算是有保证了……吗?
可惜后来就是一个先有鸡还是先有蛋的问题……
如果是完全私有的业务中,我们可以假定,客户端拿到的公钥
总是可靠的,毕竟站在计算机的角度来讲,也许每个运维人员都是上帝,不信仰运维的服务器都会下火狱……
除非出
Bug
了,出Bug
的时候服务器是大爷?
可惜,我们并没有合适的渠道能确保我们获得的公钥
总是可靠的。同样,随着新服务的增加,我们也难以及时地获取最新的公钥
。
于是证书中心(CA
)应劫而生,定位上,CA
是客户端和服务器都信任的第三方,由CA
对公钥提供担保。服务器只要持有CA
签发的证书,就可以向客户端证明自己的身份。客户端只要能验证服务器持有的证书确实是CA
签发的,就可以该服务端是可靠的。
现实中,公共业务的
CA
证书一般都是根据操作系统或者浏览器直接发布到客户端设备的,一般来说,我们可以认为这个渠道已经是比较可靠的了。
具体实现上,CA
需要自己先生成一对密钥,保管好自己的私钥(ca.key
),公开自己公钥(ca.pub
)。
而客户端需要生成一份对自己身份的描述文件(cli.csr
),并附带自己的公钥(cli.pub
),其内容可能是这样的:
账户: '大悦天'
公钥: '大悦天的公钥'
通过CA
认为可靠的渠道(例如,当面……)交给CA
,CA
再用ca.key
对这份文件进行加密,并将加密后结果返回给客户端。此时,这份加密后的结果就是传说中的数字证书
(cli.crt
)了。
这个身份描述文件的官方称呼是
证书签名请求
,缩写为CSR
。
那么这份数字证书怎么工作呢?首先,ca.pub
是公开的,也就是说,任何人拿到这份cli.crt
的时候,都可以通过ca.pub
解密证书,获得身份描述文件。但是由于ca.key
是被严加保管的,也就是说,除了当事人,没有人能伪造一份公钥
不同的证书。
则客户端生成请求的时候,除了附带数字签名
以外,同时应使用cli.key
将请求内容加密,将加密后的内容与cli.crt
一起发给服务器。
注意,这里的
数字签名
=Encrypt( HASH($req), $cli.key ),而不是前文的方式,但本质没有区别,只是不同的实现思路而已……
服务器收到了请求以后,首先通过ca.pub
解密cli.crt
,则从结果中可以拿到可靠的cli.pub
,再通过cli.pub
解密请求正文,就可以拿到请求的内容和数字签名
了,根据数字签名
可以验证请求的内容是否被篡改,并执行后续业务。
为什么说这里的
cli.pub
是可靠的?
这块涉及到的实体有点多,键者在这里简单小结一下:
// 又是充满真诚的伪代码
var ca.key, ca.pub
var cli.key, cli.pub
var cli.csr = `ID: 大悦天`
var cli.crt = ($cli.csr + $cli.pub) + $ca.key // 使用私钥对csr和pub分别加密
// 以下是客户端请求过程
var cli.req = `
ID: '大悦天'
Action: '更新',
`
// tips: sha1已被证明不安全,工程上请选用sha256或以上级别的哈希算法。
var cli.hash.req = HASH( $cli.req )
var cli.encrypt.req = $cli.req + $cli.key // 加密
var cli.encrypt.hash.req = $cli.hash.req + $cli.key // 加密
cli.Send( $cli.encrypt.req, $cli.encrypt.hash.req, $cli.crt)
// 以下是服务端的验证过程
var cli.csr, cli.pub = $cli.crt - $ca.pub // 描述的是解密过程
var cli.hash.req = $cli.encrypt.hash.req - $cli.pub // 解密
var cli.req = $cli.encrypt.req - $cli.pub // 解密
// 服务端根据req重新算一个哈希值
var cli.srvHash.req = HASH( $cli.req )
// 将csr中的身份与req中的身份进行比对
if cli.csr.ID == cli.req.ID {
// 名副其实
}else{
// 名不副实
}
// 将收到的哈希值与自己根据请求重算的哈希值进行比较
if $cli.hash.req == $cli.srvHash.req {
// 认证通过
println("虽然认识你,但就是不更新=。=")
}else{
// 认证失败
println("不认识你,不更新……")
}
事实上,认证的需求是相对的,前文描述的流程是客户端向服务端证明自己身份的思路,而替换一下身份,就可以变成服务端向客户端证明自己身份的场景。后者在实际应用中,就是HTTPS
业务中最常见的场景,服务端向客户端证明自己真的是服务端
,而不是伪造的中间者。
同样,特定场景中还会有服务端客户端双方都需要认证对方身份的情况,其实也是键者实际业务中真正需要解决的问题,不过先在此按下不表。
抛砖之言
就键者个人的愚见,从工程的角度来说,无论是先有鸡,还是先有蛋,并不妨碍键者今晚吃鸡,啊不是,应该是后续状态的变化。就像我们不知道世界的起源,并不影响我们好好的活在当下。探究起源是有意义的,活在当下也是有意义的,两者并无分高下。
放在前文的场景,就是我们信任某个公钥也好,不信任某个公钥也好,我们终归总是要先信任一些东西。放在数学上,这叫公理。放在宣传中,这叫“我认为下面这些真理是不言而喻的……”。
而密码只要存在,就有被破解或者泄露的危险。
——鲁迅《我没说过》
所以在键者看来,引入CA
的最大价值并不是解决了先有鸡还是先有蛋的问题,而是更好的解决的多对多认证的问题。
当然定义好初始规则仍然是很重要的,就像盒子不管打不打开,首先,要有猫……
虽然前文描述的都是一对一的场景,但实际上,一个CA
可以签发任意数量的证书,一个客户端也可以生成任意数量的CSR
,一个CSR
甚至也可以被任意数量的CA
进行签名,每次签名都会生成由该CA
认可的证书。同样,一个服务端也可以信任不止一个CA
,只要持有该CA
的公钥即可。 一个CA
还可以授权给子CA
,形成信任链。
所以,数字证书
的业务本质是仍然像许多其他引入三方的设计一样,将认证
的工作委托给第三方完成,无论是服务端,还是客户端,都可以更专注的完成自己的业务,同时避免关注到对方的存在。特别是面向微服务
的场景中,自建证书中心
有非常大的应用价值,增加服务时候,不需要逐个向已有的服务添加新的服务的访问密钥,而是通过证书中心
签发证书的方式,即可实现动态扩容。
其实还涉及到证书过期的或者撤销的问题,但键者今天的血槽已经空Orz……
附录:关键字
数字签名:Digital Signature
数字证书:Digital Certificate
证书中心:Certificate Authority,CA
签名请求:Certificate Signing Request,CSR