前往小程序,Get更优阅读体验!
立即前往
发布
社区首页 >专栏 >国密 SM2 公钥加密密文格式记录

国密 SM2 公钥加密密文格式记录

原创
作者头像
bowenerchen
发布2024-12-18 19:34:07
发布2024-12-18 19:34:07
1.4K30
代码可运行
举报
文章被收录于专栏:数安视界数安视界
运行总次数:0
代码可运行

背景

最近发现,使用外部开源的国密库https://github.com/duanhongyi/gmssl)进行 SM2 加密之后无法在腾讯云 KMS 系统上做解密,于是笔者针对这个问题做了一些调研、分析,最后解决了这个问题,这篇文章用来记录解决此问题的一些关键步骤和分析思路。

SM2的公钥和密文格式

SM2 公钥加密产生的密文是一个字节串,它可以被分为三个主要部分:C1、C2、C3,其中C1是随机数计算出的椭圆曲线、C2是密文数据、C3是SM3杂凑值,C1固定为64字节,C2的长度与明文相同,C3的长度固定为32字节。

标准的 SM2 公钥长度一般为 65字节,其十六进制格式类似于:04c24942bccd2fb8822282cd0aca657cd53e91577c1a76d5d030a8807d35ada743ed0d3cbefcf24475d53333201388fc95ea518c90e9cd7b763f7c8ba8795dbcfc

其中前导的字节04是固定的。

遇到的问题

腾讯云 KMS 上下载的 SM2 公钥格式形如:

代码语言:json
复制
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEwklCvM0vuIIigs0KymV81T6RV3wa
dtXQMKiAfTWtp0PtDTy+/PJEddUzMyATiPyV6lGMkOnNe3Y/fIuoeV28/A==
-----END PUBLIC KEY-----

腾讯云 KMS 在做 SM2 解密时,其对于密文的格式要求为:

腾讯云 KMS 密文格式要求
腾讯云 KMS 密文格式要求

因此最开始进行编码是,其逻辑流程为:

先对 SM2 公钥做 base64 解码,然后使用 CryptSM2 接口做加密,并且在加密时专门指定了其模式为 1(即 C1C3C2 模式),asn1=True(即需要做 ASN1 编码),最后对密文做 base64 编码:

代码语言:python
代码运行次数:0
复制
        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 解密失败
KMS SM2 解密失败

问题分析与应对措施

解密失败的几种可能

常见的解密失败的原因无非:

  • 公钥与私钥不匹配
  • 错误的公钥数据
  • 密文数据错误

公钥私钥不匹配

在KMS上,SM2私钥用户是不可见的,用户只能通过控制台下载公钥,这里我们反复对下载的公钥做了确认,因此可以排除是公钥与私钥不匹配。

错误的公钥数据

从上述的操作步骤来看,在第一步,直接对 PEM 公钥做base64解码的动作,可能会存在问题。

因此接下来首先检查base64解码后的公钥数据是否为65字节:

代码语言:python
代码运行次数:0
复制
    origin_pem_data = base64.b64decode(pem_data)
    print(origin_pem_data.hex(), len(origin_pem_data))

通过单独的 base64 解码并打印,我们发现公钥数据并不是标准的 65 字节,而是 91 字节:

代码语言:python
代码运行次数:0
复制
3059301306072a8648ce3d020106082a811ccf5501822d03420004c24942bccd2fb8822282cd0aca657cd53e91577c1a76d5d030a8807d35ada743ed0d3cbefcf24475d53333201388fc95ea518c90e9cd7b763f7c8ba8795dbcfc

并且密钥数据的前置字节也并不是固定的 04,因此可以确定,公钥数据的确存在问题。

如何获取正确的公钥数据

通过向 KMS 侧咨询,我们了解到,从 KMS 平台下载的公钥,其格式是做过 ASN1 编码的,而 ASN1 编码有很大概率会导致数据膨胀,因此我们接下来需要做的,就是对公钥做 ASN1 解码。

通用 KMS 侧反馈的信息,我们了解到 SM2 公钥的 ASN1 结构类似于:

代码语言:json
复制
-- 最外层是一个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 解码操作:

代码语言:python
代码运行次数:0
复制
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 公钥数据:

SM2 公钥数据
SM2 公钥数据

使用正确的公钥但是仍然解密失败

在使用正确的公钥加密后,我们发现仍然解密失败,因此我们怀疑是不是密文数据有问题。

我们检查了加密接口的参数设置:

CryptSM2 参数设置
CryptSM2 参数设置
CryptSM2 参数定义
CryptSM2 参数定义

可以确定的是,我们的参数设置是正确的,并没有问题。

但是为了保险,我们继续对 encrypt 函数做了检查:

encrypt 函数仅做了 mode 的判断
encrypt 函数仅做了 mode 的判断

我们发现 encrypt 函数的实现中,并没有做 ASN1 编码的动作,看来问题就出在这里,我们拿到的密文其实是 C1C3C2 模式的裸密文。

通过进一步检查,我们发现,它只在 verify 中有ASN1 相关的判断:

verify 函数中判断了 ASN1 编码
verify 函数中判断了 ASN1 编码

对密文重新做 ASN1 编码

在确定了密文格式的确存在问题后,接下来我们只需要对密文做好 ASN1 编码即可。

通过查阅 SM2 相关的文档,我们找到了其 ASN1 的对象定义:

SM2 密文 ASN1 定义
SM2 密文 ASN1 定义

基于这个定义,我们可以进行如下的编码,来实现对裸密文的 ASN1 格式编码:

代码语言:python
代码运行次数:0
复制
# 定义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

运行可以得到一下密文:

经过 ASN1 编码的密文
经过 ASN1 编码的密文
KMS解密成功
KMS解密成功
解密数据符合预期
解密数据符合预期

完整的测试代码

代码语言:python
代码运行次数:0
复制
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 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景
  • SM2的公钥和密文格式
  • 遇到的问题
  • 问题分析与应对措施
    • 解密失败的几种可能
    • 公钥私钥不匹配
    • 错误的公钥数据
    • 如何获取正确的公钥数据
    • 使用正确的公钥但是仍然解密失败
    • 对密文重新做 ASN1 编码
  • 完整的测试代码
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档