Skip to main content

gmcrypto_core/sm2/
decrypt.rs

1//! SM2 public-key decryption (GB/T 32918.4-2017 §7).
2//!
3//! # Algorithm
4//!
5//! ```text
6//! Input:  recipient private key d_B, GM/T 0009 DER ciphertext blob
7//! Output: plaintext M
8//!
9//! 1. Decode DER → (x1, y1, C3, C2)
10//! 2. Construct C1 = (x1, y1); reject if not on the SM2 curve
11//! 3. (x2, y2) = d_B * C1
12//! 4. t = KDF(x2 || y2, |C2|)
13//! 5. If t is all zeros, abort
14//! 6. M = C2 XOR t
15//! 7. u = SM3(x2 || M || y2)
16//! 8. If u != C3 (constant-time compare), abort
17//! 9. Output M
18//! ```
19//!
20//! # Failure-mode invariant
21//!
22//! Every failure mode collapses to a single
23//! [`crate::Error::Failed`] return — malformed DER, off-curve `C1`,
24//! identity `C1`, all-zero KDF, MAC mismatch. No distinguishing
25//! variants per the project's failure-mode invariant. SECURITY.md
26//! has the full rationale.
27//!
28//! # Constant-time stance
29//!
30//! Decrypt operates on the recipient's secret `d_B`. The
31//! constant-time-relevant work happens via:
32//!
33//! - `mul_var(d_B, C1)`: covered by v0.1's `ct_mul_var` harness target
34//!   plus the W0 direct-invert diagnostics.
35//! - `to_affine` after `mul_var`: covered by W0's `ct_fp_invert`.
36//! - KDF (counter-mode SM3): SM3 itself is data-independent in timing.
37//! - `M = C2 XOR t`: byte-wise XOR loop, branchless.
38//! - MAC compare: `subtle::ConstantTimeEq` on the 32-byte digest.
39//! - **All-zero KDF detection: non-branching.** A naïve early-return
40//!   on KDF-zero would gift a chosen-ciphertext attacker a timing
41//!   oracle for short C2: P(KDF zero) ≈ 2^(-8·|C2|), so a 1-byte C2
42//!   trips the branch ~1/256 of the time, and the early-return path
43//!   skips the XOR/SM3/MAC work — observably faster than a
44//!   normal MAC failure. The implementation folds the all-zero
45//!   detection into a `subtle::Choice` and combines it with the
46//!   `mac_ok` result via `&` so both classes of failure collapse to
47//!   identical control flow.
48//!
49//! v0.2 adds [`crate::sm4::Sm4Cipher`] for envelope encryption (use
50//! SM2 to wrap an SM4 key, then SM4-CBC with HMAC-SM3 for bulk data
51//! and integrity). v0.2's dudect harness adds `ct_sm2_decrypt` (W2
52//! chunk 3) — class-split by `d_B`, fixed ciphertext.
53//!
54//! # Invalid-curve attack
55//!
56//! Without the on-curve check on `C1`, an attacker could submit a
57//! point on a different curve sharing the same `x` coordinate as a
58//! point on SM2; multiplying by the secret `d_B` then leaks bits of
59//! `d_B` via the small-order subgroup of the rogue curve. The
60//! [`crate::sm2::encrypt::point_on_curve`] check is the standard
61//! defense.
62
63use crate::asn1::ciphertext::decode;
64use crate::sm2::curve::Fp;
65use crate::sm2::encrypt::{kdf, point_on_curve, projective_from_affine};
66use crate::sm2::private_key::Sm2PrivateKey;
67use crate::sm2::scalar_mul::mul_var;
68use crate::sm3::Sm3;
69use alloc::vec::Vec;
70use subtle::{Choice, ConstantTimeEq};
71use zeroize::Zeroize;
72
73/// Decrypt a GM/T 0009 DER-encoded ciphertext under recipient private
74/// key `private`.
75///
76/// Returns `Ok(plaintext)` on success, [`crate::Error::Failed`] on any
77/// failure.
78///
79/// # Errors
80///
81/// See module-doc — every failure mode collapses to one variant.
82pub fn decrypt(private: &Sm2PrivateKey, ciphertext_der: &[u8]) -> Result<Vec<u8>, crate::Error> {
83    // 1. DER decode. (Returns None on malformed input — single failure
84    //    bucket here; we collapse to Failed.)
85    let parsed = decode(ciphertext_der).ok_or(crate::Error::Failed)?;
86
87    // 2. Construct C1 from (x1, y1). Reject off-curve.
88    let x1 = Fp::new(&parsed.x);
89    let y1 = Fp::new(&parsed.y);
90    if !point_on_curve(&x1, &y1) {
91        return Err(crate::Error::Failed);
92    }
93    let c1 = projective_from_affine(x1, y1);
94    if bool::from(c1.is_identity()) {
95        return Err(crate::Error::Failed);
96    }
97
98    // 3. (x2, y2) = d_B * C1
99    let kp = mul_var(private.scalar(), &c1);
100    let (x2, y2) = kp.to_affine().ok_or(crate::Error::Failed)?;
101
102    // 4. KDF(x2 || y2, |C2|)
103    let mut z = [0u8; 64];
104    z[..32].copy_from_slice(&x2.retrieve().to_be_bytes());
105    z[32..].copy_from_slice(&y2.retrieve().to_be_bytes());
106
107    let mut t = alloc::vec![0u8; parsed.ciphertext.len()];
108    kdf(&z, &mut t);
109
110    // 5. KDF-zero detection — *non-branching*. We MUST NOT early-return
111    //    here: a chosen-ciphertext attacker who can submit a short C2
112    //    (e.g. 1 byte) hits an all-zero KDF output with probability
113    //    `≈ 2^(-8 * |C2|)`, and an early-return that skips the
114    //    XOR/SM3/MAC work would distinguish the secret-derived
115    //    predicate "d_B*C1 produced all-zero KDF" from an ordinary
116    //    MAC failure. Both outcomes must collapse to identical control
117    //    flow per the failure-mode invariant. The empty-C2 case is
118    //    explicitly excluded (vacuous all-zero on an empty buffer).
119    let nonempty: Choice = u8::from(!parsed.ciphertext.is_empty()).into();
120    let kdf_zero = nonempty & ct_all_zero(&t);
121
122    // 6. M = C2 XOR t (in place — t becomes M).
123    for (i, byte) in parsed.ciphertext.iter().enumerate() {
124        t[i] ^= byte;
125    }
126    // Rename for clarity: the buffer now holds (would-be) plaintext.
127    let mut plaintext = t;
128
129    // 7. u = SM3(x2 || M || y2) — computed unconditionally regardless
130    //    of `kdf_zero` so timing is identical on both branches.
131    let mut h = Sm3::new();
132    h.update(&z[..32]);
133    h.update(&plaintext);
134    h.update(&z[32..]);
135    let u = h.finalize();
136
137    // 8. Combine the constant-time KDF-zero detection with the MAC
138    //    compare into a single `Choice`. Using `&` on `Choice` (defined
139    //    via `BitAnd<Choice>`) preserves the constant-time contract.
140    let mac_ok = u.ct_eq(&parsed.hash);
141    let valid = mac_ok & !kdf_zero;
142
143    // Wipe the secret-derived (x2 || y2) buffer regardless of outcome.
144    z.zeroize();
145
146    if !bool::from(valid) {
147        // Wipe the would-be plaintext — the caller never sees it.
148        plaintext.zeroize();
149        return Err(crate::Error::Failed);
150    }
151
152    Ok(plaintext)
153}
154
155/// Constant-time all-zero scan returning a [`Choice`]. The bitwise OR
156/// fold gives a single 8-bit summary value that reveals only whether
157/// the buffer is all-zero — itself the mandated KDF-zero predicate
158/// — and never short-circuits.
159fn ct_all_zero(buf: &[u8]) -> Choice {
160    let mut acc: u8 = 0;
161    for b in buf {
162        acc |= b;
163    }
164    acc.ct_eq(&0u8)
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170    use crate::asn1::ciphertext::{Sm2Ciphertext, encode};
171    use crate::sm2::encrypt::encrypt;
172    use crate::sm2::private_key::Sm2PrivateKey;
173    use crate::sm2::public_key::Sm2PublicKey;
174    use crypto_bigint::U256;
175    use getrandom::SysRng;
176    use rand_core::UnwrapErr;
177
178    /// End-to-end round-trip with a random nonce: encrypt → decrypt
179    /// → recover plaintext.
180    #[test]
181    fn round_trip_random_nonce() {
182        let d =
183            U256::from_be_hex("1649AB77A00637BD5E2EFE283FBF353534AA7F7CB89463F208DDBC2920BB0DA0");
184        let key = Sm2PrivateKey::from_scalar_inner(d).expect("valid d");
185        let pk = Sm2PublicKey::from_point(key.public_key());
186        let plaintext = b"encryption standard";
187        let mut rng = UnwrapErr(SysRng);
188        let der = encrypt(&pk, plaintext, &mut rng).expect("encrypt");
189        let recovered = decrypt(&key, &der).expect("decrypt");
190        assert_eq!(recovered.as_slice(), plaintext);
191    }
192
193    /// Boundary-length round-trip across empty / 1 / 31 / 32 / 33 /
194    /// 64 / 65 byte plaintexts. Empty exercises the vacuous
195    /// KDF-zero check; 32 sits exactly on a KDF-block boundary; 33
196    /// crosses into the second KDF block.
197    #[test]
198    fn round_trip_boundary_lengths() {
199        let d =
200            U256::from_be_hex("1649AB77A00637BD5E2EFE283FBF353534AA7F7CB89463F208DDBC2920BB0DA0");
201        let key = Sm2PrivateKey::from_scalar_inner(d).expect("valid d");
202        let pk = Sm2PublicKey::from_point(key.public_key());
203        let mut rng = UnwrapErr(SysRng);
204
205        for len in [0usize, 1, 31, 32, 33, 64, 65, 128] {
206            let plaintext: Vec<u8> = (0..len)
207                .map(|i| {
208                    #[allow(clippy::cast_possible_truncation)]
209                    {
210                        (i as u8).wrapping_mul(7)
211                    }
212                })
213                .collect();
214            let der = encrypt(&pk, &plaintext, &mut rng).expect("encrypt");
215            let recovered = decrypt(&key, &der).expect("decrypt");
216            assert_eq!(recovered, plaintext, "round-trip mismatch at len={len}");
217        }
218    }
219
220    /// Decrypt rejects garbage / malformed DER.
221    #[test]
222    fn rejects_malformed_der() {
223        let d =
224            U256::from_be_hex("1649AB77A00637BD5E2EFE283FBF353534AA7F7CB89463F208DDBC2920BB0DA0");
225        let key = Sm2PrivateKey::from_scalar_inner(d).expect("valid d");
226        assert_eq!(decrypt(&key, &[]), Err(crate::Error::Failed));
227        assert_eq!(decrypt(&key, b"not DER"), Err(crate::Error::Failed));
228        assert_eq!(
229            decrypt(&key, &[0x30, 0x05, 0xff, 0xff, 0xff]),
230            Err(crate::Error::Failed)
231        );
232    }
233
234    /// Decrypt rejects ciphertext with `C1` not on the SM2 curve.
235    /// (Constructed by hand-building `Sm2Ciphertext` with arbitrary
236    /// off-curve `(x, y)`.)
237    #[test]
238    fn rejects_off_curve_c1() {
239        let d =
240            U256::from_be_hex("1649AB77A00637BD5E2EFE283FBF353534AA7F7CB89463F208DDBC2920BB0DA0");
241        let key = Sm2PrivateKey::from_scalar_inner(d).expect("valid d");
242        let off_curve = Sm2Ciphertext {
243            x: U256::from_u64(1),
244            y: U256::from_u64(1), // (1, 1) is not on SM2
245            hash: [0u8; 32],
246            ciphertext: alloc::vec![0u8; 16],
247        };
248        let der = encode(&off_curve);
249        assert_eq!(decrypt(&key, &der), Err(crate::Error::Failed));
250    }
251
252    /// Decrypt rejects ciphertext where `C3` (the MAC) doesn't match
253    /// the recomputed hash. Mutate one byte of `C3` after a valid
254    /// encrypt and verify decrypt fails.
255    #[test]
256    fn rejects_mac_mismatch() {
257        let d =
258            U256::from_be_hex("1649AB77A00637BD5E2EFE283FBF353534AA7F7CB89463F208DDBC2920BB0DA0");
259        let key = Sm2PrivateKey::from_scalar_inner(d).expect("valid d");
260        let pk = Sm2PublicKey::from_point(key.public_key());
261        let mut rng = UnwrapErr(SysRng);
262        let der = encrypt(&pk, b"encryption standard", &mut rng).expect("encrypt");
263
264        // Decode → mutate hash → re-encode → decrypt should fail.
265        let mut parsed = decode(&der).expect("decode our own DER");
266        parsed.hash[0] ^= 0x01;
267        let tampered = encode(&parsed);
268        assert_eq!(decrypt(&key, &tampered), Err(crate::Error::Failed));
269    }
270
271    /// Decrypt under the WRONG private key fails (MAC won't match).
272    #[test]
273    fn rejects_wrong_private_key() {
274        let d_a =
275            U256::from_be_hex("1649AB77A00637BD5E2EFE283FBF353534AA7F7CB89463F208DDBC2920BB0DA0");
276        let d_b =
277            U256::from_be_hex("3945208F7B2144B13F36E38AC6D39F95889393692860B51A42FB81EF4DF7C5B8");
278        let key_a = Sm2PrivateKey::from_scalar_inner(d_a).expect("valid d_a");
279        let key_b = Sm2PrivateKey::from_scalar_inner(d_b).expect("valid d_b");
280        let pk_a = Sm2PublicKey::from_point(key_a.public_key());
281        let mut rng = UnwrapErr(SysRng);
282        let der = encrypt(&pk_a, b"top secret", &mut rng).expect("encrypt to A");
283        // Decrypt with B's key — must fail.
284        assert_eq!(decrypt(&key_b, &der), Err(crate::Error::Failed));
285    }
286
287    /// Decrypt rejects ciphertext where `C2` has been mutated (one
288    /// byte XOR'd) — both the resulting plaintext bit AND the MAC
289    /// will be inconsistent.
290    #[test]
291    fn rejects_tampered_c2() {
292        let d =
293            U256::from_be_hex("1649AB77A00637BD5E2EFE283FBF353534AA7F7CB89463F208DDBC2920BB0DA0");
294        let key = Sm2PrivateKey::from_scalar_inner(d).expect("valid d");
295        let pk = Sm2PublicKey::from_point(key.public_key());
296        let mut rng = UnwrapErr(SysRng);
297        let der = encrypt(&pk, b"some plaintext data", &mut rng).expect("encrypt");
298
299        let mut parsed = decode(&der).expect("decode our own DER");
300        parsed.ciphertext[0] ^= 0xff;
301        let tampered = encode(&parsed);
302        assert_eq!(decrypt(&key, &tampered), Err(crate::Error::Failed));
303    }
304
305    /// Functional regression test for the constant-time KDF-zero
306    /// handling. Forge a ciphertext where `C2` is `[0x00; n]` for
307    /// small `n`; on decryption the random-looking `KDF(d_B*C1, n)`
308    /// won't be all-zero (the attacker can't choose `KDF` output
309    /// without knowing `d_B*C1`), so the path must collapse to
310    /// `Failed` via the MAC-mismatch arm rather than via an
311    /// early-return KDF-zero branch. The pre-fix decoder would have
312    /// taken the early return whenever the KDF *did* hit all-zero
313    /// (~1/256 of attempts for a 1-byte C2), exposing a chosen-
314    /// ciphertext timing oracle. We can't reliably hit the
315    /// all-zero KDF output here without grinding `C1`, but this
316    /// test does verify the rewrite still rejects forged short
317    /// ciphertexts cleanly across many attempts and that no panic
318    /// or `Ok` result slips through.
319    ///
320    /// Companion: see `crates/gmcrypto-core/src/sm2/decrypt.rs`
321    /// step 5 comment for the timing-oracle rationale.
322    #[test]
323    fn rejects_forged_short_ciphertext() {
324        let d =
325            U256::from_be_hex("1649AB77A00637BD5E2EFE283FBF353534AA7F7CB89463F208DDBC2920BB0DA0");
326        let key = Sm2PrivateKey::from_scalar_inner(d).expect("valid d");
327        let pk = Sm2PublicKey::from_point(key.public_key());
328        let mut rng = UnwrapErr(SysRng);
329
330        // Encrypt many distinct 1-byte messages so we exercise lots
331        // of `(C1, KDF)` pairs, then for each tamper `C3` to force
332        // the path through the new branchless KDF-zero detection +
333        // MAC compare. None should panic or return `Ok`.
334        for round in 0..32u8 {
335            let plaintext = [round];
336            let der = encrypt(&pk, &plaintext, &mut rng).expect("encrypt 1-byte");
337            let mut parsed = decode(&der).expect("decode our own DER");
338            parsed.hash[0] ^= 0x01;
339            let tampered = encode(&parsed);
340            assert_eq!(
341                decrypt(&key, &tampered),
342                Err(crate::Error::Failed),
343                "forged 1-byte ciphertext on round {round} must fail"
344            );
345        }
346    }
347
348    /// Empty plaintext round-trip is supported: `KDF(_, 0)` writes
349    /// zero bytes, the all-zero check is vacuously suppressed via
350    /// the `nonempty` Choice mask, and `SM3(x2 || empty || y2)`
351    /// is the MAC. Companion to `round_trip_boundary_lengths` —
352    /// kept independent so a regression on the empty-suppression
353    /// behavior surfaces here distinctly.
354    #[test]
355    fn round_trip_empty_plaintext() {
356        let d =
357            U256::from_be_hex("1649AB77A00637BD5E2EFE283FBF353534AA7F7CB89463F208DDBC2920BB0DA0");
358        let key = Sm2PrivateKey::from_scalar_inner(d).expect("valid d");
359        let pk = Sm2PublicKey::from_point(key.public_key());
360        let mut rng = UnwrapErr(SysRng);
361        let der = encrypt(&pk, b"", &mut rng).expect("encrypt empty");
362        let recovered = decrypt(&key, &der).expect("decrypt empty");
363        assert!(recovered.is_empty(), "empty plaintext round-trip");
364    }
365}