Skip to main content

gmcrypto_core/
sec1.rs

1//! SEC1 `ECPrivateKey` codec (RFC 5915) for SM2 keys.
2//!
3//! Wire shape (RFC 5915 §3):
4//!
5//! ```text
6//! ECPrivateKey ::= SEQUENCE {
7//!     version        INTEGER { ecPrivkeyVer1(1) },
8//!     privateKey     OCTET STRING,
9//!     parameters [0] ECParameters {{ NamedCurve }} OPTIONAL,
10//!     publicKey  [1] BIT STRING OPTIONAL
11//! }
12//! ```
13//!
14//! `parameters` is a CHOICE; this module emits and accepts the
15//! `namedCurve` arm only, with the `sm2p256v1` OID
16//! (`1.2.156.10197.1.301`). `privateKey` carries the 32-byte
17//! big-endian scalar `d`. `publicKey`, when present, carries the
18//! 65-byte uncompressed point `04 || X || Y`.
19//!
20//! # SEC1 point encoding
21//!
22//! [`encode_uncompressed_point`] / [`decode_uncompressed_point`]
23//! handle the 65-byte `04 || X || Y` form (SEC1 §2.3.3). The
24//! decoder rejects the identity point `00`, the compressed
25//! forms `02`/`03 || X` (deferred to v0.4), and any off-curve
26//! `(X, Y)`.
27//!
28//! # Failure-mode invariant
29//!
30//! Decoders return `Option`; no distinguishing variants. The
31//! free-form encoder helpers panic only on programmer error
32//! (32-byte slices guaranteed by callers).
33
34use crate::asn1::oid::SM2P256V1;
35use crate::asn1::{reader, writer};
36use crate::sm2::curve::{Fn, Fp};
37use crate::sm2::encrypt::{point_on_curve, projective_from_affine};
38use crate::sm2::point::ProjectivePoint;
39use alloc::vec::Vec;
40use crypto_bigint::U256;
41use subtle::ConstantTimeLess;
42use zeroize::Zeroize;
43
44/// SEC1 uncompressed-point tag byte (`04 || X || Y`).
45pub(crate) const SEC1_TAG_UNCOMPRESSED: u8 = 0x04;
46/// SEC1 uncompressed-point total byte length (`1 + 32 + 32`).
47pub(crate) const SEC1_UNCOMPRESSED_LEN: usize = 65;
48/// RFC 5915 `ECPrivateKey` version number (always `ecPrivkeyVer1 = 1`).
49const ECPRIVKEY_VER1: u8 = 1;
50
51/// Encode `(x, y)` field elements as a 65-byte SEC1 uncompressed point.
52///
53/// Output is `04 || X(32 bytes BE) || Y(32 bytes BE)`. Caller
54/// pre-validates that the point is on-curve and not at infinity.
55#[must_use]
56pub(crate) fn encode_uncompressed_point(x: &Fp, y: &Fp) -> [u8; SEC1_UNCOMPRESSED_LEN] {
57    let mut out = [0u8; SEC1_UNCOMPRESSED_LEN];
58    out[0] = SEC1_TAG_UNCOMPRESSED;
59    out[1..33].copy_from_slice(&x.retrieve().to_be_bytes());
60    out[33..65].copy_from_slice(&y.retrieve().to_be_bytes());
61    out
62}
63
64/// Decode a 65-byte SEC1 uncompressed point into a validated
65/// [`ProjectivePoint`]. Returns `None` for any malformed input,
66/// including:
67///
68/// - wrong length (not 65 bytes);
69/// - leading byte not `0x04`;
70/// - identity point (a single `0x00`);
71/// - `X >= p` or `Y >= p` (field-element bounds);
72/// - `(X, Y)` not on the SM2 curve.
73///
74/// Compressed forms (`0x02` / `0x03`) are rejected — decompression
75/// requires a modular square root that v0.3 does not implement.
76#[must_use]
77pub(crate) fn decode_uncompressed_point(input: &[u8]) -> Option<ProjectivePoint> {
78    if input.len() != SEC1_UNCOMPRESSED_LEN {
79        return None;
80    }
81    if input[0] != SEC1_TAG_UNCOMPRESSED {
82        return None;
83    }
84    let x_be = &input[1..33];
85    let y_be = &input[33..65];
86    let x_u = U256::from_be_slice(x_be);
87    let y_u = U256::from_be_slice(y_be);
88    let p = *Fp::MODULUS.as_ref();
89    if !bool::from(x_u.ct_lt(&p)) || !bool::from(y_u.ct_lt(&p)) {
90        return None;
91    }
92    let x = Fp::new(&x_u);
93    let y = Fp::new(&y_u);
94    if !point_on_curve(&x, &y) {
95        return None;
96    }
97    Some(projective_from_affine(x, y))
98}
99
100/// Encode an SM2 `ECPrivateKey` (RFC 5915) into DER bytes.
101///
102/// `scalar_be` is the 32-byte big-endian private scalar. The
103/// `parameters` field is emitted with `namedCurve = sm2p256v1`;
104/// the optional `publicKey` BIT STRING is emitted as the
105/// 65-byte uncompressed point when `Some`.
106///
107/// The intermediate body buffer is zeroized before return — the
108/// caller-supplied `scalar_be` slice is **not** zeroized (caller
109/// owns it).
110#[must_use]
111pub fn encode(
112    scalar_be: &[u8; 32],
113    public_uncompressed: Option<&[u8; SEC1_UNCOMPRESSED_LEN]>,
114) -> Vec<u8> {
115    let mut body = Vec::with_capacity(120);
116    // version INTEGER 1
117    writer::write_integer(&mut body, &[ECPRIVKEY_VER1]);
118    // privateKey OCTET STRING (raw 32-byte scalar; SEC1 OCTET STRING
119    // is unsigned, no DER INTEGER discipline).
120    writer::write_octet_string(&mut body, scalar_be);
121    // [0] EXPLICIT ECParameters: namedCurve OID = sm2p256v1.
122    let mut params_inner = Vec::with_capacity(SM2P256V1.len() + 2);
123    writer::write_oid(&mut params_inner, SM2P256V1);
124    writer::write_context_tagged_explicit(&mut body, 0, &params_inner);
125    // [1] EXPLICIT BIT STRING { uncompressed point } if present.
126    if let Some(pk) = public_uncompressed {
127        let mut pk_inner = Vec::with_capacity(SEC1_UNCOMPRESSED_LEN + 4);
128        writer::write_bit_string(&mut pk_inner, 0, pk);
129        writer::write_context_tagged_explicit(&mut body, 1, &pk_inner);
130    }
131    let mut out = Vec::with_capacity(body.len() + 4);
132    writer::write_sequence(&mut out, &body);
133    body.zeroize();
134    out
135}
136
137/// Decoded SEC1 `ECPrivateKey` contents.
138///
139/// Returned by [`decode`]; consumers translate the validated scalar
140/// into an [`crate::sm2::Sm2PrivateKey`] via the constructor on that
141/// type. Holds public bytes only — no Mont-form scalars.
142#[derive(Debug, Clone)]
143pub struct EcPrivateKey {
144    /// 32-byte big-endian scalar `d`. Not validated against the
145    /// curve order here — the caller must reject `d == 0` and
146    /// `d == n-1` via [`crate::sm2::Sm2PrivateKey::from_bytes_be`].
147    pub scalar_be: [u8; 32],
148    /// Decoded uncompressed public point, if the optional
149    /// `publicKey` field was present and well-formed.
150    pub public: Option<ProjectivePoint>,
151}
152
153impl Drop for EcPrivateKey {
154    fn drop(&mut self) {
155        self.scalar_be.zeroize();
156    }
157}
158
159/// Decode a DER `ECPrivateKey` blob.
160///
161/// Validates:
162///
163/// - outer SEQUENCE with no trailing bytes;
164/// - `version == 1`;
165/// - `privateKey` is a 32-byte OCTET STRING (SM2's curve has 256-bit
166///   scalars; oversize/undersize is malformed);
167/// - if present, `[0] parameters` is `namedCurve = sm2p256v1` only;
168/// - if present, `[1] publicKey` is a 65-byte uncompressed point
169///   that decodes successfully (on-curve, not identity).
170///
171/// Returns `None` for any malformed input.
172#[must_use]
173pub fn decode(input: &[u8]) -> Option<EcPrivateKey> {
174    let (body, rest) = reader::read_sequence(input)?;
175    if !rest.is_empty() {
176        return None;
177    }
178
179    // version INTEGER 1.
180    let (version, body) = reader::read_integer(body)?;
181    if version != [ECPRIVKEY_VER1] {
182        return None;
183    }
184    // privateKey OCTET STRING — exactly 32 bytes for SM2.
185    let (scalar_bytes, mut body) = reader::read_octet_string(body)?;
186    if scalar_bytes.len() != 32 {
187        return None;
188    }
189    let mut scalar_be = [0u8; 32];
190    scalar_be.copy_from_slice(scalar_bytes);
191
192    let mut public: Option<ProjectivePoint> = None;
193
194    // [0] EXPLICIT parameters (OPTIONAL).
195    if let Some((params_inner, after)) = reader::read_context_tagged_explicit(body, 0) {
196        let (oid, params_rest) = reader::read_oid(params_inner)?;
197        if !params_rest.is_empty() || oid != SM2P256V1 {
198            scalar_be.zeroize();
199            return None;
200        }
201        body = after;
202    }
203
204    // [1] EXPLICIT publicKey BIT STRING (OPTIONAL).
205    if let Some((pk_inner, after)) = reader::read_context_tagged_explicit(body, 1) {
206        let (unused, pk_bytes, pk_rest) = reader::read_bit_string(pk_inner)?;
207        if unused != 0 || !pk_rest.is_empty() {
208            scalar_be.zeroize();
209            return None;
210        }
211        if let Some(p) = decode_uncompressed_point(pk_bytes) {
212            public = Some(p);
213        } else {
214            scalar_be.zeroize();
215            return None;
216        }
217        body = after;
218    }
219
220    if !body.is_empty() {
221        scalar_be.zeroize();
222        return None;
223    }
224
225    Some(EcPrivateKey { scalar_be, public })
226}
227
228/// Bound the scalar `d` to `[1, n-1]` in the SEC1 sense — i.e. it
229/// must be a representative of a non-zero scalar field element.
230/// Returns the validated scalar in Mont form. **Caller is
231/// responsible for the tighter `[1, n-2]` range required by
232/// SM2 sign/encrypt — that lives in `Sm2PrivateKey::from_bytes_be`.**
233#[must_use]
234#[allow(dead_code)]
235pub(crate) fn validate_scalar(scalar_be: &[u8; 32]) -> Option<Fn> {
236    let d = U256::from_be_slice(scalar_be);
237    let n = *Fn::MODULUS.as_ref();
238    if d == U256::ZERO {
239        return None;
240    }
241    if !bool::from(d.ct_lt(&n)) {
242        return None;
243    }
244    Some(Fn::new(&d))
245}
246
247#[cfg(test)]
248mod tests {
249    use super::*;
250    use crate::sm2::point::ProjectivePoint;
251
252    /// SEC1 uncompressed encoding of the SM2 generator round-trips.
253    #[test]
254    fn uncompressed_point_round_trip_generator() {
255        let g = ProjectivePoint::generator();
256        let (x, y) = g.to_affine().expect("G finite");
257        let bytes = encode_uncompressed_point(&x, &y);
258        assert_eq!(bytes[0], 0x04);
259        let recovered = decode_uncompressed_point(&bytes).expect("decode");
260        let (rx, ry) = recovered.to_affine().expect("recovered finite");
261        assert_eq!(rx.retrieve(), x.retrieve());
262        assert_eq!(ry.retrieve(), y.retrieve());
263    }
264
265    #[test]
266    fn uncompressed_point_rejects_wrong_length() {
267        assert!(decode_uncompressed_point(&[0x04]).is_none());
268        assert!(decode_uncompressed_point(&[0x04; 64]).is_none());
269        assert!(decode_uncompressed_point(&[0x04; 66]).is_none());
270    }
271
272    #[test]
273    fn uncompressed_point_rejects_compressed_tag() {
274        let mut bytes = [0u8; 65];
275        bytes[0] = 0x02;
276        assert!(decode_uncompressed_point(&bytes).is_none());
277        bytes[0] = 0x03;
278        assert!(decode_uncompressed_point(&bytes).is_none());
279    }
280
281    #[test]
282    fn uncompressed_point_rejects_off_curve() {
283        let mut bytes = [0u8; 65];
284        bytes[0] = 0x04;
285        bytes[1] = 1;
286        bytes[33] = 1;
287        // (1, 1) is not on the SM2 curve.
288        assert!(decode_uncompressed_point(&bytes).is_none());
289    }
290
291    #[test]
292    fn uncompressed_point_rejects_x_at_or_above_p() {
293        let g = ProjectivePoint::generator();
294        let (_x, y) = g.to_affine().expect("G finite");
295        let p = *Fp::MODULUS.as_ref();
296        let mut bytes = [0u8; SEC1_UNCOMPRESSED_LEN];
297        bytes[0] = 0x04;
298        // X = p (the modulus, not a valid field element).
299        bytes[1..33].copy_from_slice(&p.to_be_bytes());
300        bytes[33..65].copy_from_slice(&y.retrieve().to_be_bytes());
301        assert!(
302            decode_uncompressed_point(&bytes).is_none(),
303            "X = p must be rejected"
304        );
305    }
306
307    /// `ECPrivateKey` round-trip with public-key field present.
308    #[test]
309    fn ecprivatekey_round_trip_with_public() {
310        let scalar_be: [u8; 32] = [
311            0x39, 0x45, 0x20, 0x8F, 0x7B, 0x21, 0x44, 0xB1, 0x3F, 0x36, 0xE3, 0x8A, 0xC6, 0xD3,
312            0x9F, 0x95, 0x88, 0x93, 0x93, 0x69, 0x28, 0x60, 0xB5, 0x1A, 0x42, 0xFB, 0x81, 0xEF,
313            0x4D, 0xF7, 0xC5, 0xB8,
314        ];
315        // Use the GB/T 32918.2 sample — its public point is on-curve.
316        let d = U256::from_be_slice(&scalar_be);
317        let key = crate::sm2::Sm2PrivateKey::from_scalar_inner(d).expect("valid d");
318        let (x, y) = key.public_key().to_affine().expect("finite");
319        let pk = encode_uncompressed_point(&x, &y);
320        let der = encode(&scalar_be, Some(&pk));
321
322        let recovered = decode(&der).expect("decode");
323        assert_eq!(recovered.scalar_be, scalar_be);
324        assert!(recovered.public.is_some());
325        let (rx, ry) = recovered.public.unwrap().to_affine().expect("finite");
326        assert_eq!(rx.retrieve(), x.retrieve());
327        assert_eq!(ry.retrieve(), y.retrieve());
328    }
329
330    /// `ECPrivateKey` round-trip without optional fields.
331    #[test]
332    fn ecprivatekey_round_trip_minimal() {
333        let scalar_be: [u8; 32] = [0x42; 32];
334        let der = encode(&scalar_be, None);
335        let recovered = decode(&der).expect("decode");
336        assert_eq!(recovered.scalar_be, scalar_be);
337        assert!(recovered.public.is_none());
338    }
339
340    /// Round-trip with parameters present (sm2p256v1) and public absent.
341    #[test]
342    fn ecprivatekey_round_trip_params_only() {
343        let scalar_be: [u8; 32] = [0x11; 32];
344        let der = encode(&scalar_be, None);
345        // The encoder always writes parameters. Decode and re-encode.
346        let recovered = decode(&der).expect("decode");
347        let der2 = encode(
348            &recovered.scalar_be,
349            recovered
350                .public
351                .as_ref()
352                .map(|p| {
353                    let (x, y) = p.to_affine().expect("finite");
354                    encode_uncompressed_point(&x, &y)
355                })
356                .as_ref(),
357        );
358        assert_eq!(der, der2);
359    }
360
361    #[test]
362    fn ecprivatekey_rejects_wrong_version() {
363        let bad = [
364            0x30, 0x05, // SEQ len=5
365            0x02, 0x01, 0x02, // INTEGER 2 (wrong version)
366            0x04, 0x00, // OCTET STRING empty
367        ];
368        assert!(decode(&bad).is_none());
369    }
370
371    #[test]
372    fn ecprivatekey_rejects_short_scalar() {
373        let bad = [
374            0x30, 0x06, // SEQ
375            0x02, 0x01, 0x01, // INTEGER 1
376            0x04, 0x01, 0xAB, // OCTET STRING 1 byte (wrong length)
377        ];
378        assert!(decode(&bad).is_none());
379    }
380
381    #[test]
382    fn ecprivatekey_rejects_wrong_curve_oid() {
383        // Build a SEC1 ECPrivateKey with a fake namedCurve (P-256 OID).
384        let mut body = Vec::new();
385        writer::write_integer(&mut body, &[1]);
386        let scalar = [0u8; 32];
387        writer::write_octet_string(&mut body, &scalar);
388        // P-256 OID 1.2.840.10045.3.1.7 → DER content bytes
389        // 2A 86 48 CE 3D 03 01 07
390        let p256_oid = &[0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07];
391        let mut params = Vec::new();
392        writer::write_oid(&mut params, p256_oid);
393        writer::write_context_tagged_explicit(&mut body, 0, &params);
394        let mut der = Vec::new();
395        writer::write_sequence(&mut der, &body);
396        assert!(
397            decode(&der).is_none(),
398            "non-SM2 namedCurve must be rejected"
399        );
400    }
401
402    #[test]
403    fn ecprivatekey_rejects_trailing_bytes() {
404        let scalar_be: [u8; 32] = [0x42; 32];
405        let mut der = encode(&scalar_be, None);
406        der.push(0x00);
407        assert!(decode(&der).is_none(), "trailing byte must be rejected");
408    }
409
410    #[test]
411    fn validate_scalar_rejects_zero() {
412        let zero = [0u8; 32];
413        assert!(validate_scalar(&zero).is_none());
414    }
415
416    #[test]
417    fn validate_scalar_rejects_n() {
418        let n = *Fn::MODULUS.as_ref();
419        let n_bytes = n.to_be_bytes();
420        let mut buf = [0u8; 32];
421        buf.copy_from_slice(&n_bytes);
422        assert!(validate_scalar(&buf).is_none());
423    }
424}