最近发现,使用外部开源的国密库(https://github.com/duanhongyi/gmssl)进行 SM2 加密之后无法在腾讯云 KMS 系统上做解密,于是笔者针对这个问题做了一些调研、分析,最后解决了这个问题,这篇文章用来记录解决此问题的一些关键步骤和分析思路。
SM2 公钥加密产生的密文是一个字节串,它可以被分为三个主要部分:C1、C2、C3,其中C1是随机数计算出的椭圆曲线、C2是密文数据、C3是SM3杂凑值,C1固定为64字节,C2的长度与明文相同,C3的长度固定为32字节。
标准的 SM2 公钥长度一般为 65字节,其十六进制格式类似于:04c24942bccd2fb8822282cd0aca657cd53e91577c1a76d5d030a8807d35ada743ed0d3cbefcf24475d53333201388fc95ea518c90e9cd7b763f7c8ba8795dbcfc
其中前导的字节04
是固定的。
腾讯云 KMS 上下载的 SM2 公钥格式形如:
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEwklCvM0vuIIigs0KymV81T6RV3wa
dtXQMKiAfTWtp0PtDTy+/PJEddUzMyATiPyV6lGMkOnNe3Y/fIuoeV28/A==
-----END PUBLIC KEY-----
腾讯云 KMS 在做 SM2 解密时,其对于密文的格式要求为:
因此最开始进行编码是,其逻辑流程为:
先对 SM2 公钥做 base64 解码,然后使用 CryptSM2 接口做加密,并且在加密时专门指定了其模式为 1(即 C1C3C2 模式),asn1=True(即需要做 ASN1 编码),最后对密文做 base64 编码:
sm2_crypt = sm2.CryptSM2(public_key=base64.b64decode(public_key).hex(), private_key="", mode=1, asn1=True)
ciphertxt = sm2_crypt.encrypt(data.encode("utf-8"))
encrypted_text = base64.b64encode(ciphertxt).decode(encoding='utf-8')
最后解密时得到了错误反馈:
常见的解密失败的原因无非:
在KMS上,SM2私钥用户是不可见的,用户只能通过控制台下载公钥,这里我们反复对下载的公钥做了确认,因此可以排除是公钥与私钥不匹配。
从上述的操作步骤来看,在第一步,直接对 PEM 公钥做base64解码的动作,可能会存在问题。
因此接下来首先检查base64解码后的公钥数据是否为65字节:
origin_pem_data = base64.b64decode(pem_data)
print(origin_pem_data.hex(), len(origin_pem_data))
通过单独的 base64 解码并打印,我们发现公钥数据并不是标准的 65 字节,而是 91 字节:
3059301306072a8648ce3d020106082a811ccf5501822d03420004c24942bccd2fb8822282cd0aca657cd53e91577c1a76d5d030a8807d35ada743ed0d3cbefcf24475d53333201388fc95ea518c90e9cd7b763f7c8ba8795dbcfc
并且密钥数据的前置字节也并不是固定的 04,因此可以确定,公钥数据的确存在问题。
通过向 KMS 侧咨询,我们了解到,从 KMS 平台下载的公钥,其格式是做过 ASN1 编码的,而 ASN1 编码有很大概率会导致数据膨胀,因此我们接下来需要做的,就是对公钥做 ASN1 解码。
通用 KMS 侧反馈的信息,我们了解到 SM2 公钥的 ASN1 结构类似于:
-- 最外层是一个SEQUENCE
SEQUENCE (
-- 第一个元素是一个SEQUENCE
SEQUENCE (
-- OBJECT IDENTIFIER (1.2.840.10045.2.1)
OBJECT IDENTIFIER 1.2.840.10045.2.1,
-- OCTET STRING
OCTET STRING 00049CCFEDCCDD18BDF39D2EAC6460BB87D0375650C3D56B
A03A4F9265E601DEA5876FA06592399EB6D0E00348A4A1E5143
7510C8C0A4D245B14BF67F6884094D4A
),
-- 第二个元素是一个SEQUENCE
SEQUENCE (
-- OBJECT IDENTIFIER (1.2.156.10197.1.301)
OBJECT IDENTIFIER 1.2.156.10197.1.301,
-- BIT STRING
BIT STRING
)
)
于是乎我们做了如下的公钥 ASN1 解码操作:
class SM2PubKeyASN1Sequence(univ.Sequence):
componentType = namedtype.NamedTypes(
namedtype.NamedType('field-0',
univ.Sequence(
componentType = namedtype.NamedTypes(
namedtype.NamedType('oid-1', univ.ObjectIdentifier()),
namedtype.NamedType('oid-2', univ.ObjectIdentifier())
)
)),
namedtype.NamedType('field-1', univ.BitString())
)
def get_pubkey_from_pem(pem_data: str) -> str:
origin_pem_data = base64.b64decode(pem_data)
print(origin_pem_data.hex(), len(origin_pem_data))
decoded_data, _ = decoder.decode(base64.b64decode(pem_data), asn1Spec = SM2PubKeyASN1Sequence())
# print(decoded_data)
# 获取 field-1 的位串,并转换为十六进制字符串
bit_string = decoded_data['field-1'].asOctets()
hex_string = binascii.hexlify(bit_string).decode('utf-8')
print("Hexadecimal string of field-1:", hex_string)
return hex_string
最终我们成功获得了 65 字节的 SM2 公钥数据:
在使用正确的公钥加密后,我们发现仍然解密失败,因此我们怀疑是不是密文数据有问题。
我们检查了加密接口的参数设置:
可以确定的是,我们的参数设置是正确的,并没有问题。
但是为了保险,我们继续对 encrypt 函数做了检查:
我们发现 encrypt 函数的实现中,并没有做 ASN1 编码的动作,看来问题就出在这里,我们拿到的密文其实是 C1C3C2 模式的裸密文。
通过进一步检查,我们发现,它只在 verify 中有ASN1 相关的判断:
在确定了密文格式的确存在问题后,接下来我们只需要对密文做好 ASN1 编码即可。
通过查阅 SM2 相关的文档,我们找到了其 ASN1 的对象定义:
基于这个定义,我们可以进行如下的编码,来实现对裸密文的 ASN1 格式编码:
# 定义SM2 Ciphertext结构
class SM2_C1C3C2_ASN1_Ciphertext(univ.Sequence):
componentType = namedtype.NamedTypes(
namedtype.NamedType('c1x', univ.Integer()),
namedtype.NamedType('c1y', univ.Integer()),
namedtype.NamedType('c3', univ.OctetString()),
namedtype.NamedType('c2', univ.OctetString())
)
def parse_C1C3C2(cipher_bytes: bytes) -> Tuple[bytes, bytes, bytes]:
"""
解析密文,返回C1, C3, C2
SM2密文主要由C1、C2、C3三部分构成,
其中C1是随机数计算出的椭圆曲线、C2是密文数据、C3是SM3杂凑值,
C1固定为64字节,C2的长度与明文相同,C3的长度固定为32字节,
"""
c1 = cipher_bytes[0:64]
c3 = cipher_bytes[64: 32 + 64]
c2 = cipher_bytes[32 + 64:]
return c1, c3, c2
def do_sm2_c1c3c2_asn1_encode(raw_cipher_bytes: bytes) -> bytes:
c1, c3, c2 = parse_C1C3C2(raw_cipher_bytes)
c1x = c1[:32]
c1x_int = int.from_bytes(c1x, 'big')
c1y = c1[32:]
c1y_int = int.from_bytes(c1y, 'big')
# print('c1 part:', c1.hex(), len(c1))
# print('c2 part:', c2.hex(), len(c2))
# print('c3 part:', c3.hex(), len(c3))
ciphertext = SM2_C1C3C2_ASN1_Ciphertext()
ciphertext.setComponentByName('c1x', c1x_int)
ciphertext.setComponentByName('c1y', c1y_int)
ciphertext.setComponentByName('c3', c3)
ciphertext.setComponentByName('c2', c2)
encoded_ciphertext = encoder.encode(ciphertext)
return encoded_ciphertext
运行可以得到一下密文:
import base64
import binascii
import datetime
from typing import Tuple
from gmssl import sm2
from pyasn1.codec.der import decoder, encoder
from pyasn1.type import namedtype, univ
def sm2_encrypt(public_key_in_hex: str, data: bytes) -> Tuple[bytes, str]:
sm2_crypt = sm2.CryptSM2(public_key = public_key_in_hex, private_key = "", mode = 1, asn1 = True)
ciphertext = sm2_crypt.encrypt(data)
# print('cipher in hex:', ciphertext.hex(), len(ciphertext))
encrypted_text = base64.b64encode(ciphertext).decode(encoding = 'utf-8')
# print("encrypted_text in base64:", encrypted_text)
return ciphertext, encrypted_text
class SM2PubKeyASN1Sequence(univ.Sequence):
componentType = namedtype.NamedTypes(
namedtype.NamedType('field-0',
univ.Sequence(
componentType = namedtype.NamedTypes(
namedtype.NamedType('oid-1', univ.ObjectIdentifier()),
namedtype.NamedType('oid-2', univ.ObjectIdentifier())
)
)),
namedtype.NamedType('field-1', univ.BitString())
)
def get_pubkey_from_pem(pem_data: str) -> str:
origin_pem_data = base64.b64decode(pem_data)
# print(origin_pem_data.hex(), len(origin_pem_data))
decoded_data, _ = decoder.decode(base64.b64decode(pem_data), asn1Spec = SM2PubKeyASN1Sequence())
# print(decoded_data)
# 获取 field-1 的位串,并转换为十六进制字符串
bit_string = decoded_data['field-1'].asOctets()
hex_string = binascii.hexlify(bit_string).decode('utf-8')
# print("Hexadecimal string of field-1:", hex_string)
return hex_string
def parse_C1C3C2(cipher_bytes: bytes) -> Tuple[bytes, bytes, bytes]:
"""
解析密文,返回C1, C3, C2
SM2密文主要由C1、C2、C3三部分构成,
其中C1是随机数计算出的椭圆曲线、C2是密文数据、C3是SM3杂凑值,
C1固定为64字节,C2的长度与明文相同,C3的长度固定为32字节,
"""
c1 = cipher_bytes[0:64]
c3 = cipher_bytes[64: 32 + 64]
c2 = cipher_bytes[32 + 64:]
return c1, c3, c2
# 定义SM2 Ciphertext结构
class SM2_C1C3C2_ASN1_Ciphertext(univ.Sequence):
componentType = namedtype.NamedTypes(
namedtype.NamedType('c1x', univ.Integer()),
namedtype.NamedType('c1y', univ.Integer()),
namedtype.NamedType('c3', univ.OctetString()),
namedtype.NamedType('c2', univ.OctetString())
)
def do_sm2_c1c3c2_asn1_encode(raw_cipher_bytes: bytes) -> bytes:
c1, c3, c2 = parse_C1C3C2(raw_cipher_bytes)
c1x = c1[:32]
c1x_int = int.from_bytes(c1x, 'big')
c1y = c1[32:]
c1y_int = int.from_bytes(c1y, 'big')
# print('c1 part:', c1.hex(), len(c1))
# print('c2 part:', c2.hex(), len(c2))
# print('c3 part:', c3.hex(), len(c3))
ciphertext = SM2_C1C3C2_ASN1_Ciphertext()
ciphertext.setComponentByName('c1x', c1x_int)
ciphertext.setComponentByName('c1y', c1y_int)
ciphertext.setComponentByName('c3', c3)
ciphertext.setComponentByName('c2', c2)
encoded_ciphertext = encoder.encode(ciphertext)
return encoded_ciphertext
if __name__ == '__main__':
# 客户的公钥
public_key = """
MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEwklCvM0vuIIigs0KymV81T6RV3wa
dtXQMKiAfTWtp0PtDTy+/PJEddUzMyATiPyV6lGMkOnNe3Y/fIuoeV28/A==
"""
# 我自己的公钥
# public_key = """
# MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEnM/twM3Ri9850urGRgu4fQN1ZQw9
# VroDpPkmXmAdHqWHb6BlkjmettDgA0ikHlFDdRDIwKTSRbFL9n9ohAlNSg==
# """
# 获取公钥的十六进制字符串
hex_pub_key = get_pubkey_from_pem(public_key)
print('hex_pub_key:', hex_pub_key)
# 待加密数据
data_bytes = datetime.datetime.now().isoformat(timespec = 'microseconds').encode('utf-8')
print('data_bytes:', data_bytes.hex())
# 使用外部开源库加密
ret_cipher_bytes, cipher_base64 = sm2_encrypt(hex_pub_key, data_bytes)
ret_asn1_cipher = do_sm2_c1c3c2_asn1_encode(ret_cipher_bytes)
print('asn1_cipher:', ret_asn1_cipher.hex())
print('asn1 base64 cipher:', base64.b64encode(ret_asn1_cipher).decode('utf-8'))
tmp, _ = decoder.decode(ret_asn1_cipher, asn1Spec = SM2_C1C3C2_ASN1_Ciphertext())
print('测试外部库生成的 C1C3C2 密文被自定义 ASN1编码后能否解密:', tmp)
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。