加密和签名技术分析
2017年11月22日


早在2013年,我就设计了 开放平台,那时参考了 新浪开放平台 \腾讯\百度\淘宝\支付宝\豆瓣 开放平台,并研究了 OAUTH(1.0和2.0)

最终第一版采用的OAUTH 1.0实现,第二版采用OAUTH 2.0实现。


但是有一个疑问,当时没弄清楚,就是 “为什么 支付宝用的RSA加密和签名,而新浪、豆瓣等用的AES加密、SHA1签名?”


现在,我又深入研究了一晚上,终于想明白了,下面从头说起。


一、关于加密算法


只谈AES算法和RSA算法,其他的都不讨论,比如DES,已经过时了。


问:AES和RSA算法的区别是什么,哪个安全性更高?

    AES属于 对称加密算法,加密和解密用的密钥是一有的。

    而RSA属于 非对称加密算,加密和解密过程使用不同的密钥。公钥即为公开的密钥,一般用作加密;私钥即为私有的密钥,一般用作解密。

    从安全性角度比较,以目前的科技水平来看,RSA和AES都很难破解,可以认为是足够安全的。从算法角度来比较,AES 256 的安全性比 RSA 1024 的安全性还是要高得多,而且AES算法的计算速度比RSA要快得多。RSA太慢了,以至于不适合对比较大的数据进行加解密。

    但从应用角度来看,RSA和AES各有各的用途,AES要向使用者公开密匙,是有很大安全隐患的。而RSA则只需要对使用者公开公钥,私钥不对外公开,但是由于RSA运算速度很慢,所以一般只用于签名等数据很少的情况。


AES算法,常用的两种模式:CBC 和 GCM。


据说从理论角度来看,GCM更有优势,应该是未来的主流趋势。而现在的主流,是CBC模式。


亚马逊云 AWS,默认采用的就是  AES_256_GCM (全称:ALG_AES_256_GCM_IV12_TAG16_HKDF_SHA384_ECDSA_P384),安全性非常高。但是只提供了 Java和Python的 client,而且我没搜到有其他语言的第三方实现。


而 AES_256_CBC,应用更为广泛,微信开放平台就是用的它,而且提供了 Java、PHP、Python、C#、C++ 五种语言的官方实现,而且第三方的实现还包括 Go、NodeJS等。总之,这个算法实现较容易,各个语言对它的支持非常广,使用方便。


综上,我目前还是推荐 AES_256_CBC,我认为强度足够了,关键是方便。


AES算法,是对称加密算法,加密、解密共用一个密匙。

而RSA算法,是非对称加密算法,它有一对密匙:公钥用于加密、验签,私钥用于解密、加签。所以,RSA算法用于加解密时,可以把公钥直接公布出来,对方拿去加密,而私钥只有一份在自己手上,只有自己能解密,这个特性非常有用!本文后面会看到。

然而,遗憾的是,RSA算法只能用于很短的数据加密,以1024位key为例,最多只能加密127位数据。



二、关于签名算法

常见的签名算法是 MD5、SHA1、SHA256,其他的本文不谈。


所谓签名算法,就是可以根据数据,计算出一个长度固定的内容,只要数据有任何改变,算出来的值都不一样。


MD5算法安全性较弱,比较容易被暴力破解。但是MD5签名的速度很快,性能好。可以用于不是特别注重安全性的场景。


SHA1,可以作为MD5的替代者,它的性能也不错,而且几乎不会被破解。而SHA256,强度更高。


问题:消息摘要(MD-Message Digest,例如MD5、SHA256等)和消息认证(MA-Message Authentication,例如HMAC-MD5、HMAC-SHA1、HMAC-SHA256等)的区别是什么?

    MD是用来防篡改的,而MA是用来核对身份的。

    比如一个镜像文件,计算其MD5值,如果相同则没有被修改。但是如果进一步要求,这个镜像文件必须是从A网站下载的,这是就要使用MA进行认证,使用者根据key计算MA值,认证通过后就可以确认这个文件是从A网站下载的,而且没有被篡改。

    再比如你和对方共享了一个密钥K,现在你要发消息给对方,既要保证消息没有被篡改,又要能证明信息确实是你本人发的,那么就把原信息和使用K计算的HMAC的值一起发过去。对方接到之后,使用自己手中的K把消息计算一下HMAC,如果和你发送的HMAC一致,那么可以认为这个消息既没有被篡改也没有冒充。


三、签名和加密结合的算法

常用的是 SHA1WithRSA、SHA256WithRSA,即 用SHA算法进行签名,再用RSA算法进行加密。最终得到的一个密文的签名。



四、常见应用案例 - 加密加签、防重放攻击


1、AES和RSA组合使用,发挥各自的特点

    为了避免AES Key在网络上明文传输,先动态生成AES的 key,然后对数据进行 AES 加密。然后用 RSA 公钥加密 AES Key。最后将加密的数据和aesKey一起传给接收方。

    接收方用RSA 私钥解密AES Key,然后用解密后的AES Key对数据进行AES解密。


2、AES和SHA组合使用,发挥各自的特点

   与上面的RSA有所不同,但是利用RSA有异曲同工之妙。方法是,双方都保存同一个token(可以理解为密码),然后用token和所有参数一起利用SHA算法计算一个签名,接收方利用自己本地的相同的token和接收到的参数一起也计算一个签名,签名一致则代表数据没有被修改。为保证数据不明文传输,也可以用AES进行加密,但是AES的密匙需要事前配置好,双方都用同一个AES Key。


综上,不难看出这两种主流方式的区别。应该说特点不一样,各有优劣。AES+RSA方案,只需要客户端配置一个RSA公钥即可,用于加密AES Key。而AES+SHA方案,需要客户端配置aesKey和token。并且注意到,后者连签名一起做了,而前者只有加密,没有签名,如果要签名,则还需要引入SHA1-RSA算法,再提供一对公钥密钥。


3、两个方案的具体分析以及如何防重放攻击

AES和RSA:【发送方】操作步骤

    1. 随机生成AES密匙(aesKey),用以对明文数据(clearData)加密,加密后的数据为encryptData。

    2. 用接收方提供的RSA公钥(receiverPublicRsaKey),对AES密匙进行加密,得到encryptAesKey。

    3. 用自己的RSA私钥(senderPrivateRsaKey),对密文数据(encryptData+encryptAesKey)进行签名,得到sign。

关键代码如下:

public byte[] encryptAndSign(
        byte[] clearData, 
        String encryptType, byte[] receiverPublicRsaKey, 
        String signType, byte[] senderPrivateRsaKey) {
    
    if (null != receiverPublicRsaKey) {
        // 加密
    }
    
    if (null != senderPrivateRsaKey) {
        // 加签
    }
    
    return clearData;
}

AES和RSA:【接收方】操作步骤

        1. 用发送方提供的RSA公钥(senderPublicRsaKey),对接收密文数据(encryptData+encryptAesKey)进行验签,验签失败则结束。

        2. 用自己的RSA私钥(receiverPrivateRsaKey),对encryptAesKey进行RSA解密,得到aesKey。

        3. 用aesKey对密文数据encryptData进行AES解密,得到明文数据clearData。

 关键代码如下:

public byte[] checkSignAndDecrypt(
        byte[] data, 
        String signType, byte[] senderPublicRsaKey, byte[] sign, 
        String encryptType, byte[] receiverPrivateRsaKey) {
    
    if (signType != null) {
        // 验签
    }
    if (encryptType != null) {
        // 解密
    }
    
    return null;
}

  关键之处: 应用端(接收方),要向服务端(发送方)提供 自己的appId和RSA解密公钥。

  并且,要保存服务端(发送方)提供的RSA验签公钥。


  为方便和易扩展,服务端(发送方)可以选择是否对数据进行加密和加签,

  并且从数据上就可以判断,是否进行了加密和加签,以及加密和加签的类型。

  所以,一个完整的数据组成如下(以JSON为例):

  {data:"...", encryptType:"AES...", sign:"...", signType:"RSA..."}

  其中,encryptType、sign和signType,都是非必须的。


  注意到另外一种流行的签名方式:msg_signature=sha1(sort(Token、timestamp、nonce, msg_encrypt))

  这种签名方式的好处在于,它使用SHA1算法,这个算法和MD5类似,是公开的,不需要密匙。

  另外注意,此处的token,其实是一个密码,是应用端(接收方)配置的,和appid一起配置的,

  服务端(发送方)也知道这个密码,所以这个密码(Token)不会在网络上传输。

  这样一来,黑客即使知道算法和传递的参数,但他不知道密码(Token),所以他生成的signature,在服务端无法通过校验。


  另外,之所以加上timestamp 和 nonce,是为了能够防止重放攻击(Replay-Attack),但为此还得做特殊处理:

  可选的实现方式是把每一次请求的Nonce保存到数据库,客户端再一次提交请求时将请求头中得Nonce与数据库中得数据作比较,

  如果已存在该Nonce,则证明该请求有可能是恶意的。然而这种解决方案也有个问题,很有可能在两次正常的资源请求中,

  产生的随机数是一样的,这样就造成正常的请求也被当成了攻击,随着数据库中保存的随机数不断增多,

  这个问题就会变得很明显。所以,还需要加上另外一个参数Timestamp(时间戳)。

  之所以把timestamp 和 nonce一并做SHA1,是防止被修改,

  假如他修改了timestamp,他又没办法生成有效的signature,所以无法通过校验。


  问题又来了,随着用户访问的增加,数据库中保存的nonce/timestamp/appid数据量会变得非常大。

  对于这个问题,可选的解决方案是对数据设定一个“过期时间”,比如说在数据库中保存超过一天的数据将会被清除。

  如果是这样的,攻击者可以等待一天后,再将拦截到的HTTP报文提交到服务器,这时候因为nonce/timestamp/appid数据已被服务器清除,请求将会被认为是有效的。

  要解决这个问题,就需要给时间戳设置一个超时时间,比如说将时间戳与服务器当前时间比较,如果相差一天则认为该时间戳是无效的。


  对比RSA签名方案,他们其实都要保存密码,RSA方案服务端(发送方)需要保存一个自己的RSA私钥以及所有应用端(接收方)的RSA公钥,

  而SHA1方案,服务端(发送方)需要保存所有应用端(接收方)的token。但是,SHA1速度更快,

  RSA验签其实是先把消息进行SHA1或者SHA256(以便控制长度,RSA加解密对长度有限制),然后再RSA解密,即SHA1WithRSA、SHA256WithRSA算法,故速度稍慢。

  说到防重放攻击,RSA方案也是可以的,在RSA参数中,加上nonce和timestamp,并且一起做签名。

  黑客无法篡改nonce和timestamp,但是又只能一次性使用,那就达到了防重放的目的。

  总的来说,RSA方案缺点明显:担心私钥泄露,而且私钥改了会影响所有客户端数据签名,其优点就是不用保存客户端的密钥。

  个人建议使用SHA方案,配置aesKey用于加密,配置token配合SHA算法用于数据校验。实际上微信开放平台就是用的这个方案。