Skip to main content

gmcrypto_core/
spki.rs

1//! X.509 `SubjectPublicKeyInfo` codec (RFC 5280 §4.1.2.7) for SM2 keys.
2//!
3//! Wire shape:
4//!
5//! ```text
6//! SubjectPublicKeyInfo ::= SEQUENCE {
7//!     algorithm        AlgorithmIdentifier,
8//!     subjectPublicKey BIT STRING
9//! }
10//!
11//! AlgorithmIdentifier ::= SEQUENCE {
12//!     algorithm   OBJECT IDENTIFIER,
13//!     parameters  ANY DEFINED BY algorithm OPTIONAL
14//! }
15//! ```
16//!
17//! For SM2 the algorithm OID is `id-ecPublicKey`
18//! (`1.2.840.10045.2.1`) and `parameters` carries the
19//! `namedCurve` OID `sm2p256v1` (`1.2.156.10197.1.301`).
20//! `subjectPublicKey` is a BIT STRING wrapping the SEC1
21//! uncompressed point `04 || X || Y`.
22//!
23//! # Failure-mode invariant
24//!
25//! Decoders return `Option`; no distinguishing variants per
26//! `CLAUDE.md`.
27
28use crate::asn1::oid::{ID_EC_PUBLIC_KEY, SM2P256V1};
29use crate::asn1::{reader, writer};
30use crate::sec1::{SEC1_UNCOMPRESSED_LEN, decode_uncompressed_point, encode_uncompressed_point};
31use alloc::vec::Vec;
32
33/// Encode an SM2 public key as a DER `SubjectPublicKeyInfo` blob.
34///
35/// Caller pre-validates that the key's point is on-curve and not at
36/// infinity. (The standard accessor
37/// [`crate::sm2::Sm2PublicKey::to_sec1_uncompressed`] feeds this
38/// helper after extracting the affine `(x, y)`.)
39///
40/// # Panics
41///
42/// Panics if the underlying point is at infinity (callers must reject the
43/// identity point at the boundary).
44#[must_use]
45#[allow(clippy::missing_panics_doc)]
46pub fn encode(key: &crate::sm2::Sm2PublicKey) -> Vec<u8> {
47    let (x, y) = key.point().to_affine().expect("SPKI: point at infinity");
48    let pk = encode_uncompressed_point(&x, &y);
49    encode_uncompressed(&pk)
50}
51
52/// Encode the pre-formatted SEC1 uncompressed `04 || X || Y` bytes
53/// directly into a `SubjectPublicKeyInfo` blob. Avoids the affine
54/// extraction when the caller already has the bytes.
55#[must_use]
56pub fn encode_uncompressed(uncompressed: &[u8; SEC1_UNCOMPRESSED_LEN]) -> Vec<u8> {
57    // AlgorithmIdentifier { algorithm = id-ecPublicKey, parameters = sm2p256v1 OID }
58    let mut alg_inner = Vec::with_capacity(ID_EC_PUBLIC_KEY.len() + SM2P256V1.len() + 4);
59    writer::write_oid(&mut alg_inner, ID_EC_PUBLIC_KEY);
60    writer::write_oid(&mut alg_inner, SM2P256V1);
61
62    let mut alg_seq = Vec::with_capacity(alg_inner.len() + 4);
63    writer::write_sequence(&mut alg_seq, &alg_inner);
64
65    // subjectPublicKey BIT STRING { uncompressed }
66    let mut bitstr = Vec::with_capacity(uncompressed.len() + 4);
67    writer::write_bit_string(&mut bitstr, 0, uncompressed);
68
69    let mut body = Vec::with_capacity(alg_seq.len() + bitstr.len());
70    body.extend_from_slice(&alg_seq);
71    body.extend_from_slice(&bitstr);
72
73    let mut out = Vec::with_capacity(body.len() + 4);
74    writer::write_sequence(&mut out, &body);
75    out
76}
77
78/// Decode a DER `SubjectPublicKeyInfo` blob into a validated
79/// [`crate::sm2::Sm2PublicKey`].
80///
81/// Validates:
82///
83/// - outer SEQUENCE with no trailing bytes;
84/// - `algorithm == id-ecPublicKey` and `parameters == sm2p256v1`;
85/// - `subjectPublicKey` BIT STRING with `unused_bits == 0` wrapping
86///   exactly 65 bytes;
87/// - the wrapped 65 bytes decode as an on-curve, non-identity SEC1
88///   uncompressed point.
89///
90/// Returns `None` for any malformed input.
91#[must_use]
92pub fn decode(input: &[u8]) -> Option<crate::sm2::Sm2PublicKey> {
93    let (body, rest) = reader::read_sequence(input)?;
94    if !rest.is_empty() {
95        return None;
96    }
97
98    // AlgorithmIdentifier SEQUENCE
99    let (alg_inner, body) = reader::read_sequence(body)?;
100    let (alg_oid, alg_inner) = reader::read_oid(alg_inner)?;
101    if alg_oid != ID_EC_PUBLIC_KEY {
102        return None;
103    }
104    // parameters = namedCurve OID = sm2p256v1
105    let (curve_oid, alg_inner) = reader::read_oid(alg_inner)?;
106    if curve_oid != SM2P256V1 || !alg_inner.is_empty() {
107        return None;
108    }
109
110    // subjectPublicKey BIT STRING
111    let (unused, pk_bytes, body) = reader::read_bit_string(body)?;
112    if unused != 0 || !body.is_empty() {
113        return None;
114    }
115    Some(crate::sm2::Sm2PublicKey::from_point(
116        decode_uncompressed_point(pk_bytes)?,
117    ))
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use crate::sm2::point::ProjectivePoint;
124
125    /// SPKI round-trip for the SM2 generator.
126    #[test]
127    fn round_trip_generator() {
128        let g = ProjectivePoint::generator();
129        let der = encode(&crate::sm2::Sm2PublicKey::from_point(g));
130        let recovered = decode(&der).expect("decode");
131        let (gx, gy) = g.to_affine().expect("G finite");
132        let (rx, ry) = recovered.point().to_affine().expect("recovered finite");
133        assert_eq!(rx.retrieve(), gx.retrieve());
134        assert_eq!(ry.retrieve(), gy.retrieve());
135    }
136
137    /// Encoded SPKI starts with the expected SEQUENCE tag and has
138    /// the right length.
139    #[test]
140    fn encoded_form_shape() {
141        let g = ProjectivePoint::generator();
142        let der = encode(&crate::sm2::Sm2PublicKey::from_point(g));
143        assert_eq!(der[0], 0x30, "outer tag must be SEQUENCE");
144        // SubjectPublicKeyInfo for SM2 is always 91 bytes: 2-byte SEQUENCE
145        // header + 18-byte AlgorithmIdentifier + 71-byte BIT STRING wrapping 65-byte key
146        // = 2 + 16 + 73 = 91 bytes.
147        assert_eq!(der.len(), 91, "SM2 SPKI is 91 bytes");
148    }
149
150    #[test]
151    fn rejects_wrong_algorithm_oid() {
152        // AlgorithmIdentifier with id-rsaEncryption (1.2.840.113549.1.1.1):
153        // 2A 86 48 86 F7 0D 01 01 01.
154        let mut alg_inner = Vec::new();
155        writer::write_oid(
156            &mut alg_inner,
157            &[0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01],
158        );
159        writer::write_null(&mut alg_inner);
160        let mut alg_seq = Vec::new();
161        writer::write_sequence(&mut alg_seq, &alg_inner);
162        let mut bitstr = Vec::new();
163        writer::write_bit_string(&mut bitstr, 0, &[0u8; 65]);
164        let mut body = Vec::new();
165        body.extend_from_slice(&alg_seq);
166        body.extend_from_slice(&bitstr);
167        let mut der = Vec::new();
168        writer::write_sequence(&mut der, &body);
169        assert!(decode(&der).is_none());
170    }
171
172    #[test]
173    fn rejects_wrong_curve_oid() {
174        // P-256 OID 1.2.840.10045.3.1.7 — same algorithm but wrong curve.
175        let mut alg_inner = Vec::new();
176        writer::write_oid(&mut alg_inner, ID_EC_PUBLIC_KEY);
177        writer::write_oid(
178            &mut alg_inner,
179            &[0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07],
180        );
181        let mut alg_seq = Vec::new();
182        writer::write_sequence(&mut alg_seq, &alg_inner);
183        let mut bitstr = Vec::new();
184        writer::write_bit_string(&mut bitstr, 0, &[0u8; 65]);
185        let mut body = Vec::new();
186        body.extend_from_slice(&alg_seq);
187        body.extend_from_slice(&bitstr);
188        let mut der = Vec::new();
189        writer::write_sequence(&mut der, &body);
190        assert!(decode(&der).is_none());
191    }
192
193    #[test]
194    fn rejects_off_curve_point() {
195        let mut alg_inner = Vec::new();
196        writer::write_oid(&mut alg_inner, ID_EC_PUBLIC_KEY);
197        writer::write_oid(&mut alg_inner, SM2P256V1);
198        let mut alg_seq = Vec::new();
199        writer::write_sequence(&mut alg_seq, &alg_inner);
200        let mut pt = [0u8; 65];
201        pt[0] = 0x04;
202        pt[1] = 1;
203        pt[33] = 1; // (1, 1) — off the curve.
204        let mut bitstr = Vec::new();
205        writer::write_bit_string(&mut bitstr, 0, &pt);
206        let mut body = Vec::new();
207        body.extend_from_slice(&alg_seq);
208        body.extend_from_slice(&bitstr);
209        let mut der = Vec::new();
210        writer::write_sequence(&mut der, &body);
211        assert!(decode(&der).is_none());
212    }
213
214    #[test]
215    fn rejects_trailing_bytes() {
216        let g = ProjectivePoint::generator();
217        let mut der = encode(&crate::sm2::Sm2PublicKey::from_point(g));
218        der.push(0x00);
219        assert!(decode(&der).is_none());
220    }
221}