Skip to main content

gmcrypto_core/sm2/
public_key.rs

1//! SM2 public keys.
2
3use crate::sec1::{SEC1_UNCOMPRESSED_LEN, decode_uncompressed_point, encode_uncompressed_point};
4use crate::sm2::point::ProjectivePoint;
5use subtle::ConstantTimeEq;
6
7/// SM2 public key: a curve point `P = d·G`.
8#[derive(Clone, Copy, Debug)]
9pub struct Sm2PublicKey {
10    point: ProjectivePoint,
11}
12
13impl Sm2PublicKey {
14    /// Wrap a curve point as a public key. Caller is responsible for
15    /// having checked the point is on-curve and not at infinity. API entry
16    /// points that need stronger failure guarantees perform their own
17    /// boundary checks.
18    #[must_use]
19    pub const fn from_point(point: ProjectivePoint) -> Self {
20        Self { point }
21    }
22
23    /// Underlying point.
24    #[must_use]
25    pub const fn point(&self) -> ProjectivePoint {
26        self.point
27    }
28
29    /// Decode a SEC1 uncompressed public key (`04 || X || Y`, 65 bytes)
30    /// into an `Sm2PublicKey`. Rejects the identity point and any
31    /// off-curve `(X, Y)`. Returns `None` for any malformed input.
32    #[must_use]
33    pub fn from_sec1_bytes(bytes: &[u8]) -> Option<Self> {
34        let point = decode_uncompressed_point(bytes)?;
35        // decode_uncompressed_point already enforces on-curve and
36        // length=65. The identity point cannot be encoded as `04 || X
37        // || Y` (the identity has no affine form), so the on-curve
38        // check on (X, Y) implicitly excludes it. Be explicit anyway.
39        if bool::from(point.is_identity()) {
40            return None;
41        }
42        Some(Self { point })
43    }
44
45    /// Encode this public key as 65 bytes of SEC1 uncompressed
46    /// `04 || X || Y`. Panics only if the underlying point is at
47    /// infinity, which `from_sec1_bytes` and the [`crate::sm2::Sm2PrivateKey`]
48    /// constructor both rule out at the boundary.
49    ///
50    /// # Panics
51    ///
52    /// Panics if the underlying point is at infinity.
53    #[must_use]
54    #[allow(clippy::missing_panics_doc)]
55    pub fn to_sec1_uncompressed(&self) -> [u8; SEC1_UNCOMPRESSED_LEN] {
56        let (x, y) = self
57            .point
58            .to_affine()
59            .expect("Sm2PublicKey at infinity violates the invariant");
60        encode_uncompressed_point(&x, &y)
61    }
62}
63
64impl From<ProjectivePoint> for Sm2PublicKey {
65    fn from(p: ProjectivePoint) -> Self {
66        Self::from_point(p)
67    }
68}
69
70impl ConstantTimeEq for Sm2PublicKey {
71    fn ct_eq(&self, other: &Self) -> subtle::Choice {
72        self.point.ct_eq(&other.point)
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use crate::sm2::Sm2PrivateKey;
80    use crypto_bigint::U256;
81
82    /// Round-trip the SM2 generator's public-point uncompressed encoding.
83    #[test]
84    fn sec1_round_trip_generator() {
85        let g = Sm2PublicKey::from_point(ProjectivePoint::generator());
86        let bytes = g.to_sec1_uncompressed();
87        let recovered = Sm2PublicKey::from_sec1_bytes(&bytes).expect("decode");
88        assert_eq!(bytes, recovered.to_sec1_uncompressed());
89    }
90
91    /// Round-trip the GB/T 32918.2 sample public key derived from D.
92    #[test]
93    fn sec1_round_trip_gbt_sample() {
94        let d =
95            U256::from_be_hex("3945208F7B2144B13F36E38AC6D39F95889393692860B51A42FB81EF4DF7C5B8");
96        let priv_key = Sm2PrivateKey::from_scalar_inner(d).expect("valid d");
97        let pub_key = Sm2PublicKey::from_point(priv_key.public_key());
98        let bytes = pub_key.to_sec1_uncompressed();
99        assert_eq!(bytes[0], 0x04);
100        let recovered = Sm2PublicKey::from_sec1_bytes(&bytes).expect("decode");
101        assert!(bool::from(pub_key.ct_eq(&recovered)));
102    }
103
104    /// `from_sec1_bytes` rejects wrong length / wrong tag / off-curve.
105    #[test]
106    fn sec1_rejects_malformed() {
107        assert!(Sm2PublicKey::from_sec1_bytes(&[0x04]).is_none());
108        let mut bad = [0u8; 65];
109        bad[0] = 0x04;
110        bad[1] = 1;
111        bad[33] = 1;
112        assert!(Sm2PublicKey::from_sec1_bytes(&bad).is_none());
113        // Compressed form rejected.
114        bad[0] = 0x02;
115        assert!(Sm2PublicKey::from_sec1_bytes(&bad).is_none());
116    }
117}