10.3. hmac — 加密消息签名和验证

未匹配的标注

目的:hmac 模块实现用于消息验证的密钥散列,如 RFC 2104 中所述。

HMAC算法可用于验证在应用程序之间传递或存储在潜在易受攻击位置的信息的完整性。基本思想是生成与共享密钥组合的实际数据的加密散列。然后,可以使用所得到的散列来检查所发送或存储的消息以确定信任级别,而不发送秘密密钥。

警告

免责声明:我不是安全专家。有关 HMAC 的完整详细信息,请查看 RFC 2104.

签名消息

new() 函数创建一个用于计算消息签名的新对象。此示例使用默认的 MD5 散列算法。

hmac_simple.py

import hmac

digest_maker = hmac.new(b'secret-shared-key-goes-here')

with open('lorem.txt', 'rb') as f:
    while True:
        block = f.read(1024)
        if not block:
            break
        digest_maker.update(block)

digest = digest_maker.hexdigest()
print(digest)

运行时,代码读取数据文件并为其计算 HMAC 签名。

$ python3 hmac_simple.py

4bcb287e284f8c21e87e14ba2dc40b16

可选的摘要类型

尽管默认的 hmac 算法是 MD5,但那不是最安全的方法。MD5 摘要算法有一些缺点,例如碰撞?(两个不同的消息产生了不同的哈希值)。 SHA-1 被公认为是比较健壮的,应该使用它。

hmac_sha.py

import hmac
import hashlib

digest_maker = hmac.new(
    b'secret-shared-key-goes-here',
    b'',
    hashlib.sha1,
)

with open('hmac_sha.py', 'rb') as f:
    while True:
        block = f.read(1024)
        if not block:
            break
        digest_maker.update(block)

digest = digest_maker.hexdigest()
print(digest)

new() 函数接受三个参数值,第一个是密钥,共享于两个通信的端点之间,所以两个端点都使用相同的值。第二个参数是初始化消息值。如果需要认证的消息内容非常小,例如时间戳或者 HTTP POST,那么整个消息体可以传入 new() 而不用 update() 方法。最后一个参数是要使用的摘要算法。默认的是  hashlib.md5,例子中使用的是 hashlib.sha1

$ python3 hmac_sha.py

dcee20eeee9ef8a453453f510d9b6765921cf099

二进制摘要

前一个例子使用了  hexdigest()  方法生成可打印的摘要字符串。这个摘要字符串是由 digest() 方法生成的值的不同表现形式而已,这个值可能包括包括不可打印字符,包括 NUL。一些 web 服务(Google checkout, Amazon S3)使用了二进制摘要的 base64 版本而不是 hexdigest。

hmac_base64.py

import base64
import hmac
import hashlib

with open('lorem.txt', 'rb') as f:
    body = f.read()

hash = hmac.new(
    b'secret-shared-key-goes-here',
    body,
    hashlib.sha1,
)

digest = hash.digest()
print(base64.encodestring(digest))

base64 编码的字符串以换行符结尾,当将字符串嵌入到 HTTP 头或者其他敏感的上下文中时,经常需要将这个换行符删除。

$ python3 hmac_base64.py

b'olW2DoXHGJEKGU0aE9fOwSVE/o4=\n'

消息签名的应用

HMAC 算法应该用于任何公共网络服务,并且任何数据都应该存储在安全性很重要的地方。例如,当数据被通过管道或者 socket 发送的时候,数据应该被签名,然后在使用之前验签。此处给出的扩展示例位于文件 hmac_pickle.py

第一步是创建一个函数计算一个字符串的摘要,以及一个简单的类,用于实例化并通过通信通道传递。

hmac_pickle.py

import hashlib
import hmac
import io
import pickle
import pprint

def make_digest(message):
    "Return a digest for the message."
    hash = hmac.new(
        b'secret-shared-key-goes-here',
        message,
        hashlib.sha1,
    )
    return hash.hexdigest().encode('utf-8')

class SimpleObject:
    """Demonstrate checking digests before unpickling.
    """

    def __init__(self, name):
        self.name = name

    def __str__(self):
        return self.name

接下来,创建一个  BytesIO 缓冲池代表一个socket或者管道。例子中使用了一个原生的,但是很容易解析的,格式化的数据流。首先数据的摘要和长度被写入,后面紧跟了一个换行符。对象的序列化形式由 pickle 生成。真实的系统可能不希望依赖于长度值,因为如果摘要错误,则长度也是错误的。某些不太可能出现在实际数据中的终结符序列可能更加合适。

然后示例程序中往数据流中写入了两个对象。第一个是使用正确的摘要值写入的。

# 使用缓冲区模拟可写套接字或者管道
out_s = io.BytesIO()

# 往流中写入一个有效的对象
#  digest\nlength\npickle
o = SimpleObject('digest matches')
pickled_data = pickle.dumps(o)
digest = make_digest(pickled_data)
header = b'%s %d\n' % (digest, len(pickled_data))
print('WRITING: {}'.format(header))
out_s.write(header)
out_s.write(pickled_data)

第二个写入流中的对象使用了一个无效的摘要值,摘要值是由其他一些数据生成的而不是序列化之后的值。

# 往流中写入一个无效的对象
o = SimpleObject('digest does not match')
pickled_data = pickle.dumps(o)
digest = make_digest(b'not the pickled data at all')
header = b'%s %d\n' % (digest, len(pickled_data))
print('\nWRITING: {}'.format(header))
out_s.write(header)
out_s.write(pickled_data)

out_s.flush()

记住数据是在 BytesIO  缓冲池中的,它可以再次被读出。首先读取带有摘要和数据长度的行。然后使用长度值读取剩下的数据。pickle.load() 可以直接从流中读取数据,但是它假设一个可信任的数据流,而且这个数据还不足够可信能够反序列化它。从流中读取序列化值为字符串,而不实际反序列化更安全。

# 使用 BytesIO 模拟可读的 socket 或者管道
in_s = io.BytesIO(out_s.getvalue())

# 读取数据
while True:
    first_line = in_s.readline()
    if not first_line:
        break
    incoming_digest, incoming_length = first_line.split(b' ')
    incoming_length = int(incoming_length.decode('utf-8'))
    print('\nREAD:', incoming_digest, incoming_length)

一旦序列化数据被读取到内存,摘要值可以被重新计算,并且使用 compare_digest() 同传递过来的数据比较。如果摘要匹配,证明它是安全的去信任数据并且反序列化它。

    incoming_pickled_data = in_s.read(incoming_length)

    actual_digest = make_digest(incoming_pickled_data)
    print('ACTUAL:', actual_digest)

    if hmac.compare_digest(actual_digest, incoming_digest):
        obj = pickle.loads(incoming_pickled_data)
        print('OK:', obj)
    else:
        print('WARNING: Data corruption')

输出表示第一个对象被验证通过,第二个被视为「损坏」。

$ python3 hmac_pickle.py

WRITING: b'f49cd2bf7922911129e8df37f76f95485a0b52ca 69\n'

WRITING: b'b01b209e28d7e053408ebe23b90fe5c33bc6a0ec 76\n'

READ: b'f49cd2bf7922911129e8df37f76f95485a0b52ca' 69
ACTUAL: b'f49cd2bf7922911129e8df37f76f95485a0b52ca'
OK: digest matches

READ: b'b01b209e28d7e053408ebe23b90fe5c33bc6a0ec' 76
ACTUAL: b'2ab061f9a9f749b8dd6f175bf57292e02e95c119'
WARNING: Data corruption

可以在定时攻击中使用简单的字符串或字节比较来比较两个摘要,以通过传递不同长度的摘要来暴露部分或全部秘密密钥。compare_digest() 实现了一个快速但是常量时间的比较函数去防止定时攻击。

推荐阅读

本文章首发在 LearnKu.com 网站上。

本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://learnku.com/docs/pymotw/hmac-cry...

译文地址:https://learnku.com/docs/pymotw/hmac-cry...

上一篇 下一篇
讨论数量: 0
发起讨论 只看当前版本


暂无话题~