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