Skip to main content

gmcrypto_core/asn1/
ciphertext.rs

1//! GM/T 0009-2012 §6 SM2 ciphertext DER encoding.
2//!
3//! Structure:
4//!
5//! ```text
6//! SM2Cipher ::= SEQUENCE {
7//!     XCoordinate INTEGER,         -- C1.x  (positive, ≤ 256 bits)
8//!     YCoordinate INTEGER,         -- C1.y  (positive, ≤ 256 bits)
9//!     HASH        OCTET STRING,    -- C3, exactly 32 bytes (SM3 digest)
10//!     CipherText  OCTET STRING     -- C2, variable length
11//! }
12//! ```
13//!
14//! v0.3 re-implements on top of [`super::reader`] / [`super::writer`];
15//! the wire output and accept/reject behaviour are byte-identical to
16//! v0.2. Raw byte-concat formats (`C1 || C3 || C2`,
17//! `C1 || C2 || C3`) live in [`crate::sm2::raw_ciphertext`] (W4); this
18//! module remains DER-only.
19//!
20//! INTEGER decoding follows strict X.690 canonical-encoding rules
21//! enforced in [`super::reader::read_integer`], plus two
22//! ciphertext-specific deltas applied here:
23//! - the canonical single-byte zero `02 01 00` is **accepted** as the
24//!   field element `0` (a `(0, y)` point on the curve is a valid C1);
25//! - 32-byte coordinates `≥ p` are **rejected** so that `Fp::new`
26//!   cannot silently reduce a non-canonical encoding modulo `p`,
27//!   which would create ciphertext malleability.
28//!
29//! Accepting non-canonical INTEGER encodings would create ciphertext
30//! malleability — multiple distinct DER blobs mapping to the same
31//! `(C1, C3, C2)` tuple. The strict-canonical reader rules + the
32//! `< p` field bound here are the malleability defense.
33
34use alloc::vec::Vec;
35use crypto_bigint::U256;
36use subtle::ConstantTimeLess;
37
38use crate::sm2::curve::Fp;
39
40use super::{reader, writer};
41
42/// SM3 digest size — fixed at 32 bytes; the spec mandates it.
43const HASH_LEN: usize = 32;
44
45/// Parsed SM2 ciphertext components.
46///
47/// `x` and `y` are the affine coordinates of `C1 = kG`; `hash` is `C3`,
48/// the SM3 digest computed during encryption; `ciphertext` is `C2`, the
49/// KDF-XOR'd plaintext.
50#[derive(Clone, Debug)]
51pub struct Sm2Ciphertext {
52    /// `C1.x` — 32-byte big-endian field element.
53    ///
54    /// v0.22 reshaped this from `crypto_bigint::U256` to `[u8; 32]` so the
55    /// public API names no `crypto-bigint` type (`docs/v0.22-scope.md` §3
56    /// Q22.4). The bytes are the canonical big-endian encoding of the
57    /// coordinate; [`decode`] guarantees `x < p`, but a value built directly
58    /// is **not** validated until [`crate::sm2::decrypt()`]'s on-curve check.
59    pub x: [u8; 32],
60    /// `C1.y` — 32-byte big-endian field element (see [`Sm2Ciphertext::x`]).
61    pub y: [u8; 32],
62    /// `C3 = SM3(x2 || M || y2)`. Always 32 bytes.
63    pub hash: [u8; HASH_LEN],
64    /// `C2 = M XOR KDF(x2 || y2, |M|)`. Length matches plaintext length.
65    pub ciphertext: Vec<u8>,
66}
67
68/// Encode an [`Sm2Ciphertext`] as a GM/T 0009 SEQUENCE.
69#[must_use]
70pub fn encode(ct: &Sm2Ciphertext) -> Vec<u8> {
71    let mut body = Vec::with_capacity(ct.ciphertext.len() + 80);
72    writer::write_integer(&mut body, &ct.x);
73    writer::write_integer(&mut body, &ct.y);
74    writer::write_octet_string(&mut body, &ct.hash);
75    writer::write_octet_string(&mut body, &ct.ciphertext);
76    let mut out = Vec::with_capacity(body.len() + 4);
77    writer::write_sequence(&mut out, &body);
78    out
79}
80
81/// Decode a GM/T 0009 SEQUENCE into [`Sm2Ciphertext`]. Returns `None`
82/// for any malformed input. **No distinguishing failure modes** —
83/// malleability defense per the project's failure-mode invariant.
84#[must_use]
85pub fn decode(input: &[u8]) -> Option<Sm2Ciphertext> {
86    let (body, rest) = reader::read_sequence(input)?;
87    if !rest.is_empty() {
88        return None;
89    }
90    let (x, body) = read_field_element(body)?;
91    let (y, body) = read_field_element(body)?;
92    let (hash_bytes, body) = reader::read_octet_string(body)?;
93    let (ciphertext, body) = reader::read_octet_string(body)?;
94    if !body.is_empty() {
95        return None;
96    }
97    if hash_bytes.len() != HASH_LEN {
98        return None;
99    }
100    let mut hash = [0u8; HASH_LEN];
101    hash.copy_from_slice(hash_bytes);
102    Some(Sm2Ciphertext {
103        x,
104        y,
105        hash,
106        ciphertext: ciphertext.to_vec(),
107    })
108}
109
110/// Read a DER INTEGER and decode its content as a 32-byte unsigned
111/// big-endian field element of `Fp`. Accepts zero (the canonical
112/// `02 01 00`); rejects coordinates `≥ p` so that `Fp::new` cannot
113/// silently reduce modulo `p` (which would create malleability).
114fn read_field_element(input: &[u8]) -> Option<([u8; 32], &[u8])> {
115    let (bytes, rest) = reader::read_integer(input)?;
116    if bytes.len() > 32 {
117        return None;
118    }
119    let mut padded = [0u8; 32];
120    padded[32 - bytes.len()..].copy_from_slice(bytes);
121    // Reject coordinates ≥ p. C1 coordinates are public; using
122    // `ConstantTimeLess` matches the rest of the crate's idiom even
123    // though no secret material flows here. v0.22 returns the canonical
124    // `[u8; 32]` (was `U256`) but keeps this `< p` malleability bound at
125    // the decode boundary unchanged.
126    let value = U256::from_be_slice(&padded);
127    let in_field: bool = value.ct_lt(Fp::MODULUS.as_ref()).into();
128    if !in_field {
129        return None;
130    }
131    Some((padded, rest))
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    fn make_ct(ciphertext: Vec<u8>) -> Sm2Ciphertext {
139        Sm2Ciphertext {
140            x: crate::u256_to_be32(&U256::from_be_hex(
141                "1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF1234567890ABCDEF",
142            )),
143            y: crate::u256_to_be32(&U256::from_be_hex(
144                "FEDCBA0987654321FEDCBA0987654321FEDCBA0987654321FEDCBA0987654321",
145            )),
146            hash: [0xa5u8; 32],
147            ciphertext,
148        }
149    }
150
151    /// Helper for hand-built malformed-blob tests: prepend `30 LEN`
152    /// to a body. Mirrors the `push_length` boundary the writer
153    /// enforces.
154    fn wrap_sequence(body: &[u8]) -> Vec<u8> {
155        let mut out = Vec::new();
156        writer::write_sequence(&mut out, body);
157        out
158    }
159
160    /// Standard round-trip: encode → decode → equal.
161    #[test]
162    fn round_trip_short() {
163        let ct = make_ct(b"hello world".to_vec());
164        let der = encode(&ct);
165        let decoded = decode(&der).expect("decode round-trip");
166        assert_eq!(decoded.x, ct.x);
167        assert_eq!(decoded.y, ct.y);
168        assert_eq!(decoded.hash, ct.hash);
169        assert_eq!(decoded.ciphertext, ct.ciphertext);
170    }
171
172    /// Round-trip with a high-bit-set top byte on `x` — exercises the
173    /// writer's INTEGER 0x00-pad path.
174    #[test]
175    fn round_trip_x_high_bit_set() {
176        let mut ct = make_ct(b"x".to_vec());
177        ct.x = crate::u256_to_be32(&U256::from_be_hex(
178            "FFEDCBA9876543210FEDCBA9876543210FEDCBA9876543210FEDCBA987654321",
179        ));
180        let der = encode(&ct);
181        let decoded = decode(&der).expect("decode high-bit round-trip");
182        assert_eq!(decoded.x, ct.x);
183    }
184
185    /// Round-trip with a ciphertext spanning the 256-byte length boundary
186    /// (exercises the 0x82 length encoding in the writer).
187    #[test]
188    fn round_trip_medium_ciphertext_300_bytes() {
189        let mut payload = alloc::vec![0u8; 300];
190        for (i, b) in payload.iter_mut().enumerate() {
191            #[allow(clippy::cast_possible_truncation)]
192            {
193                *b = (i as u8).wrapping_mul(13);
194            }
195        }
196        let ct = make_ct(payload.clone());
197        let der = encode(&ct);
198        let decoded = decode(&der).expect("decode 300-byte round-trip");
199        assert_eq!(decoded.ciphertext, payload);
200    }
201
202    /// Round-trip with empty ciphertext — RFC 5652 §6 doesn't forbid
203    /// zero-length OCTET STRING content; our DER must accept it.
204    #[test]
205    fn round_trip_empty_ciphertext() {
206        let ct = make_ct(Vec::new());
207        let der = encode(&ct);
208        let decoded = decode(&der).expect("decode empty-ciphertext round-trip");
209        assert!(decoded.ciphertext.is_empty());
210    }
211
212    /// Decode rejects garbage / truncated / empty input.
213    #[test]
214    fn rejects_malformed() {
215        assert!(decode(&[]).is_none(), "empty input");
216        assert!(decode(&[0x30]).is_none(), "truncated SEQUENCE header");
217        assert!(decode(&[0x31, 0x00]).is_none(), "wrong outer tag");
218        // SEQUENCE with declared body shorter than declared length
219        assert!(decode(&[0x30, 0x05, 0x02, 0x01, 0x01]).is_none());
220    }
221
222    /// Decode rejects a hash field whose length is anything other than
223    /// 32 bytes. SM3 always produces 32 bytes; smaller or larger is
224    /// malformed.
225    #[test]
226    fn rejects_wrong_hash_length() {
227        // Build a SEQUENCE where HASH OCTET STRING has 31 bytes instead of 32.
228        let bad_hash = [0x55u8; 31];
229        let ciphertext = b"x";
230        let mut body = Vec::new();
231        writer::write_integer(&mut body, &[0x01]);
232        writer::write_integer(&mut body, &[0x02]);
233        writer::write_octet_string(&mut body, &bad_hash);
234        writer::write_octet_string(&mut body, ciphertext);
235        let der = wrap_sequence(&body);
236        assert!(
237            decode(&der).is_none(),
238            "31-byte HASH must be rejected; SM3 always produces 32 bytes"
239        );
240    }
241
242    /// Strict canonical INTEGER: redundant `00`-pad on `x` rejected
243    /// (the same rule `read_integer` enforces). Prevents
244    /// ciphertext malleability across multiple DER encodings of the
245    /// same `(x, y, hash, ct)` tuple.
246    #[test]
247    fn rejects_non_canonical_x_leading_zero() {
248        // Build SEQUENCE with x = INTEGER 0x00 0x01 (BER-style, non-canonical).
249        let mut body = Vec::new();
250        body.extend_from_slice(&[0x02, 0x02, 0x00, 0x01]); // x: bad
251        writer::write_integer(&mut body, &[0x02]); // y: ok
252        writer::write_octet_string(&mut body, &[0u8; 32]);
253        writer::write_octet_string(&mut body, b"");
254        let der = wrap_sequence(&body);
255        assert!(
256            decode(&der).is_none(),
257            "non-canonical 00-pad on x must be rejected"
258        );
259    }
260
261    /// Strict canonical INTEGER: high-bit-set first byte (would be
262    /// negative in two's complement) rejected on `y`.
263    #[test]
264    fn rejects_negative_y_encoding() {
265        let mut body = Vec::new();
266        writer::write_integer(&mut body, &[0x01]);
267        body.extend_from_slice(&[0x02, 0x01, 0x80]); // y = INTEGER 0x80 (sign-bit set, no pad)
268        writer::write_octet_string(&mut body, &[0u8; 32]);
269        writer::write_octet_string(&mut body, b"");
270        let der = wrap_sequence(&body);
271        assert!(decode(&der).is_none());
272    }
273
274    /// Trailing garbage after the ciphertext OCTET STRING is rejected
275    /// — strict DER parsing.
276    #[test]
277    fn rejects_trailing_bytes() {
278        let ct = make_ct(b"hi".to_vec());
279        let mut der = encode(&ct);
280        der.push(0xff); // trailing garbage
281        assert!(decode(&der).is_none());
282    }
283
284    /// Canonical DER encoding of zero (`02 01 00`) on `x` round-trips.
285    /// `(0, y)` is a valid affine C1 if it lies on the curve; the wire
286    /// format must accept the field element 0.
287    #[test]
288    fn round_trip_x_zero() {
289        let mut ct = make_ct(b"z".to_vec());
290        ct.x = [0u8; 32];
291        let der = encode(&ct);
292        let decoded = decode(&der).expect("decode round-trip with x = 0");
293        assert_eq!(decoded.x, [0u8; 32]);
294        assert_eq!(decoded.y, ct.y);
295    }
296
297    /// Strict canonical INTEGER: a 32-byte coordinate ≥ p is rejected.
298    /// Without this bound, `Fp::new` silently reduces the value modulo
299    /// `p`, admitting a second DER blob for the same field element —
300    /// ciphertext malleability. Regression test for the v0.2 codex
301    /// pre-publish review finding.
302    #[test]
303    fn rejects_x_at_or_above_p() {
304        // Build a SEQUENCE with x = p (the SM2 prime).
305        let p = *Fp::MODULUS.as_ref();
306        let p_bytes = p.to_be_bytes();
307        let mut body = Vec::new();
308        writer::write_integer(&mut body, &p_bytes);
309        writer::write_integer(&mut body, &[0x01]);
310        writer::write_octet_string(&mut body, &[0u8; 32]);
311        writer::write_octet_string(&mut body, b"");
312        let der = wrap_sequence(&body);
313        assert!(
314            decode(&der).is_none(),
315            "x = p is not a field element and must be rejected"
316        );
317
318        // Also verify `2^256 - 1` is rejected (well above p).
319        let max_bytes = [0xffu8; 32];
320        let mut body = Vec::new();
321        writer::write_integer(&mut body, &max_bytes);
322        writer::write_integer(&mut body, &[0x01]);
323        writer::write_octet_string(&mut body, &[0u8; 32]);
324        writer::write_octet_string(&mut body, b"");
325        let der = wrap_sequence(&body);
326        assert!(decode(&der).is_none(), "x = 2^256 - 1 must be rejected");
327    }
328
329    /// Companion check: `p - 1` is the largest valid coordinate and
330    /// must round-trip cleanly.
331    #[test]
332    fn round_trip_x_p_minus_one() {
333        let p_minus_one = Fp::MODULUS.as_ref().wrapping_sub(&U256::ONE);
334        let mut ct = make_ct(b"q".to_vec());
335        ct.x = crate::u256_to_be32(&p_minus_one);
336        let der = encode(&ct);
337        let decoded = decode(&der).expect("decode round-trip with x = p - 1");
338        assert_eq!(decoded.x, crate::u256_to_be32(&p_minus_one));
339    }
340
341    /// The 0x83 length encoding boundary: a ciphertext payload exactly
342    /// 65,536 bytes long forces 3-byte length.
343    #[test]
344    fn round_trip_65536_byte_ciphertext_uses_3byte_length() {
345        let payload = alloc::vec![0xa5u8; 65_536];
346        let ct = make_ct(payload.clone());
347        let der = encode(&ct);
348        let decoded = decode(&der).expect("decode 65,536-byte round-trip");
349        assert_eq!(decoded.ciphertext, payload);
350    }
351}