如何对 Python 中的 Public 键进行 EC 压缩?

How to Do EC Compression on a Public Key in Python?

我正在尝试找到 Python 等价于 运行 openssl ec -pubin -in example.pem -inform PEM -outform DER conv_form compressed

使用以下 public 键的示例:

-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE4q+ot7o3PuJqsBonZni2spVPvqLk
6FCiEF9GXzTyYZ1snzreGB+pyoiUUkz2/H60XWmQsgC7zZ60TBT0rVimtg==
-----END PUBLIC KEY-----

和 运行 以下命令给出以下输出:

echo '-----BEGIN PUBLIC KEY-----\r\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE4q+ot7o3PuJqsBonZni2spVPvqLk\r\n6FCiEF9GXzTyYZ1snzreGB+pyoiUUkz2/H60XWmQsgC7zZ60TBT0rVimtg==\r\n-----END PUBLIC KEY-----\r\n' | openssl ec -pubin -inform PEM -outform DER -conv_form compressed | base64

read EC key
writing EC key
MDkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDIgAC4q+ot7o3PuJqsBonZni2spVPvqLk6FCiEF9GXzTyYZ0=

现在使用相同的命令加上 -text 标志并减去 base64 编码提供了更多的冗长:

echo '-----BEGIN PUBLIC KEY-----\r\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE4q+ot7o3PuJqsBonZni2spVPvqLk\r\n6FCiEF9GXzTyYZ1snzreGB+pyoiUUkz2/H60XWmQsgC7zZ60TBT0rVimtg==\r\n-----END PUBLIC KEY-----\r\n' | openssl ec -pubin -inform PEM -outform DER -conv_form compressed -text

read EC key
Private-Key: (256 bit)
pub:
    02:e2:af:a8:b7:ba:37:3e:e2:6a:b0:1a:27:66:78:
    b6:b2:95:4f:be:a2:e4:e8:50:a2:10:5f:46:5f:34:
    f2:61:9d
ASN1 OID: prime256v1
NIST CURVE: P-256
writing EC key
090*�H�*�H�="⯨��7>�j�'fx���O����P�_F_4�a�%

到目前为止我可以做类似的事情:

import base64
import cryptography
csr_crypto = cryptography.x509.load_pem_x509_csr(csr_encoded) # csr_encoded being the CSR in PEM format that the public key is derived from
pub_key = csr_crypto.public_key()
compressed_bytes = pub_key.public_bytes(cryptography.hazmat.primitives.serialization.Encoding.X962, cryptography.hazmat.primitives.serialization.PublicFormat.CompressedPoint).hex()

这导致 compressed_bytes 等于

02e2afa8b7ba373ee26ab01a276678b6b2954fbea2e4e850a2105f465f34f2619d

如果你仔细观察,它就等于上面 pub 中的内容。

这些十六进制字节最终如何转换为 openssl 输出为压缩 public 密钥的字符串 MDkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDIgAC4q+ot7o3PuJqsBonZni2spVPvqLk6FCiEF9GXzTyYZ0=

发布的第一个密钥是 X.509/SPKI 格式(PEM 编码)的 public 密钥,其中包含 未压缩 格式的实际密钥 0x04 + <x> + <y>.
使用 OpenSSL 语句由此导出的密钥也是 X.509/SPKI 格式的 public 密钥,但包含 compressed 格式的实际密钥 0x02 + <x>0x03 + <x> 分别表示偶数或奇数 y。这种格式还包含完整的信息,因为对于给定的曲线,可以从压缩密钥中导出未压缩密钥。
X.509/SPKI 密钥可以使用 ASN.1 解析器(除了 OpenSSL)进行解析,例如在线:https://lapo.it/asn1js.


在 Python 中,可以使用 PyCryptodome 库将具有未压缩密钥的 X.509/SPKI 密钥转换为具有压缩密钥的 X.509/SPKI 密钥, 请参阅 export_key():

from base64 import b64encode
from Crypto.PublicKey import ECC

x509PemWithUncompressed = '''-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE4q+ot7o3PuJqsBonZni2spVPvqLk
6FCiEF9GXzTyYZ1snzreGB+pyoiUUkz2/H60XWmQsgC7zZ60TBT0rVimtg==
-----END PUBLIC KEY-----'''

publicKey = ECC.import_key(x509PemWithUncompressed);
x509withCompressed = publicKey.export_key(format='DER', compress=True)
print(b64encode(x509withCompressed).decode('utf-8')) # MDkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDIgAC4q+ot7o3PuJqsBonZni2spVPvqLk6FCiEF9GXzTyYZ0=

另一方面,Cryptography 库只允许导出带有未压缩密钥的 X.509/SPKI 密钥。为此,必须使用 SubjectPublicKeyInfo. An export as X.509/SPKI key with compressed key is not possible. The options UncompressedPoint and CompressedPoint 指定格式,仅允许以 0x04 + x + y0x02/0x03 + x 格式导出,但不允许以 X.509/SPKI 格式导出。

但是对于所需的转换有一个解决方法。在 X.509/SPKI 格式中,实际密钥(压缩或未压缩)位于末尾。前面的部分包含有关曲线、数据长度等的信息。对于具有 compressed 密钥和曲线 P-256 的 X.509/SPKI 密钥,前面的部分是:0x3039301306072a8648ce3d020106082a8648ce3d030107032200(注意 X.509 /SPKI 密钥与 uncompressed 密钥的前缀因数据长度不同而不同)并且可以用作转换压缩密钥的前缀0x02/0x03 + x 格式到 X.509/SPKI 格式:

from base64 import b64encode
from cryptography.hazmat.primitives import serialization

x509PemWithUncompressed = b'''-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE4q+ot7o3PuJqsBonZni2spVPvqLk
6FCiEF9GXzTyYZ1snzreGB+pyoiUUkz2/H60XWmQsgC7zZ60TBT0rVimtg==
-----END PUBLIC KEY-----'''

publicKey = serialization.load_pem_public_key(x509PemWithUncompressed) 
compressedKey = publicKey.public_bytes(serialization.Encoding.X962, serialization.PublicFormat.CompressedPoint)
x509withCompressed = bytes.fromhex('3039301306072a8648ce3d020106082a8648ce3d030107032200') + compressedKey 
print(b64encode(x509withCompressed).decode('utf-8')) # MDkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDIgAC4q+ot7o3PuJqsBonZni2spVPvqLk6FCiEF9GXzTyYZ0=