Skip to content

Latest commit

 

History

History
323 lines (265 loc) · 19.9 KB

sm2.md

File metadata and controls

323 lines (265 loc) · 19.9 KB

SM2椭圆曲线公钥密码算法应用指南

参考标准

  • 《GB/T 32918.1-2016 信息安全技术 SM2椭圆曲线公钥密码算法 第1部分:总则》
  • 《GB/T 32918.2-2016 信息安全技术 SM2椭圆曲线公钥密码算法 第2部分:数字签名算法》
  • 《GB/T 32918.3-2016 信息安全技术 SM2椭圆曲线公钥密码算法 第3部分:密钥交换协议》
  • 《GB/T 32918.4-2016 信息安全技术 SM2椭圆曲线公钥密码算法 第4部分:公钥加密算法》
  • 《GB/T 32918.5-2017 信息安全技术 SM2椭圆曲线公钥密码算法 第5部分:参数定义》
  • 《GB/T 35276-2017 信息安全技术 SM2密码算法使用规范》
  • 《GB/T 33560-2017 信息安全技术 密码应用标识规范》
  • 《GB/T 35275-2017 信息安全技术 SM2密码算法加密签名消息语法规范》(对应PKCS#7)

您可以从国家标准全文公开系统在线阅读这些标准。

概述

SM2既然是椭圆曲线公钥密码算法,它就和NIST P系列椭圆曲线公钥密码算法类似,特别是P-256。NIST P 系列椭圆曲线公钥密码算法主要用于数字签名和密钥交换,NIST没有定义基于椭圆曲线的公钥加密算法标准,SEC 1: Elliptic Curve Cryptography第五章定义了“Elliptic Curve Integrated Encryption Scheme (ECIES)”,不过应用不广。SM2公钥加密算法与其相似,只是MAC不同。感兴趣的同学可以进一步对比一下:

SM2 SEC 1
数字签名算法 ECDSA
密钥交换协议 ECMQV
公钥加密算法 ECIES

:最新的阿里KMS支持ECIES,难道客户有这个需求? ECIES_DH_SHA_1_XOR_HMAC:遵循SEC 1: Elliptic Curve Cryptography, Version 2.0标准,密钥协商算法采用ECDH,密钥派生算法采用 KDF2 with SHA-1,MAC算法采用HMAC-SHA-1,对称加密算法采用XOR。

业界对RSA非对称加密的安全性担忧与日俱增

SM2公私钥对

SM2公私钥对的话,要么是自己产生,要么是别的系统产生后通过某种方式传输给您的。

SM2公私钥对的生成

您可以通过调用sm2.GenerateKey方法产生SM2公私钥对,SM2的私钥通过组合方式扩展了ecdsa.PrivateKey,用于定义一些SM2特定的方法:

// PrivateKey represents an ECDSA SM2 private key.
// It implemented both crypto.Decrypter and crypto.Signer interfaces.
type PrivateKey struct {
	ecdsa.PrivateKey
	...
}

SM2的公钥类型沿用了ecdsa.PublicKey结构。注意:Go从v1.20开始,ecdsa.PublicKey增加了func (k *PublicKey) ECDH() (*ecdh.PublicKey, error)方法,这个方法对SM2的公钥不适用,SM2公钥请使用func PublicKeyToECDH(k *ecdsa.PublicKey) (*ecdh.PublicKey, error)

SM2公钥的解析、构造

通常情况下,公钥是通过PEM编码的文本传输的,您可以通过两步获得公钥:

  • 获得PEM中的block
  • 解析block中的公钥
func getPublicKey(pemContent []byte) (any, error) {
	block, _ := pem.Decode(pemContent)
	if block == nil {
		return nil, errors.New("Failed to parse PEM block")
	}
	return smx509.ParsePKIXPublicKey(block.Bytes)
}

由于smx509.ParsePKIXPublicKey返回any类型,您需要通过pub, ok := publicKey.(*ecdsa.PublicKey)转型。

有些应用可能会直接存储公钥的曲线点X, Y 坐标值,这时候,您可以通过以下类似方法构造公钥(假设输入的是点的非压缩序列化字节数组):

func ExampleNewPublicKey() {
	keypoints, _ := hex.DecodeString("048356e642a40ebd18d29ba3532fbd9f3bbee8f027c3f6f39a5ba2f870369f9988981f5efe55d1c5cdf6c0ef2b070847a14f7fdf4272a8df09c442f3058af94ba1")
	pub, err := sm2.NewPublicKey(keypoints)
	if err != nil {
		log.Fatalf("fail to new public key %v", err)
	}
	fmt.Printf("%x\n", elliptic.Marshal(sm2.P256(), pub.X, pub.Y))
	// Output: 048356e642a40ebd18d29ba3532fbd9f3bbee8f027c3f6f39a5ba2f870369f9988981f5efe55d1c5cdf6c0ef2b070847a14f7fdf4272a8df09c442f3058af94ba1
}

当然,您也可以使用ecdh包下的方法ecdh.P256().NewPublicKey来构造,目前只支持非压缩方式。

SM2私钥的解析、构造

私钥的封装格式主要有以下几种,相关讨论

  • RFC 5915 / SEC1 - http://www.secg.org/sec1-v2.pdf
  • PKCS#12
  • PKCS#8
  • PKCS#7(《GB/T 35275-2017 信息安全技术 SM2密码算法加密签名消息语法规范》)
  • CFCA自定义封装
  • 《GB/T 35276-2017 信息安全技术 SM2密码算法使用规范》

(存在于智能密码钥匙中,符合《GB/T 35291-2017 信息安全技术 智能密码钥匙应用接口规范》的,不在这里说明。)

所以,当您拿到一个密钥文件,您需要知道它的封装格式,然后选用合适的方法。PEM编码的密钥文本通常第一行会有相关信息。如果您得到的是一个ASN.1编码,那可能需要通过ASN.1结构和一些其中的OID来判断了。私钥信息是非常关键的信息,通常密钥文件被加密保护。可能是标准落后于应用的原因,目前这一块的互操作性可能差一点。

封装格式 解析方法
RFC 5915 / SEC1 smx509.ParseSM2PrivateKey
PKCS#12 使用 github.com/emmansun/go-pkcs12 解析
PKCS#8 smx509.ParsePKCS8PrivateKey可以处理未加密的;pkcs8.ParsePKCS8PrivateKeySM2可以处理未加密的,也可以处理加密的
PKCS#7 Cryptographic Message Syntax, 可以参考github.com/emmansun/pkcs7/sign_enveloped_test.go中的TestParseSignedEvnvelopedData,测试数据来自 https://www.gmcert.org/
CFCA自定义封装 顾名思义,这个封装是CFCA特定的,修改自PKCS#12,使用cfca.ParseSM2方法来解析
《GB/T 35276-2017 信息安全技术 SM2密码算法使用规范》 这个规范还比较新,使用sm2.ParseEnvelopedPrivateKey解析。典型的应用场景是CA机构返回CSRResponse, 里面包含签名证书、CA生成的SM2加密私钥以及相应的SM2加密证书,其中SM2加密私钥就用该规范定义的方式加密封装。请参考《GM/T 0092-2020 基于SM2算法的证书申请语法规范》

有些系统可能会直接存储、得到私钥的字节数组,那么您可以使用如下方法来构造私钥:

func ExampleNewPrivateKey() {
	keyBytes, _ := hex.DecodeString("6c5a0a0b2eed3cbec3e4f1252bfe0e28c504a1c6bf1999eebb0af9ef0f8e6c85")
	priv, err := sm2.NewPrivateKey(keyBytes)
	if err != nil {
		log.Fatalf("fail to new private key %v", err)
	}
	fmt.Printf("%x\n", priv.D.Bytes())
	// Output: 6c5a0a0b2eed3cbec3e4f1252bfe0e28c504a1c6bf1999eebb0af9ef0f8e6c85
}

func ExampleNewPrivateKeyFromInt() {
	key := big.NewInt(0x123456)
	priv, err := sm2.NewPrivateKeyFromInt(key)
	if err != nil {
		log.Fatalf("fail to new private key %v", err)
	}
	fmt.Printf("%x\n", priv.D.Bytes())
	// Output: 123456
}

当然,你也可以使用ecdh包的方法ecdh.P256().NewPrivateKey来构造私钥,您要确保输入的字节数组是256位(32字节)的,如果不是,请先自行处理。

数字签名算法

您可以直接使用sm2私钥的签名方法Sign

// This is a reference method to force SM2 standard with SDK [crypto.Signer].
func ExamplePrivateKey_Sign_forceSM2() {
	toSign := []byte("ShangMi SM2 Sign Standard")
	// real private key should be from secret storage
	privKey, _ := hex.DecodeString("6c5a0a0b2eed3cbec3e4f1252bfe0e28c504a1c6bf1999eebb0af9ef0f8e6c85")
	testkey, err := sm2.NewPrivateKey(privKey)
	if err != nil {
		log.Fatalf("fail to new private key %v", err)
	}

	// force SM2 sign standard and use default UID
	sig, err := testkey.Sign(rand.Reader, toSign, sm2.DefaultSM2SignerOpts)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error from sign: %s\n", err)
		return
	}

	// Since sign is a randomized function, signature will be
	// different each time.
	fmt.Printf("%x\n", sig)
}

我们通过SignerOpts参数来指示toSign已经是hash值,还是需要进行处理的原始信息。通常情况下,toSign传入原始信息、SignerOpts传入sm2.DefaultSM2SignerOpts。如果将来标准支持自定义的uid,那么您可以通过调用sm2.NewSM2SignerOption来构造一个自定义的SignerOpts

当然,您也可以通过调用SM2私钥的SignWithSM2方法,区别在于,Sign方法是crypto.Singer接口中定义的方法,而SignWithSM2方法是sm2.Signer接口中定义的方法。

您可以使用sm2.VerifyASN1WithSM2来校验SM2签名:

func ExampleVerifyASN1WithSM2() {
	// real public key should be from cert or public key pem file
	keypoints, _ := hex.DecodeString("048356e642a40ebd18d29ba3532fbd9f3bbee8f027c3f6f39a5ba2f870369f9988981f5efe55d1c5cdf6c0ef2b070847a14f7fdf4272a8df09c442f3058af94ba1")
	testkey, err := sm2.NewPublicKey(keypoints)
	if err != nil {
		log.Fatalf("fail to new public key %v", err)
	}

	toSign := []byte("ShangMi SM2 Sign Standard")
	signature, _ := hex.DecodeString("304402205b3a799bd94c9063120d7286769220af6b0fa127009af3e873c0e8742edc5f890220097968a4c8b040fd548d1456b33f470cabd8456bfea53e8a828f92f6d4bdcd77")

	ok := sm2.VerifyASN1WithSM2(testkey, nil, toSign, signature)

	fmt.Printf("%v\n", ok)
	// Output: true
}

密钥交换协议

这里有两个实现,一个是传统实现,位于sm2包中;另外一个参考最新go语言的实现在ecdh包中。在这里不详细介绍使用方法,一般只有tls/tlcp才会用到,普通应用通常不会涉及这一块,感兴趣的话可以参考github.com/Trisia/gotlcp中的应用。

公钥加密算法

请牢记,非对称加密算法通常不用于加密大量数据,而是用来加密对称加密密钥,我们在tlcp以及信封加密机制中能找到这种用法。

SM2公钥加密算法支持的密文编码格式有两种:

  • 简单串接方式: C1C3C2,曾经老的标准为 C1C2C3
  • ASN.1格式

SM2公钥加密示例:

func ExampleEncryptASN1() {
	// real public key should be from cert or public key pem file
	keypoints, _ := hex.DecodeString("048356e642a40ebd18d29ba3532fbd9f3bbee8f027c3f6f39a5ba2f870369f9988981f5efe55d1c5cdf6c0ef2b070847a14f7fdf4272a8df09c442f3058af94ba1")
	testkey, err := sm2.NewPublicKey(keypoints)
	if err != nil {
		log.Fatalf("fail to new public key %v", err)
	}

	secretMessage := []byte("send reinforcements, we're going to advance")

	// crypto/rand.Reader is a good source of entropy for randomizing the
	// encryption function.
	rng := rand.Reader

	ciphertext, err := sm2.EncryptASN1(rng, testkey, secretMessage)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error from encryption: %s\n", err)
		return
	}
	// Since encryption is a randomized function, ciphertext will be
	// different each time.
	fmt.Printf("Ciphertext: %x\n", ciphertext)
}

如果您需要普通拼接编码输出,您可以调用sm2.Encrypt方法,其中EncrypterOpts类型参数可以传入nil,表示默认C1C3C2。

sm2包也提供了辅助方法用于密文输出编码格式转换:您可以通过sm2.ASN1Ciphertext2Plain方法把ASN.1密文转换为简单拼接输出;反过来,您也可以通过sm2.PlainCiphertext2ASN1将简单拼接密文输出转换为ASN.1密文。你还可以通过sm2.AdjustCiphertextSplicingOrder方法来改变串接顺序。

SM2公钥加密算法解密示例:

func ExamplePrivateKey_Decrypt() {
	ciphertext, _ := hex.DecodeString("308194022100bd31001ce8d39a4a0119ff96d71334cd12d8b75bbc780f5bfc6e1efab535e85a02201839c075ff8bf761dcbe185c9750816410517001d6a130f6ab97fb23337cce150420ea82bd58d6a5394eb468a769ab48b6a26870ca075377eb06663780c920ea5ee0042be22abcf48e56ae9d29ac770d9de0d6b7094a874a2f8d26c26e0b1daaf4ff50a484b88163d04785b04585bb")

	// real private key should be from secret storage
	privKey, _ := hex.DecodeString("6c5a0a0b2eed3cbec3e4f1252bfe0e28c504a1c6bf1999eebb0af9ef0f8e6c85")
	testkey, err := sm2.NewPrivateKey(privKey)
	if err != nil {
		log.Fatalf("fail to new private key %v", err)
	}

	plaintext, err := testkey.Decrypt(nil, ciphertext, nil)
	if err != nil {
		fmt.Fprintf(os.Stderr, "Error from decryption: %s\n", err)
		return
	}

	fmt.Printf("Plaintext: %s\n", string(plaintext))
	// Output: Plaintext: send reinforcements, we're going to advance
}

这个SM2私钥的解密方法Decrypt,通常情况下,对crypto.DecrypterOpts类型参数,您只需传入nil,系统会自己检测输入密文是ASN.1还是普通拼接,但是,如果密文是老旧的C1||C2||C3拼接,请传入相应的crypto.DecrypterOpts类型参数,或者您可以先通过上面介绍的辅助函数转换一下。

具体API文档请参考:API Document

关于C1C2C3 和 C1C3C2

目前有据可查的是,国家密码管理局2010版SM2标准还是用C1C2C3格式,到了2012年标准就改用了C1C3C2,并延续至今。 其实C1C2C3是符合《SEC 1: Elliptic Curve Cryptography》(May 21, 2009 Version 2.0)Elliptic Curve Integrated Encryption Scheme 5.1.3中的密文输出描述:9. Output C = ($\overline{\text{R}}$, EM, D). Optionally, the ciphertext maybe output as C = $\overline{\text{R}}$ || EM || D. 这里 $\overline{\text{R}}$ 相对于C1, EM相对于C2, D相对于C3。

关于点到字节串的转换

我没有找到到哪个文档写有固定64字节转换的。从国家密码管理局2010年版的《SM2椭圆曲线公钥密码算法》,到2012年的GM/T 0003-2012 SM2椭圆曲线公钥密码算法,再到GB/T 32918-2016 信息安全技术 SM2椭圆曲线公钥密码算法,都在第一部分第四章的“点到字节串的转换”有详细说明。这也符合《SEC 1: Elliptic Curve Cryptography》(May 21, 2009 Version 2.0)中2.3.3 Elliptic-Curve-Point-to-Octet-String Conversion的描述。

来自AI的问答

问: Why some crypto implementations of EC point to octet string conversion without form indicator?
答: In Elliptic Curve Cryptography (ECC), the conversion of an elliptic curve point to an octet string can be done in several ways, typically either in compressed or uncompressed form. The form indicator (also known as the prefix) is used to denote whether the point is represented in its compressed or uncompressed form.

However, some implementations might choose to omit the form indicator for a few reasons:

  1. Simplicity: Omitting the form indicator simplifies the implementation, as there's no need to handle different forms of point representation.

  2. Assumed Knowledge: If it's known in advance that all points will be in a certain form (e.g., all points are always uncompressed), the form indicator might be deemed unnecessary.

  3. Compatibility: Not all systems or libraries support both forms of point representation. To ensure maximum compatibility, an implementation might choose to always use one form and thus omit the form indicator.

  4. Security: In some cases, to prevent certain types of attacks that rely on the form of the point, the form indicator might be omitted.

Please note that these are general reasons and the specific reason can vary depending on the exact context and the specific cryptographic protocol or system in use.

问:Won't this cause interoperability issues?
答: Yes, you're correct. Omitting the form indicator in the conversion of an elliptic curve point to an octet string can indeed cause interoperability issues.

When systems need to interoperate, they must agree on a common format for data exchange. If one system encodes elliptic curve points without a form indicator and another system expects the form indicator to be present, they won't be able to correctly interpret each other's data.

This is why standards like those defined by the SEC (Standards for Efficient Cryptography) specify the use of a form indicator. For example, the SEC 1 standard specifies that the form indicator should be 0x04 for uncompressed points and 0x02 or 0x03 for compressed points.

So, while omitting the form indicator might simplify the implementation or provide other benefits within a closed system, it can hinder interoperability with other systems. It's generally best to follow established standards when encoding and decoding cryptographic data to ensure maximum compatibility.

性能

v0.27.0开始,对大数据量的加解密做了优化处理,尤其是KDF并行计算。详情请参考SM2加解密性能

与KMS集成

国内云服务商的KMS服务大都提供SM2密钥,我们一般调用其API进行签名和解密,而验签和加密操作,一般在本地用公钥即可完成。不过需要注意的是,KMS提供的签名通常需要您在本地进行hash操作,而sm2签名的hash又比较特殊,下面示例供参考(自版本v0.24.0开始,您可以直接使用函数sm2.CalculateSM2Hash):

func calculateSM2Hash(pub *ecdsa.PublicKey, data, uid []byte) ([]byte, error) {
	if len(uid) == 0 {
		uid = defaultUID
	}
	za, err := sm2.CalculateZA(pub, uid)
	if err != nil {
		return nil, err
	}
	md := sm3.New()
	md.Write(za)
	md.Write(data)
	return md.Sum(nil), nil
}

公钥加密就没啥特殊,只要确保输出密文的编码格式和KMS一致即可。

SM2扩展应用

SM2的一些扩展应用,譬如从签名中恢复公钥、半同态加密、环签名等,大多尚处于POC状态,也无相关标准。其它扩展应用(但凡椭圆曲线公钥密码算法能用到的场合),包括但不限于:

从签名中恢复公钥

ECDSA 签名由两个数字(整数)组成:r 和 s。以太坊还引入了额外的变量 v(恢复标识符)。签名可以表示成 {r, s, v}。SM2 签名也由两个数字(整数)组成:r 和 s。签名算法中都只取随机点的X坐标,并对N取模,所以只有签名r和s的情况下,可以恢复出多个公钥。

// RecoverPublicKeysFromSM2Signature recovers two or four SM2 public keys from a given signature and hash.
// It takes the hash and signature as input and returns the recovered public keys as []*ecdsa.PublicKey.
// If the signature or hash is invalid, it returns an error.
// The function follows the SM2 algorithm to recover the public keys.
func RecoverPublicKeysFromSM2Signature(hash, sig []byte) ([]*ecdsa.PublicKey, error)

返回的结果:

  • 公钥0 - Rx = (r - e) mod N; Ry是偶数(compressFlag = 2)
  • 公钥1 - Rx = (r - e) mod N; Ry是奇数(compressFlag = 3)
  • 公钥2 - Rx = ((r - e) mod N) + N; Ry是偶数(compressFlag = 2)
  • 公钥3 - Rx = ((r - e) mod N) + N; Ry是奇数(compressFlag = 3)

Rx, Ry代表随机点R的X,Y坐标值。绝大多数情况下,只会返回两个公钥,后两者只有当(r - e) mod N的值小于P-1-N时才可能。

半同态加解密

EC-ElGamal with SM2的半同态加密(Partially Homomorphic Encryption, PHE), 支持uint32 或者 int32类型。Partially Homomorphic Encryption, EC-ElGamal with SM2.

环签名

Ring Signature Schemes Based on SM2 Digital Signature Algorithm.