Python中使用SHA256对字节字符串进行签名

3

目前我有一些代码,使用本地的OpenSSL二进制文件对字节串进行SHA256算法签名,该代码调用外部进程,发送参数,并将结果返回到Python代码中。

当前代码如下:

signed_digest_proc = subprocess.Popen(
    ['openssl', 'dgst', '-sha256', '-sign', tmp_path],
    stdin=subprocess.PIPE,
    stdout=subprocess.PIPE
)
signed_digest_proc.stdin.write(original_string)
signed_digest, _ = signed_digest_proc.communicate()

base64.encodestring(signed_digest).decode().replace('\n', '')

original_string太大时,我可能会遇到结果问题(我认为这是与外部进程通信有关),因此我正在尝试将其更改为仅使用Python的解决方案:

import hmac, hashlib
h = hmac.new(bytes(key_pem(), 'ASCII'), original_string, hashlib.sha256)
result = base64.encodestring(h).decode().replace('\n', '')

这将导致与第一个字符串完全不同的结果。如何在不调用外部进程的情况下实现原始代码?

2
你在使用openssl进行两个操作,但都没有使用HMAC签名。你正在生成一个普通的SHA256摘要,然后还对该摘要进行签名 - Martijn Pieters
2
要进行签名,请查看cryptography包,并根据您拥有的私钥类型(DSA、RSA等)选择其中一种算法。然后阅读Signing部分。例如,对于RSA,有如何签署消息的文档 - Martijn Pieters
1个回答

7
你使用的 openssl 命令执行了三个操作:
  • 使用 SHA256 创建数据的哈希
  • 如果使用 RSA,将消息填充到特定长度,使用 PKCS#1 1.5
  • 使用提供的私钥签署(填充的)哈希。算法取决于密钥类型。
hmac 模块不具有相同的功能。
您需要安装类似 cryptography 这样的加密包来复制 openssl dgst -sign 的功能。 cryptography 将 OpenSSL 用作后端,因此它将生成相同的输出。
然后,您可以:
  • 使用 load_pem_private_key() 函数 加载密钥。这将返回用于所使用算法的正确类型的对象。
  • 使用密钥签署消息;每个密钥类型都有一个 sign() 方法,如果需要,此方法将为您处理哈希消息。例如,参见 RSA 的签名 部分

    但是,您需要为不同的 .sign() 方法提供不同类型的配置。只有 RSA、DSA 和椭圆曲线密钥可以用于创建签名摘要。

您必须在不同的类型之间切换以正确生成签名:
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import dsa, ec, rsa, utils
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import padding

# configuration per key type, each lambda takes a hashing algorithm
_signing_configs = (
    (dsa.DSAPrivateKey, lambda h: {
        'algorithm': h}),
    (ec.EllipticCurvePrivateKey, lambda h: {
        'signature_algorithm': ec.ECDSA(h)}),
    (rsa.RSAPrivateKey, lambda h: {
        'padding': padding.PKCS1v15(),
        'algorithm': h
    }),
)

def _key_singing_config(key, hashing_algorithm):
    try:
        factory = next(
            config
            for type_, config in _signing_configs
            if isinstance(key, type_)
        )
    except StopIteration:
        raise ValueError('Unsupported key type {!r}'.format(type(key)))
    return factory(hashing_algorithm)

def sign(private_key, data, algorithm=hashes.SHA256()):
    with open(private_key, 'rb') as private_key:
        key = serialization.load_pem_private_key(
            private_key.read(), None, default_backend())

    return key.sign(data, **_key_singing_config(key, algorithm))

如果你需要对大量数据进行哈希,可以先将数据分块自行哈希,然后只传递摘要和特殊的util.Prehashed()对象

def sign_streaming(private_key, data_iterable, algorithm=hashes.SHA256()):
    with open(private_key, 'rb') as private_key:
        key = serialization.load_pem_private_key(
            private_key.read(), None, default_backend())

    hasher = hashes.Hash(algorithm, default_backend())
    for chunk in data_iterable:
        hasher.update(chunk)
    digest = hasher.finalize()
    prehashed = utils.Prehashed(algorithm)

    return key.sign(digest, **_key_singing_config(key, prehashed))

with open(large_file, 'rb') as large_file:
    signature = sign_streaming(private_key_file, iter(lambda: large_file.read(2 ** 16), b''))

这里使用iter()函数以64千字节的块来读取二进制文件中的数据。

演示代码:我正在使用我在 /tmp/test_rsa.pem 中生成的 RSA 密钥。使用命令行为 Hello world! 生成签名摘要:

$ echo -n 'Hello world!' | openssl dgst -sign /tmp/test_rsa.pem -sha256 | openssl base64
R1bRhzEr+ODNThyYiHbiUackZpx+TCviYR6qPlmiRGd28wpQJZGnOFg9tta0IwkT
HetvITcdggXeiqUqepzzT9rDkIw6CU7mlnDRcRu2g76TA4Uyq+0UzW8Ati8nYCSx
Wyu09YWaKazOQgIQW3no1e1Z4HKdN2LtZfRTvATk7JB9/nReKlXgRjVdwRdE3zl5
x3XSPlaMwnSsCVEhZ8N7Gf1xJf3huV21RKaXZw5zMypHGBIXG5ngyfX0+aznYEve
x1uBrtZQwUGuS7/RuHw67WDIN36aXAK1sRP5Q5CzgeMicD8d9wr8St1w7WtYLXzY
HwzvHWcVy7kPtfIzR4R0vQ==

或者使用Python代码:
>>> signature = sign(keyfile, b'Hello world!')
>>> import base64
>>> print(base64.encodebytes(signature).decode())
R1bRhzEr+ODNThyYiHbiUackZpx+TCviYR6qPlmiRGd28wpQJZGnOFg9tta0IwkTHetvITcdggXe
iqUqepzzT9rDkIw6CU7mlnDRcRu2g76TA4Uyq+0UzW8Ati8nYCSxWyu09YWaKazOQgIQW3no1e1Z
4HKdN2LtZfRTvATk7JB9/nReKlXgRjVdwRdE3zl5x3XSPlaMwnSsCVEhZ8N7Gf1xJf3huV21RKaX
Zw5zMypHGBIXG5ngyfX0+aznYEvex1uBrtZQwUGuS7/RuHw67WDIN36aXAK1sRP5Q5CzgeMicD8d
9wr8St1w7WtYLXzYHwzvHWcVy7kPtfIzR4R0vQ==

尽管行长度不同,但两个输出的base64数据明显相同。

或者,使用包含随机二进制数据的生成文件,大小为32kb:

$ dd if=/dev/urandom of=/tmp/random_data.bin bs=16k count=2
2+0 records in
2+0 records out
32768 bytes transferred in 0.002227 secs (14713516 bytes/sec)
$ cat /tmp/random_data.bin | openssl dgst -sign /tmp/test_rsa.pem -sha256 | openssl base64
b9sYFdRzpBtJTan7Pnfod0QRon+YfdaQlyhW0aWabia28oTFYKKiC2ksiJq+IhrF
tIMb0Ti60TtBhbdmR3eF5tfRqOfBNHGAzZxSaRMau6BuPf5AWqCIyh8GvqNKpweF
yyzWNaTBYATTt0RF0fkVioE6Q2LdfrOP1q+6zzRvLv4BHC0oW4qg6F6CMPSQqpBy
dU/3P8drJ8XCWiJV/oLhVehPtFeihatMzcZB3IIIDFP6rN0lY1KpFfdBPlXqZlJw
PJQondRBygk3fh+Sd/pGYzjltv7/4mC6CXTKlDQnYUWV+Rqpn6+ojTElGJZXCnn7
Sn0Oh3FidCxIeO/VIhgiuQ==

使用Python处理相同文件:

>>> with open('/tmp/random_data.bin', 'rb') as random_data:
...     signature = sign_streaming('/tmp/test_rsa.pem', iter(lambda: random_data.read(2 ** 16), b''))
...
>>> print(base64.encodebytes(signature).decode())
b9sYFdRzpBtJTan7Pnfod0QRon+YfdaQlyhW0aWabia28oTFYKKiC2ksiJq+IhrFtIMb0Ti60TtB
hbdmR3eF5tfRqOfBNHGAzZxSaRMau6BuPf5AWqCIyh8GvqNKpweFyyzWNaTBYATTt0RF0fkVioE6
Q2LdfrOP1q+6zzRvLv4BHC0oW4qg6F6CMPSQqpBydU/3P8drJ8XCWiJV/oLhVehPtFeihatMzcZB
3IIIDFP6rN0lY1KpFfdBPlXqZlJwPJQondRBygk3fh+Sd/pGYzjltv7/4mC6CXTKlDQnYUWV+Rqp
n6+ojTElGJZXCnn7Sn0Oh3FidCxIeO/VIhgiuQ==

谢谢,你的示例代码非常清晰,现在我得到了与原始的OpenSSL实现相同的结果。 - Luis Alberto Santana

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接