29.密码学知识-消息认证码MAC-6——2019年12月19日

1. 消息认证码

1.1 消息认证

消息认证码(message authentication code)是一种确认完整性并进行认证的技术,取三个单词的首字母,简称为MAC。

1539176665517

  • 思考改进方案?

    从哈希函数入手

    需要将要发送的数据进行哈希运算, 将哈希值和原始数据一并发送

    需要在进行哈希运算的时候引入加密的步骤

    • 在alice对数据进行哈希运算的时候引入一个秘钥, 让其参与哈希运算, 生成散列值
    • bob对数据校验
      • bob收到原始和散列值之后,
        • 处理原始数据: 通过秘钥和哈希算法对原始数据生成散列值
        • 散列值比较: 生成的散列值 和 接收到的散列值进行比对

1.2 消息认证码的使用步骤

1539177690828

  1. 前提条件:
    • 在消息认证码生成的一方和校验的一方, 必须有一个秘钥
    • 双方约定好使用同样的哈希函数对数据进行运算
  2. 流程:
    • 发送者:
      • 发送原始法消息
      • 将原始消息生成消息认证码
        • ((原始消息) + 秘钥) * 函数函数 = 散列值(消息认证码)
      • 将消息认证码发送给对方
    • 接收者:
      • 接收原始数据
      • 接收消息认证码
      • 校验:
        • ( 接收的消息 + 秘钥 ) * 哈希函数 = 新的散列值
        • 通过新的散列值和接收的散列值进行比较

1.3 go中对消息认证码的使用

有一个包: crypto/hmac

func New(h func() hash.Hash, key []byte) hash.Hash
- 返回值: hash接口
- 参数1: 函数函数的函数名
	sha1.new
	md5.new
	sha256.new
- 参数2: 秘钥

第二步: 添加数据
type Hash interface {
// 通过嵌入的匿名io.Writer接口的Write方法向hash中添加更多数据,永远不返回错误
io.Writer
// 返回添加b到当前的hash值后的新切片,不会改变底层的hash状态
Sum(b []byte) []byte
// 重设hash为无数据输入的状态
Reset()
// 返回Sum会返回的切片的长度
Size() int
// 返回hash底层的块大小;Write方法可以接受任何大小的数据,
// 但提供的数据是块大小的倍数时效率更高
BlockSize() int
}
type Writer interface {
Write(p []byte) (n int, err error)
}
第三步: 计算散列值

1.4 消息认证码的问题

  1. 弊端
    • 有秘钥分发困难的问题
  2. 无法解决的问题
    • 不能进行第三方证明
    • 不能防止否认

2. 数字签名

2.1 签名的生成和验证

  1. 签名
    • 有原始数据对其进行哈希运算 -> 散列值
    • 使用非对称加密的私钥对散列值加密 -> 签名
    • 将原始数据和签名一并发送给对方
  2. 验证
    • 接收数据
      • 原始数据
      • 数字签名
    • 数字签名, 需要使用公钥解密, 得到散列值
    • 对原始数据进行哈希运算得到新的散列值

2.2 非对称加密和数字签名

总结:

  1. 数据通信

    • 公钥加密, 私钥解密
    1. 数字签名:
- 私钥加密, 公钥解密

2.3 数字签名的方法

1539177860475

2.4 使用RSA进行数字签名

  1. 使用rsa生成密钥对

    1. 生成密钥对
    2. 序列化
    3. 保存到磁盘文件
  2. 使用私钥进行数字签名

    1. 打开磁盘的私钥文件
    2. 将私钥文件中的内容读出
    3. 使用pem对数据解码, 得到了pem.Block结构体变量
    4. x509将数据解析成私钥结构体 -> 得到了私钥
    5. 创建一个哈希对象 -> md5/sha1
    6. 给哈希对象添加数据
    7. 计算哈希值
    8. 使用rsa中的函数对散列值签名
    func SignPKCS1v15(rand io.Reader, priv *PrivateKey, hash crypto.Hash, hashed []byte) (s []byte, err error)
    参数1: rand.Reader
    参数2: 非对称加密的私钥
    参数3: 使用的哈希算法
    	crypto.sha1
    	crypto.md5
    参数4: 数据计算之后得到的散列值
    返回值: 
       - s: 得到的签名数据
       - err: 错误信息
    
  3. 使用公钥进行签名认证

    1. 打开公钥文件, 将文件内容读出 - []byte
    2. 使用pem解码 -> 得到pem.Block结构体变量
    3. 使用x509对pem.Block中的Bytes变量中的数据进行解析 -> 得到一接口
    4. 进行类型断言 -> 得到了公钥结构体
    5. 对原始消息进行哈希运算(和签名使用的哈希算法一致) -> 散列值
      1. 创建哈希接口
      2. 添加数据
      3. 哈希运算
    6. 签名认证 - rsa中的函数
    func VerifyPKCS1v15(pub *PublicKey, hash crypto.Hash, hashed []byte, sig []byte) (err error)
    参数1: 公钥
    参数2: 哈希算法 -> 与签名使用的哈希算法一致
    参数3: 将原始数据进行哈希原始得到的散列值
    参数4: 签名的字符串
    返回值: 
       	- nil -> 验证成功
       	- !=nil -> 失败
    

2.5 使用椭圆曲线进行数字签名

椭圆曲线在go中对应的包: import "crypto/elliptic"

使用椭圆曲线在go中进行数字签名: import "crypto/ecdsa"

美国FIPS186-2标准, 推荐使用5个素域上的椭圆曲线, 这5个素数模分别是:

P192 = 2192 - 264 - 1

P224 = 2224 - 296 + 1

P256 = 2256 - 2224 + 2192 - 296 -1

P384 = 2384 - 2128 - 296 + 232 -1

P512 = 2512 - 1

  1. 秘钥对称的生成, 并保存到磁盘

    1. 使用ecdsa生成密钥对
    func GenerateKey(c elliptic.Curve, rand io.Reader) (priv *PrivateKey, err error)
    
    1. 将私钥写入磁盘
      • 使用x509进行序列化
    func MarshalECPrivateKey(key *ecdsa.PrivateKey) ([]byte, error)
    
    • 将得到的切片字符串放入pem.Block结构体中

    block := pem.Block{

    Type : "描述....",

    Bytes : MarshalECPrivateKey返回值中的切片字符串,

    }

    • 使用pem编码

    pem.Encode();

    1. 将公钥写入磁盘
      • 从私钥中得到公钥
      • 使用x509进行序列化
    func MarshalPKIXPublicKey(pub interface{}) ([]byte, error)
    
    • 将得到的切片字符串放入pem.Block结构体中

    block := pem.Block{

    Type : "描述....",

    Bytes : MarshalECPrivateKey返回值中的切片字符串,

    }

    • 使用pem编码

    pem.Encode();

  2. 使用私钥进行数字签名

    1. 打开私钥文件, 将内容读出来 ->[]byte
    2. 使用pem进行数据解码 -> pem.Decode()
    3. 使用x509, 对私钥进行还原
    func ParseECPrivateKey(der []byte) (key *ecdsa.PrivateKey, err error)
    
    
    1. 对原始数据进行哈希运算 -> 散列值
    2. 进行数字签名
    func Sign(rand io.Reader, priv *PrivateKey, hash []byte) (r, s *big.Int, err error)
       - 得到的r和s不能直接使用, 因为这是指针
    	应该将这两块内存中的数据进行序列化 -> []byte
    	func (z *Int) MarshalText() (text []byte, err error)
    
    
  3. 使用公钥验证数字签名

    1. 打开公钥文件, 将里边的内容读出 -> []byte
    2. pem解码 -> pem.Decode()
    3. 使用x509对公钥还原
    func ParsePKIXPublicKey(derBytes []byte) (pub interface{}, err error)
    
    
    1. 将接口 -> 公钥
    2. 对原始数据进行哈希运算 -> 得到散列值
    3. 签名的认证 - > ecdsa
    func Verify(pub *PublicKey, hash []byte, r, s *big.Int) bool
       - 参数1: 公钥
       - 参数2: 原始数据生成的散列值
       - 参数3,4: 通过签名得到的连个点
    	func (z *Int) UnmarshalText(text []byte) error
    
    

2.6 数字签名无法解决的问题

1539178819165

6. 消息认证码

"消息认证码 --- 消息被正确传送了吗?"

6.1 什么是消息认证码

  • Alice 和 Bob 的故事

    像以前一样,我们还是从一个Alice和Bob的故事开始讲起。不过,这一次Alice和Bob分别是两家银行,Alice银行通过网络向Bob银行发送了一条汇款请求,Bob银行收到的请求内容是:

    $$
    从账户A-5374 向账户B-6671汇款1000万元
    $$

    当然,Bob银行所收到的汇款请求内容必须与Alice银行所发送的内容是完全一致的。如果主动攻击者Mallory在中途将Alice银行发送的汇款请求进行了篡改,那么Bob银行就必须要能够识别出这种篡改,否则如果Mallory将收款账户改成了自己的账户,那么1000万元就会被盗走。

    话说回来,这条汇款请求到底是不是Alice银行发送的呢?有可能Alice银行根本就没有发送过汇款请求,而是由主动攻击者Mallory伪装成Alice银行发送的。如果汇款请求不是来自Alice银行,那么就绝对不能执行汇款。

    现在我们需要关注的问题是汇款请求(消息)的 “完整性" 和 “认证" 这两个性质。

    消息的完整性(integrity), 指的是“消息没有被篡改"这一性质,完整性也叫一致性。如果能够确认汇款请求的内容与Alice银行所发出的内容完全一致,就相当于是确认了消息的完整性,也就意味着消息没有被篡改。

    消息的认证(authentication)指的是“消息来自正确的发送者"这一性质。如果能够确认汇款请求确实来自Alice银行,就相当于对消息进行了认证,也就意味着消息不是其他人伪装成发送者所发出的。

    通过使用本章中要介绍的消息认证码,我们就可以同时识别出篡改和伪装,也就是既可以确认消息的完整性,也可以进行认证。

  • 什么是消息认证码

    消息认证码(message authentication code)是一种确认完整性并进行认证的技术,取三个单词的首字母,简称为MAC。

    消息认证码的输入包括任意长度的消息和一个发送者与接收者之间共享的密钥,它可以输出固定长度的数据,这个数据称为MAC值

    根据任意长度的消息输出固定长度的数据,这一点和单向散列函数很类似。但是单向散列函数中计算散列值时不需要密钥,而消息认证码中则需要使用发送者与接收者之间共享的密钥。

    要计算MAC必须持有共享密钥,没有共享密钥的人就无法计算MAC值,消息认证码正是利用这一性质来完成认证的。此外,和单向散列函数的散列值一样,哪怕消息中发生1比特的变化,MAC值也会产生变化,消息认证码正是利用这一性质来确认完整性的。

    消息认证码有很多种实现方法,大家可以暂且这样理解:消息认证码是一种与密钥相关联的单向散列函数。

    					                           **单向散列函数与消息认证码的比较**
    
    

    1538733638766

6.2 消息认证码的使用步骤

我们还是以Alice银行和Bob银行的故事为例,来讲解一下消息认证码的使用步骤:

1538733684992

  1. 发送者Alice与接收者Bob事先共享密钥。
  2. 发送者Alice根据汇款请求消息计算MAC值(使用共享密钥)。
  3. 发送者Alice将汇款请求消息和MAC值两者发送给接收者Bob。
  4. 接收者Bob根据接收到的汇款请求消息计算MAC值(使用共享密钥)。
  5. 接收者Bob将自己计算的MAC值与从Alice处收到的MAC值进行对比。
  6. 如果两个MAC值一致,则接收者Bob就可以断定汇款请求的确来自Alice(认证成功);如果不一致,则可以断定消息不是来自Alice(认证失败)。

6.3 HMAC

6.3.1 HMAC介绍

HMAC是一种使用单向散列函数来构造消息认证码的方法(RFC2104),其中HMAC的H就是Hash的意思。

HMAC中所使用的单向散列函数并不仅限于一种,任何高强度的单向散列函数都可以被用于HMAC,如果将来设计出新的单向散列函数,也同样可以使用。

使用SHA-I、MD5、RIPEMD-160所构造的HMAC,分别称为HMAC-SHA-1、HMAC-MD5和HMAC-RlPEMD。

			**使用HMAC通过秘钥将消息生成消息认证码的内部实现**:

1538733763591

通过上述流程我们可以看出,最后得到的MAC值,一定是一个和输入的消息以及密钥都相关的长度固定的比特序列。

6.3.2 Go中对HMAC的使用

需要使用的包

import "crypto/hmac"

使用的函数

func New(h func() hash.Hash, key []byte) hash.Hash
func Equal(mac1, mac2 []byte) bool

  1. hamc.New 函数
    • 参数1: 创建一个新的使用哈希校验算法的hash.Hash接口, 如:
      • md5.New()
      • sha1.New()
      • sha256.New()
    • 参数2: 使用的秘钥
    • 返回值: 通过该哈希接口添加数据和计算消息认证码
      • 添加数据: Write(p []byte) (n int, err error)
      • 计算结果: Sum(b []byte) []byte
  2. hmac.Equal 函数
    • 比较两个MAC是否相同

生成消息认证码:

// 生成消息认证码
func GenerateHMAC(src, key []byte) []byte {
	// 1. 创建一个底层采用sha256算法的 hash.Hash 接口
	myHmac := hmac.New(sha256.New, key)
	// 2. 添加测试数据
	myHmac.Write(src)
	// 3. 计算结果
	result := myHmac.Sum(nil)

	return result
}

重要函数说明

  1. 创建一个底层采用哈希算法的 hash.Hash 接口

    函数对应的包: "crypto/hmac"
    func New(h func() hash.Hash, key []byte) hash.Hash
        - 参数 h: 函数指针, 返回值为hash.Hash, 可以使用哈希算法对应的New方法, 如:
            -- md5.New
            -- sha1.New
            -- sha256.New
            -- sha256.New224
            -- sha512.New
            -- sha512.New384
        - 参数 key: 和数据进行混合运算使用的秘钥
        - 返回值: hash.Hash 接口
    
    

验证消息认证码

func VerifyHMAC(res, src, key []byte) bool {

	// 1. 创建一个底层采用sha256算法的 hash.Hash 接口
	myHmac := hmac.New(sha256.New, key)
	// 2. 添加测试数据
	myHmac.Write(src)
	// 3. 计算结果
	result := myHmac.Sum(nil)
	// 4. 比较结果
	return hmac.Equal(res, result)
}

重要函数说明:

  1. 比较两个MAC是否相同

    函数对应的包: "crypto/hmac"
    func Equal(mac1, mac2 []byte) bool
        - 参数 mac1, mac2: 通过哈希算法计算得到的消息认证码
        - 返回值: 如果mac1==mac2, 返回 true; 否则, 返回 false
    
    

测试代码

func HMacTest() {
	key := []byte("我是消息认证码秘钥")
	src := []byte("我是消息认证码测试数据")
	result := GenerateHMAC(src, key)
	final := VerifyHMAC(result, src, key)
	if final {
		fmt.Println("消息认证码认证成功!!!")
	} else {
		fmt.Println("消息认证码认证失败 ......")
	}
}

6.3 消息认证码的密钥配送问题

在消息认证码中,需要发送者和接收者之间共享密钥,而这个密钥不能被主动攻击者Mallory获取。如果这个密钥落入Mallory手中,则Mallory也可以计算出MAC值,从而就能够自由地进行篡改和伪装攻击,这样一来消息认证码就无法发挥作用了。

发送者和接收者需要共享密钥,这一点和我们介绍的对称加密很相似。实际上,对称加密的密钥配送问题在消息认证码中也同样会发生。关于秘钥的配送后边章节会介绍如何使用非对称加密的方式进行解决。

6.4 消息认证码无法解决的问题

假设发送者Alice要向接收者Bob发送消息,如果使用了消息认证码,接收者Bob就能够断定自己收到的消息与发送者Alice所发出的消息是一致的,这是因为消息中的MAC值只有用Alice和Bob之间共享的密钥才能够计算出来,即便主动攻击者Mallory篡改消息,或者伪装成Alice发送消息,Bob也能够识别出消息的篡改和伪装。

但是,消息认证码也不能解决所有的问题,例如“对第三方证明"和“防止否认",这两个问题就无法通过消息认证码来解决。下面我们来逐一解释一下。

6.4.1 对第三方证明

假设Bob在接收了来自Alice的消息之后,想要向第三方验证者Victor证明这条消息的确是Alice发送的,但是用消息认证码无法进行这样的证明,这是为什么呢?

首先,Victor要校验MAC值,就需要知道Alice和Bob之间共享的密钥。

假设Bob相信Victor, 同意将密钥告诉Victor,即便如此,Victor也无法判断这条消息是由Alice发送的,因为Victor可以认为:“即使MAC值是正确的,发送这条消息的人也不一定是Alice,还有可能是Bob。"

能够计算出正确MAC值的人只有Alice和Bob,在他们两个人之间进行通信时,可以断定是对方计算了MAC值,这是因为共享这个密钥的双方之中,有一方就是自己。然而,对于第三方Victor、Alice或Bob却无法证明是对方计算了MAC值,而不是自己。

使用第7章中将要介绍的数字签名就可以实现对第三方的证明。

6.4.2 防止否认

假设Bob收到了包含MAC值的消息,这个MAC值是用Alice和Bob共享的密钥计算出来的,因此Bob能够判断这条消息的确来自Alice。

但是,上面我们讲过,Bob无法向验证者Victor证明这一点,也就是说,发送者Alice可以向Victor声称:“我没有向Bob发送过这条消息。”这样的行为就称为否认(repudiation)。

Alice可以说“这条消息是Bob自己编的吧",“说不定Bob的密钥被主动攻击者Mallory给盗取了,我的密钥可是妥善保管着呢" 等。说白了,就是Alice和Bob吵起来了。

即便Bob拿MAC值来举证,Victor也无法判断Alice和Bob谁的主张才是正确的,也就是说,用消息认证码无法防止否认(nonrepudiatlon)

6.5 总结

消息认证码是对消息进行认证并确认其完整性的技术。通过使用发送者和接收者之间共享的密钥,就可以识别出是否存在伪装和篡改行为。

消息认证码可以使用单向散列函数HMAC, 对称加密也可以实现, 这里不再进行介绍。

消息认证码中,由于发送者和接收者共享相同的密钥,因此会产生无法对第三方证明以及无法防止否认等问题。在下一章中,我们将介绍能够解决这些问题的数字签名。

posted @ 2019-12-19 16:03  一颗小苹果  阅读(872)  评论(0编辑  收藏  举报