github.com/emmansun/gmsm@v0.29.1/docs/sm2.md (about) 1 # SM2椭圆曲线公钥密码算法应用指南 2 3 ## 参考标准 4 * 《GB/T 32918.1-2016 信息安全技术 SM2椭圆曲线公钥密码算法 第1部分:总则》 5 * 《GB/T 32918.2-2016 信息安全技术 SM2椭圆曲线公钥密码算法 第2部分:数字签名算法》 6 * 《GB/T 32918.3-2016 信息安全技术 SM2椭圆曲线公钥密码算法 第3部分:密钥交换协议》 7 * 《GB/T 32918.4-2016 信息安全技术 SM2椭圆曲线公钥密码算法 第4部分:公钥加密算法》 8 * 《GB/T 32918.5-2017 信息安全技术 SM2椭圆曲线公钥密码算法 第5部分:参数定义》 9 * 《GB/T 35276-2017 信息安全技术 SM2密码算法使用规范》 10 * 《GB/T 33560-2017 信息安全技术 密码应用标识规范》 11 * 《GB/T 35275-2017 信息安全技术 SM2密码算法加密签名消息语法规范》(对应PKCS#7) 12 13 您可以从[国家标准全文公开系统](https://openstd.samr.gov.cn/)在线阅读这些标准。 14 15 ## 概述 16 SM2既然是椭圆曲线公钥密码算法,它就和NIST P系列椭圆曲线公钥密码算法类似,特别是P-256。NIST P 系列椭圆曲线公钥密码算法主要用于数字签名和密钥交换,NIST没有定义基于椭圆曲线的公钥加密算法标准,[SEC 1: Elliptic Curve Cryptography](https://www.secg.org/sec1-v2.pdf)第五章定义了“Elliptic Curve Integrated Encryption Scheme (ECIES)”,不过应用不广。SM2公钥加密算法与其相似,只是MAC不同。感兴趣的同学可以进一步对比一下: 17 18 | SM2 | SEC 1 | 19 | :--- | :--- | 20 | 数字签名算法 | ECDSA | 21 | 密钥交换协议 | ECMQV | 22 | 公钥加密算法 | ECIES | 23 24 **注**:最新的阿里KMS支持ECIES,难道客户有这个需求? 25 ECIES_DH_SHA_1_XOR_HMAC:遵循[SEC 1: Elliptic Curve Cryptography, Version 2.0](https://www.secg.org/sec1-v2.pdf)标准,密钥协商算法采用ECDH,密钥派生算法采用 KDF2 with SHA-1,MAC算法采用HMAC-SHA-1,对称加密算法采用XOR。 26 27 **业界对RSA非对称加密的安全性担忧与日俱增**: 28 * [The Marvin Attack](https://people.redhat.com/~hkario/marvin/) 29 * [CVE-2023-45287 Detail](https://nvd.nist.gov/vuln/detail/CVE-2023-45287) 30 * [Vulnerability Report: GO-2023-2375](https://pkg.go.dev/vuln/GO-2023-2375) 31 * [Seriously, stop using RSA](https://blog.trailofbits.com/2019/07/08/fuck-rsa/) 32 33 ## SM2公私钥对 34 SM2公私钥对的话,要么是自己产生,要么是别的系统产生后通过某种方式传输给您的。 35 36 ### SM2公私钥对的生成 37 您可以通过调用```sm2.GenerateKey```方法产生SM2公私钥对,SM2的私钥通过组合方式扩展了```ecdsa.PrivateKey```,用于定义一些SM2特定的方法: 38 ```go 39 // PrivateKey represents an ECDSA SM2 private key. 40 // It implemented both crypto.Decrypter and crypto.Signer interfaces. 41 type PrivateKey struct { 42 ecdsa.PrivateKey 43 ... 44 } 45 ``` 46 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)```。 47 48 ### SM2公钥的解析、构造 49 通常情况下,公钥是通过PEM编码的文本传输的,您可以通过两步获得公钥: 50 * 获得PEM中的block 51 * 解析block中的公钥 52 53 ```go 54 func getPublicKey(pemContent []byte) (any, error) { 55 block, _ := pem.Decode(pemContent) 56 if block == nil { 57 return nil, errors.New("Failed to parse PEM block") 58 } 59 return smx509.ParsePKIXPublicKey(block.Bytes) 60 } 61 ``` 62 由于```smx509.ParsePKIXPublicKey```返回any类型,您需要通过```pub, ok := publicKey.(*ecdsa.PublicKey)```转型。 63 64 有些应用可能会直接存储公钥的曲线点X, Y 坐标值,这时候,您可以通过以下类似方法构造公钥(假设输入的是点的非压缩序列化字节数组): 65 ```go 66 func ExampleNewPublicKey() { 67 keypoints, _ := hex.DecodeString("048356e642a40ebd18d29ba3532fbd9f3bbee8f027c3f6f39a5ba2f870369f9988981f5efe55d1c5cdf6c0ef2b070847a14f7fdf4272a8df09c442f3058af94ba1") 68 pub, err := sm2.NewPublicKey(keypoints) 69 if err != nil { 70 log.Fatalf("fail to new public key %v", err) 71 } 72 fmt.Printf("%x\n", elliptic.Marshal(sm2.P256(), pub.X, pub.Y)) 73 // Output: 048356e642a40ebd18d29ba3532fbd9f3bbee8f027c3f6f39a5ba2f870369f9988981f5efe55d1c5cdf6c0ef2b070847a14f7fdf4272a8df09c442f3058af94ba1 74 } 75 ``` 76 当然,您也可以使用ecdh包下的方法```ecdh.P256().NewPublicKey```来构造,目前只支持非压缩方式。 77 78 ### SM2私钥的解析、构造 79 私钥的封装格式主要有以下几种,[相关讨论](https://github.com/emmansun/gmsm/issues/104): 80 * RFC 5915 / SEC1 - http://www.secg.org/sec1-v2.pdf 81 * PKCS#12 82 * PKCS#8 83 * PKCS#7(《GB/T 35275-2017 信息安全技术 SM2密码算法加密签名消息语法规范》) 84 * CFCA自定义封装 85 * 《GB/T 35276-2017 信息安全技术 SM2密码算法使用规范》 86 87 (存在于智能密码钥匙中,符合《GB/T 35291-2017 信息安全技术 智能密码钥匙应用接口规范》的,不在这里说明。) 88 89 所以,当您拿到一个密钥文件,您需要知道它的封装格式,然后选用合适的方法。PEM编码的密钥文本通常第一行会有相关信息。如果您得到的是一个ASN.1编码,那可能需要通过ASN.1结构和一些其中的OID来判断了。私钥信息是非常关键的信息,通常密钥文件被加密保护。可能是标准落后于应用的原因,目前这一块的互操作性可能差一点。 90 91 | 封装格式 | 解析方法 | 92 | :--- | :--- | 93 | RFC 5915 / SEC1 | ```smx509.ParseSM2PrivateKey``` | 94 | PKCS#12 | 使用 github.com/emmansun/go-pkcs12 解析 | 95 | PKCS#8 | ```smx509.ParsePKCS8PrivateKey```可以处理未加密的;```pkcs8.ParsePKCS8PrivateKeySM2```可以处理未加密的,也可以处理加密的 | 96 | PKCS#7 | Cryptographic Message Syntax, 可以参考github.com/emmansun/pkcs7/sign_enveloped_test.go中的```TestParseSignedEvnvelopedData```,测试数据来自 https://www.gmcert.org/ | 97 | CFCA自定义封装 | 顾名思义,这个封装是CFCA特定的,修改自PKCS#12,使用```cfca.ParseSM2```方法来解析 | 98 |《GB/T 35276-2017 信息安全技术 SM2密码算法使用规范》| 这个规范还比较新,使用```sm2.ParseEnvelopedPrivateKey```解析。典型的应用场景是CA机构返回CSRResponse, 里面包含签名证书、CA生成的SM2加密私钥以及相应的SM2加密证书,其中SM2加密私钥就用该规范定义的方式加密封装。请参考《GM/T 0092-2020 基于SM2算法的证书申请语法规范》 | 99 100 有些系统可能会直接存储、得到私钥的字节数组,那么您可以使用如下方法来构造私钥: 101 ```go 102 func ExampleNewPrivateKey() { 103 keyBytes, _ := hex.DecodeString("6c5a0a0b2eed3cbec3e4f1252bfe0e28c504a1c6bf1999eebb0af9ef0f8e6c85") 104 priv, err := sm2.NewPrivateKey(keyBytes) 105 if err != nil { 106 log.Fatalf("fail to new private key %v", err) 107 } 108 fmt.Printf("%x\n", priv.D.Bytes()) 109 // Output: 6c5a0a0b2eed3cbec3e4f1252bfe0e28c504a1c6bf1999eebb0af9ef0f8e6c85 110 } 111 112 func ExampleNewPrivateKeyFromInt() { 113 key := big.NewInt(0x123456) 114 priv, err := sm2.NewPrivateKeyFromInt(key) 115 if err != nil { 116 log.Fatalf("fail to new private key %v", err) 117 } 118 fmt.Printf("%x\n", priv.D.Bytes()) 119 // Output: 123456 120 } 121 ``` 122 当然,你也可以使用ecdh包的方法```ecdh.P256().NewPrivateKey```来构造私钥,您要确保输入的字节数组是256位(32字节)的,如果不是,请先自行处理。 123 124 ### 关于《GM/T 0091-2020 基于口令的密钥派生规范》 125 这个规范就是[RFC8018 PKCS#5](https://datatracker.ietf.org/doc/html/rfc8018) 国密定制版,其中PBES/PBKDF/PBMAC使用了不同的OID,但是这些OID似乎没有注册过。而且表A.1 中**id-hmacWithSM3**的OID为没有注册过的**1.2.156.10197.1.401.3.1**,和我们常用的**1.2.156.10197.1.401.2**不一致,也与该文档本身附录C不一致。不知道哪个产品遵从了这个行业规范。 126 127 | 对象标识符OID | 对象标识符定义 | 128 | :--- | :--- | 129 | 1.2.156.10197.6.1.4.1.5 | 基于口令的密钥派生规范 | 130 | 1.2.156.10197.6.1.4.1.5.1 | 基于口令的密钥派生函数 PBKDF (其实就是PBKDF2) | 131 | 1.2.156.10197.6.1.4.1.5.2 | 基于口令的加密方案PBES (其实就是PBES2) | 132 | 1.2.156.10197.6.1.4.1.5.3 | 基于口令的消息鉴别码PBMAC | 133 134 规范中让人困惑的地方: 135 1. 附录 **A.2 伪随机函数** 引入了这个新的**id-hmacWithSM3**的OID**1.2.156.10197.1.401.3.1** 136 2. 附录 **A.4 基础消息鉴别方案**,给出的实例片段中的OID为**1.2.156.10197.1.401**,让人怀疑这个实例抄的是**PKCS12-MAC**,而不是**PBMAC1**。 137 3. 附录 **B.2 PBES结构**,**PBES-Encs**竟然给出了**pbeWithSM3AndSM4-CBC OJBECTIDENTIFIER ::= {1.2.156.10197.6.1.4.1.12.1.1}**,难不成又要抄**PBES1**? 138 4. 附录 **C. ASN.1 结构定义**,**id-hmacWithSM3**的OID又是**1.2.156.10197.1.401.2**。 139 140 ## 数字签名算法 141 您可以直接使用sm2私钥的签名方法```Sign```: 142 ```go 143 // This is a reference method to force SM2 standard with SDK [crypto.Signer]. 144 func ExamplePrivateKey_Sign_forceSM2() { 145 toSign := []byte("ShangMi SM2 Sign Standard") 146 // real private key should be from secret storage 147 privKey, _ := hex.DecodeString("6c5a0a0b2eed3cbec3e4f1252bfe0e28c504a1c6bf1999eebb0af9ef0f8e6c85") 148 testkey, err := sm2.NewPrivateKey(privKey) 149 if err != nil { 150 log.Fatalf("fail to new private key %v", err) 151 } 152 153 // force SM2 sign standard and use default UID 154 sig, err := testkey.Sign(rand.Reader, toSign, sm2.DefaultSM2SignerOpts) 155 if err != nil { 156 fmt.Fprintf(os.Stderr, "Error from sign: %s\n", err) 157 return 158 } 159 160 // Since sign is a randomized function, signature will be 161 // different each time. 162 fmt.Printf("%x\n", sig) 163 } 164 ``` 165 我们通过```SignerOpts```参数来指示```toSign```已经是hash值,还是需要进行处理的原始信息。通常情况下,```toSign```传入原始信息、```SignerOpts```传入```sm2.DefaultSM2SignerOpts```。如果将来标准支持自定义的uid,那么您可以通过调用```sm2.NewSM2SignerOption```来构造一个自定义的```SignerOpts```。 166 167 当然,您也可以通过调用SM2私钥的```SignWithSM2```方法,区别在于,```Sign```方法是```crypto.Singer```接口中定义的方法,而```SignWithSM2```方法是```sm2.Signer```接口中定义的方法。 168 169 您可以使用```sm2.VerifyASN1WithSM2```来校验SM2签名: 170 ```go 171 func ExampleVerifyASN1WithSM2() { 172 // real public key should be from cert or public key pem file 173 keypoints, _ := hex.DecodeString("048356e642a40ebd18d29ba3532fbd9f3bbee8f027c3f6f39a5ba2f870369f9988981f5efe55d1c5cdf6c0ef2b070847a14f7fdf4272a8df09c442f3058af94ba1") 174 testkey, err := sm2.NewPublicKey(keypoints) 175 if err != nil { 176 log.Fatalf("fail to new public key %v", err) 177 } 178 179 toSign := []byte("ShangMi SM2 Sign Standard") 180 signature, _ := hex.DecodeString("304402205b3a799bd94c9063120d7286769220af6b0fa127009af3e873c0e8742edc5f890220097968a4c8b040fd548d1456b33f470cabd8456bfea53e8a828f92f6d4bdcd77") 181 182 ok := sm2.VerifyASN1WithSM2(testkey, nil, toSign, signature) 183 184 fmt.Printf("%v\n", ok) 185 // Output: true 186 } 187 ``` 188 189 ## 密钥交换协议 190 这里有两个实现,一个是传统实现,位于sm2包中;另外一个参考最新go语言的实现在ecdh包中。在这里不详细介绍使用方法,一般只有tls/tlcp才会用到,普通应用通常不会涉及这一块,感兴趣的话可以参考github.com/Trisia/gotlcp中的应用。 191 192 ## 公钥加密算法 193 请牢记,非对称加密算法通常不用于加密大量数据,而是用来加密对称加密密钥,我们在**tlcp**以及**信封加密**机制中能找到这种用法。 194 195 SM2公钥加密算法支持的密文编码格式有两种: 196 * 简单串接方式: C1C3C2,曾经老的标准为 C1C2C3 197 * ASN.1格式 198 199 SM2公钥加密示例: 200 ```go 201 func ExampleEncryptASN1() { 202 // real public key should be from cert or public key pem file 203 keypoints, _ := hex.DecodeString("048356e642a40ebd18d29ba3532fbd9f3bbee8f027c3f6f39a5ba2f870369f9988981f5efe55d1c5cdf6c0ef2b070847a14f7fdf4272a8df09c442f3058af94ba1") 204 testkey, err := sm2.NewPublicKey(keypoints) 205 if err != nil { 206 log.Fatalf("fail to new public key %v", err) 207 } 208 209 secretMessage := []byte("send reinforcements, we're going to advance") 210 211 // crypto/rand.Reader is a good source of entropy for randomizing the 212 // encryption function. 213 rng := rand.Reader 214 215 ciphertext, err := sm2.EncryptASN1(rng, testkey, secretMessage) 216 if err != nil { 217 fmt.Fprintf(os.Stderr, "Error from encryption: %s\n", err) 218 return 219 } 220 // Since encryption is a randomized function, ciphertext will be 221 // different each time. 222 fmt.Printf("Ciphertext: %x\n", ciphertext) 223 } 224 ``` 225 如果您需要普通拼接编码输出,您可以调用```sm2.Encrypt```方法,其中```EncrypterOpts```类型参数可以传入nil,表示默认C1C3C2。 226 227 sm2包也提供了辅助方法用于密文输出编码格式转换:您可以通过```sm2.ASN1Ciphertext2Plain```方法把ASN.1密文转换为简单拼接输出;反过来,您也可以通过```sm2.PlainCiphertext2ASN1```将简单拼接密文输出转换为ASN.1密文。你还可以通过```sm2.AdjustCiphertextSplicingOrder```方法来改变串接顺序。 228 229 SM2公钥加密算法解密示例: 230 ```go 231 func ExamplePrivateKey_Decrypt() { 232 ciphertext, _ := hex.DecodeString("308194022100bd31001ce8d39a4a0119ff96d71334cd12d8b75bbc780f5bfc6e1efab535e85a02201839c075ff8bf761dcbe185c9750816410517001d6a130f6ab97fb23337cce150420ea82bd58d6a5394eb468a769ab48b6a26870ca075377eb06663780c920ea5ee0042be22abcf48e56ae9d29ac770d9de0d6b7094a874a2f8d26c26e0b1daaf4ff50a484b88163d04785b04585bb") 233 234 // real private key should be from secret storage 235 privKey, _ := hex.DecodeString("6c5a0a0b2eed3cbec3e4f1252bfe0e28c504a1c6bf1999eebb0af9ef0f8e6c85") 236 testkey, err := sm2.NewPrivateKey(privKey) 237 if err != nil { 238 log.Fatalf("fail to new private key %v", err) 239 } 240 241 plaintext, err := testkey.Decrypt(nil, ciphertext, nil) 242 if err != nil { 243 fmt.Fprintf(os.Stderr, "Error from decryption: %s\n", err) 244 return 245 } 246 247 fmt.Printf("Plaintext: %s\n", string(plaintext)) 248 // Output: Plaintext: send reinforcements, we're going to advance 249 } 250 ``` 251 这个SM2私钥的解密方法```Decrypt```,通常情况下,对```crypto.DecrypterOpts```类型参数,您只需传入nil,系统会自己检测输入密文是ASN.1还是普通拼接,但是,如果密文是老旧的C1||C2||C3拼接,请传入相应的```crypto.DecrypterOpts```类型参数,或者您可以先通过上面介绍的辅助函数转换一下。 252 253 具体API文档请参考:[API Document](https://godoc.org/github.com/emmansun/gmsm) 254 255 ### 关于C1C2C3 和 C1C3C2 256 目前有据可查的是,国家密码管理局2010版SM2标准还是用C1C2C3格式,到了2012年标准就改用了C1C3C2,并延续至今。 257 其实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。 258 259 ### 关于点到字节串的转换 260 我没有找到到哪个文档写有固定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的描述。 261 262 #### 来自AI的问答 263 **问: Why some crypto implementations of EC point to octet string conversion without form indicator?** 264 **答:** 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. 265 266 However, some implementations might choose to omit the form indicator for a few reasons: 267 268 1. **Simplicity**: Omitting the form indicator simplifies the implementation, as there's no need to handle different forms of point representation. 269 270 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. 271 272 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. 273 274 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. 275 276 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. 277 278 **问:Won't this cause interoperability issues?** 279 **答:** 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. 280 281 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. 282 283 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. 284 285 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. 286 287 ### 性能 288 从**v0.27.0**开始,对大数据量的加解密做了优化处理,尤其是KDF并行计算。详情请参考[SM2加解密性能](https://github.com/emmansun/gmsm/wiki/SM2%E5%8A%A0%E8%A7%A3%E5%AF%86%E6%80%A7%E8%83%BD)。 289 290 ## 与KMS集成 291 国内云服务商的KMS服务大都提供SM2密钥,我们一般调用其API进行签名和解密,而验签和加密操作,一般在本地用公钥即可完成。不过需要注意的是,KMS提供的签名通常需要您在本地进行hash操作,而sm2签名的hash又比较特殊,下面示例供参考(自版本**v0.24.0**开始,您可以直接使用函数```sm2.CalculateSM2Hash```): 292 ```go 293 func calculateSM2Hash(pub *ecdsa.PublicKey, data, uid []byte) ([]byte, error) { 294 if len(uid) == 0 { 295 uid = defaultUID 296 } 297 za, err := sm2.CalculateZA(pub, uid) 298 if err != nil { 299 return nil, err 300 } 301 md := sm3.New() 302 md.Write(za) 303 md.Write(data) 304 return md.Sum(nil), nil 305 } 306 ``` 307 公钥加密就没啥特殊,只要确保输出密文的编码格式和KMS一致即可。 308 309 ## SM2扩展应用 310 SM2的一些扩展应用,譬如从签名中恢复公钥、半同态加密、环签名等,大多尚处于POC状态,也无相关标准。其它扩展应用(但凡椭圆曲线公钥密码算法能用到的场合),包括但不限于: 311 * [确定性签名](https://datatracker.ietf.org/doc/html/rfc6979) 312 * [可验证随机函数ECVRF](https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-vrf-04) 313 * 盲签名 314 * 群签名 315 * 门限签名 316 * [Pederson承诺](https://crypto.stackexchange.com/questions/64437/what-is-a-pedersen-commitment) 317 318 ### 从签名中恢复公钥 319 ECDSA 签名由两个数字(整数)组成:r 和 s。以太坊还引入了额外的变量 v(恢复标识符)。签名可以表示成 {r, s, v}。SM2 签名也由两个数字(整数)组成:r 和 s。签名算法中都只取随机点的X坐标,并对N取模,所以只有签名r和s的情况下,可以恢复出多个公钥。 320 ```go 321 // RecoverPublicKeysFromSM2Signature recovers two or four SM2 public keys from a given signature and hash. 322 // It takes the hash and signature as input and returns the recovered public keys as []*ecdsa.PublicKey. 323 // If the signature or hash is invalid, it returns an error. 324 // The function follows the SM2 algorithm to recover the public keys. 325 func RecoverPublicKeysFromSM2Signature(hash, sig []byte) ([]*ecdsa.PublicKey, error) 326 ``` 327 返回的结果: 328 * 公钥0 - Rx = (r - e) mod N; Ry是偶数(compressFlag = 2) 329 * 公钥1 - Rx = (r - e) mod N; Ry是奇数(compressFlag = 3) 330 * 公钥2 - Rx = ((r - e) mod N) + N; Ry是偶数(compressFlag = 2) 331 * 公钥3 - Rx = ((r - e) mod N) + N; Ry是奇数(compressFlag = 3) 332 333 Rx, Ry代表随机点R的X,Y坐标值。绝大多数情况下,只会返回两个公钥,后两者只有当(r - e) mod N的值小于P-1-N时才可能。 334 335 ### 半同态加解密 336 EC-ElGamal with SM2的半同态加密(Partially Homomorphic Encryption, PHE), 支持uint32 或者 int32类型。[Partially Homomorphic Encryption, EC-ElGamal with SM2](https://github.com/emmansun/sm2elgamal). 337 338 ### 环签名 339 [Ring Signature Schemes Based on SM2 Digital Signature Algorithm](https://github.com/emmansun/sm2rsign).