1.1 数据的完整性
在计算机世界里,无论文件、声音、视频,还是软件安装包都是数据。数据从发送者抵达接收者之前,会经过若干个通信设备,在这个过程中如果数据被恶意攻击者修改或植入木马,导致接收者获取的数据不再是可信任的,甚至数据中的恶意逻辑会攻击接收者的计算机。
除了上图的篡改攻击之外,还可能存在:
I. 接收者的计算机中有病毒,这个病毒感染了这个数据;
II. 在网络传输中数据传输错误、丢包等问题而造成数据不完整;
在了解如何知道接收到的数据与发送的数据是否一致之前,我们先看几个例子:
(3)Go语言中文社区:https://studygolang.com/dl
从上面几个官方网站上可以看到,哈希(如Sha256、sha512、MD5)和签名(pgp)是两种比较常用的用于校验安装包完整性的方式。
1.2 使用Hash校验软件安装包的完整性
使用哈希校验软件安装包原理逻辑如下图所示:
官方网站开发了一个应用程序之后,会把该应用程序打包成安装包,并计算出安装包的散列值,一块在官网上公布。用户从官网下载安装软件之后,针对下载的安装软件也计算自己的散列值,然后与官网公布的散列值进行对比,如果相同则说明用户下载的软件安装包与官网的软件安装包是相同的,即没有被黑客篡改过。
那么问题来了,用户怎么计算自己下载的软件安装包的散列值呢?别担心,操作系统为我们提供了相关的工具和命令。下面以Windows操作系统为例演示如何对Tomcat安装包进行完整性校验:
(1) 打开Apache官网,下载apache-tomcat-9.0.16-windows-x64.zip后放到D盘;
(2) 打开命令窗口,切换到D盘,执行certUtil -hashfile apache-tomcat-9.0.16-windows-x64.zip SHA512命令计算散列值,其中
> certUtil:Windows操作系统自带命令
> -hashfile:固定参数
> apache-tomcat-9.0.16-windows-x64.zip:刚下载的tomcat软件安装包
> SHA512:由于官网使用sha512,所以这里也得使用sha512
(3) 打印出散列值
(4) 进入官网查看Tomcat官网的公布的安装包散列值:
3f9063cfbe78f6bdaf5015f4914d1918e1f38311caecbebea76fd311cbd01a2be8ad1d189b0b3b0ac6a12c4fa62142250fcaa1926b3a3abd94046fdd6d2ee473
(5) 比较使用windows工具计算出的散列值与官网公布的散列值是否相同
@备注:Linux操作系统下使用sha256sum命令,参见www.apache.org/info/verification.html
1.2.1 为什么哈希可以校验软件安装包的完整性?
电影或电视剧中经常有这么一个镜头,案发现场刑警正在收集指纹信息,请您特别注意,指纹有如下关键性质:
(1)单向性:由犯罪嫌疑人可以获取犯罪嫌疑人的指纹,但仅仅凭指纹无法获取犯罪嫌疑人;想要根据指纹推断出犯罪嫌疑人,还需要用犯罪嫌疑人的指纹与该指纹对比。
(2) 抗碰撞性:现实世界两个人的指纹是不同的,即使这两个人是双胞胎,他们的指纹也是不同的。
哈希其实就是现实世界指纹在密码学的映射,哈希也具备单向性、抗碰撞性的特性,除此之外哈希还具备:
(3) 长度固定性: 无论软件包大小多么不同,哈希函数生成的散列值都是固定长度的。
现在我们再想想为什么“哈希可以校验软件完整性”?假设A软件包的散列值为“β”,如果攻击者成功地实施了如图1所示的攻击,那么用户计算的散列值肯定与官网的散列值β不匹配,从而知道软件包A被篡改了,达到了校验软件完整性的目的。
1.2.1 哈希常见种类
(1)MD4和MD5:都产生128比特的散列值,随着计算机算力增加,这两种算法的抗碰撞性已被攻破,不再推荐使用。
(2)SHA1:是美国国家标准技术研究所NIST在1995年发布的,能产生160比特散列值,这个算法被列为谨慎使用的哈希。
(3)SHA2:由SHA-224、SHA-256、SHA-512/224、SHA-512/256、SHA-384和SHA-512等6个哈希函数组成的,这些均是NIST设计的,是目前推荐使用的哈希。
(4)SHA3:通过5年的公开选拨于2012年确定的下一代哈希函数,采用了一个名叫Keccak的算法,由于本书是软件基础安全,对于算法就不具体讲述了。
1.2.3 Go语言实现哈希
@备注:I、这里假定读者是熟悉Golang语言的 II、详细代码见https://github.com/qingke/Security
下面的代码中对读者提供了4个调用函数,分别为:
(1) 对字符串数据产生散列值
GetHexHash(kind uint8, bytes []byte) (hexstr string)
> kind:目前只支持0、1、2三个值,分别对应MD5、SHA-256和SHA-512算法
> bytes:要计算散列值的数据,如果你是字符串,可以把字符串转换为[]byte
> hexstr:数据bytes的散列值
(2)对文件产生散列值
GetFileHexHash(kind uint8, filePath string) (hexstr string, err error)
> kind:目前只支持0、1、2三个值,分别对应MD5、SHA-256和SHA-512算法
> filePath:要计算散列值的文件路径
> hexstr:路径filePath对应的文件的散列值
> err:可能存在文件不存在或者打开文件失败的情况
(3)计算字符串数据的散列值,并判断与给定的散列值hexHashStr是否相等
EqualSign(kind uint8, bytes []byte, hexHashStr string) bool
> kind:目前只支持0、1、2三个值,分别对应MD5、SHA-256和SHA-512算法
> bytes:要计算散列值的数据,如果你是字符串,可以把字符串转换为[]byte
> hexHashStr:给定的散列值,用于与数据产生的散列值进行比较
> bool:相同为true,不同为false
(4)计算文件的散列值,并判断与给定的散列值hexHashStr是否相等
EqualFileSign(kind uint8, filePath string, hexHashStr string) (b bool, err error)
> kind:目前只支持0、1、2三个值,分别对应MD5、SHA-256和SHA-512算法
> filePath:要计算散列值的文件路径
> hexHashStr:给定的散列值,用于与数据产生的散列值进行比较
> bool:相同为true,不同为false
> err:可能存在文件不存在或者打开文件失败的情况
具体代码如下:
package chash
import (
"crypto/md5"
"crypto/sha256"
"crypto/sha512"
"encoding/hex"
"hash"
"io"
"os"
)
/**
* 由kind获取对应的Hash对象
* kind = 0: MD5
* kind = 1: SHA-256
* kind = 2: SHA-512
*/
func getHashObj(kind uint8) (h hash.Hash) {
if kind == 0 {
h = md5.New()
} else if kind == 1 {
h = sha256.New()
} else {
h = sha512.New()
}
return
}
/**
* 计算[]byte的散列值
* kind = 0: MD5
* kind = 1: SHA-256
* kind = 2: SHA-512
*/
func GetHexHash(kind uint8, bytes []byte) (hexstr string) {
var h hash.Hash = getHashObj(kind)
h.Write(bytes)
hexstr = hex.EncodeToString(h.Sum(nil))
return
}
/**
* 计算文件的散列值
* kind = 0: MD5
* kind = 1: SHA-256
* kind = 2: SHA-512
*/
func GetFileHexHash(kind uint8, filePath string) (hexstr string, err error) {
file, err := os.Open(filePath)
if err != nil {
return
}
defer file.Close()
var h hash.Hash = getHashObj(kind)
_, err = io.Copy(h, file)
if err != nil {
return
}
return hex.EncodeToString(h.Sum(nil)), nil
}
/**
* 判断散列值是否相等
* kind = 0: MD5
* kind = 1: SHA-256
* kind = 2: SHA-512
*/
func EqualSign(kind uint8, bytes []byte, hexHashStr string) bool {
var hexStr string = GetHexHash(kind, bytes)
if hexStr == hexHashStr {
return true
}
return false
}
/**
* 判断文件散列值是否相等
* kind = 0: MD5
* kind = 1: SHA-256
* kind = 2: SHA-512
*/
func EqualFileSign(kind uint8, filePath string, hexHashStr string) (b bool, err error) {
hexStr, err := GetFileHexHash(kind, filePath)
if err != nil {
return
}
b = (hexStr == hexHashStr)
return
}
以下载tomcat为例:我把下载的apache-tomcat-9.0.16-windows-x64.zip放到了D盘下,官网上该安装包对应的散列值为:3f9063cfbe78f6bdaf5015f4914d1918e1f38311caecbebea76fd311cbd01a2be8ad1d189b0b3b0ac6a12c4fa62142250fcaa1926b3a3abd94046fdd6d2ee473,是使用SHA-512计算出来的。为了校验我下载的该Tomcat安装包在下载过程中是否被恶意攻击者篡改,我只需要调用一下EqualFileSign()函数即可,如下:
func main() {
var kind uint8 = 2
var filePath string = "D:/apache-tomcat-9.0.16-windows-x64.zip"
var hexHashStr string = "3f9063cfbe78f6bdaf5015f4914d1918e1f38311caecbebea76fd311cbd01a2be8ad1d189b0b3b0ac6a12c4fa62142250fcaa1926b3a3abd94046fdd6d2ee473"
flag, err := chash.EqualFileSign(kind, filePath, hexHashStr)
if err != nil{
fmt.Println("open file failure.", err)
}
fmt.Println(flag)
}
运行结果为:
同样地,你也可以下载MySQL并调用上面的函数进行校验 :)
1.3 使用PGP校验软件安装包的完整性
使用PGP校验软件安装包原理逻辑如下图所示:
官方开发应用程序之后,会把该应用程序打包成软件安装包,并计算出安装包的散列值,再使用私钥对散列值进行加密得到签名值,此时软件安装包、签名值和公钥一块在官网上公布。用户从官网下载软件安装包、公钥和签名,使用公钥对签名进行解密得到散列值1,同时用户针对下载的安装软件也计算自己的散列值2,通过对比散列值1和散列值2是否相同来判断软件安装包是否被黑客篡改过。
@备注:Linux操作系统下有缺省的开源pgp工具可用,该部分内容是在Ubuntu上演示的
(1) 打开Apache官网,下载apache-tomcat-9.0.16.tar.gz、pgp和KEYS放到Downloads目录下
(2)通过gpg --import命令把公钥导入到pgp工具
(3)执行gpg --verify apache-tomcat-9.0.16.tar.gz.asc apache-tomcat-9.0.16.tar.gz来校验apache-tomcat-9.0.16.tar.gz安装包的完整性
可以看到“Good signature”标识,说明这个签名是好的,从而这个软件安装包是没有被篡改过的。
1.3.1 为什么签名可以校验软件安装包的完整性?
在解答这个问题之前先把注意力放在密钥上,直观理解,使用密钥key加密,也必须使用key解密,那么密钥key被称为对称密钥。密钥学里面还有一种非对称密钥,我们逛超市时经常会把一些物品放到储物箱,放一个硬币打开储物箱,物品放置好关上门后,必须使用钥匙才能打开,这种情况就类似非对称加密的公私钥:公钥对应硬币,任何人都可以使用公钥;私钥对应钥匙,只有钥匙持有人才能打开储物箱。
再看一下Alice和Bob的故事: Alice有一对公私钥,然后把公钥告诉了她的三个异性朋友,其中Bob对Alice有爱慕之意。有天Bob写了一封求爱信给Alice,Bob肯定希望这封求爱信只有Alice能看到,于是Bob使用公钥加密求爱信,由于只有私钥才能解密,即便Jackson或Mike获得这个加密的求爱信也无法解密。这个是非对称加密的典型用法:公钥加密、私钥解密,用于保障数据机密性。
Alice也很喜欢Bob,Alice看完求爱信后写了一封回信。如果Alice使用私钥加密这封回信,结果就悲剧了,因为除了Bob能解密之外,Jackson和Mike也能解密,这样Alice说的悄悄话就被Jackson和Mike窥探到了,但这却带来另外一个副作用:可以证明这封回信是Alice写的,相当于变相地实现了内容签名,这个是非对称加密的另一典型用法:私钥加密,公钥解密,用于证明数据完整性。
1.3.2 什么是PGP?
PGP是菲得普.季默曼在1990年编写的密码软件,可在Windows、Linux、Mac OS平台上运行;此外还有一个由GNU遵照RFC4880规范编写的叫GnuPG的自由软件。这两个软件都具备如下功能:
I. 对称加密
II. 非对称加密
III. 哈希
IV. 证书
V. 钥匙串管理等
只有涉及签名时才用非对称密钥的私钥加密,公钥解密进行验签。很明显使用非对称进行签名过程明显比单纯计算软件安装包的散列值要复杂一些。为了加深大家对PGP的理解,我们在Win8操作系统中下载一款名叫gpg4win的PGP软件。
(2)打开gpg4win的客户端Kleopatra,创建公钥密钥对
(3)输入名称和邮箱,如下图
这里你可以设置“高级设置”,用于指定密钥类型和证书用途,如下:
(4)选择“下一步”后进入密钥对创建向导,单点“新建”
(5)为了进一步保护这个要创建的密钥对,这里让你再使用一个口令进行保护,我输入的口令为Test1234@
(6)接下提示你密钥创建成功,同时你还可以把公钥公布出去,这里直接选择“完成”
@备注:上面(2)~(6)的作用等同于gpg --gen-key命令
(7)使用私钥对C:\Users\chen\Downloads\apache-tomcat-9.0.16-windows-x64.zip文件进行签名
(8)这里只是为了演示签名,所以只勾选了“签名身份”
@备注:上面(7)~(8)的作用等同于gpg --sign apache-tomcat-9.0.16-windows-x64.zip
(9)对上面的签名使用公钥进行验签,依次选择“校验” > 签名文件 > 打开
@备注:上面(9)的作用等同于gpg --verify apache-tomcat-9.0.16-windows-x64.zip.sig apache-tomcat-9.0.16-windows-x64.zip
1.3.3 上面示例的漏洞
上面我们知道了使用私钥进行签名,使用公钥进行验签看看软件安装包的完整性。其中公钥可以公布给任何人,但服务器上的私钥该如何保存呢?上面示例中私钥一直是明文的,这无形中会增加私钥泄露风险。
要解决私钥的机密性,容易想到的是:“再用key2加密私钥”,那么key2的机密性怎么保护呢?依次类推就会形成这么一个链条:私钥 -> key2 -> key3 -> key4 -> key5 -> ......,无休无止,形成了一个蛋生鸡和鸡生蛋的问题。在软件体系不存在这么长的无休止密钥链条,并且在实际编码中这个链条根据具体业务场景,建议不要超过4层。
此图分为了左中右三部分,其中左侧部分就是我们一直讨论的签名,中间部分展示了对私钥进行保护,右侧部分说明了用于保护私钥的密钥是怎么产生的。
1.4 小结
本章介绍了哈希、数字签名的相关知识,学习了如何使用哈希、数字签名对软件包的完整性进行校验,此外还讨论了私钥如何保护。业界对于完整性校验还有CMS和内层文件的校验问题,考虑到大部分都是哈希和数字签名,所以没有对其它的再展开。
在本章我们下载了软件安装包,也验证了软件安装包的完整性,接下来我们就探讨在软件安装时要注意哪些问题。