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

Sign a byte string with SHA256 in Python

目前我有一些代码使用本机 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', '')

这导致与第一个字符串完全不同的字符串。

在不调用外部进程的情况下实现原始代码的方法是什么?

您使用的 openssl 命令做了三件事:

  • 使用 SHA256 创建数据的哈希值
  • 如果使用 RSA,将消息填充到特定长度,使用 PKCS#1 1.5
  • 使用您提供的私钥签署(填充的)散列。这将取决于使用什么算法的密钥类型。

hmac 模块不提供相同的功能。

您需要安装像 cryptography 这样的加密包来复制 openssl dgst -sign 的功能。 cryptography 使用 OpenSSL 作为后端,因此它会产生相同的输出。

然后你可以

  • 使用 load_pem_private_key() function 加载密钥。这 returns 所用算法的正确对象类型。
  • 使用密钥对消息进行签名;每个密钥类型都有一个 sign() 方法,如果您愿意,此方法将负责为您散列消息。例如参见 [​​=28=].

    但是,您需要为不同的 .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() object:

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() function 从二进制文件中以 64 KB 的块读取数据。

演示;我正在使用我在 /tmp/test_rsa.pem 中生成的 RSA 密钥。使用 command-line 为 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==