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