Skip to main content

x509_info/keys/
mod.rs

1pub(crate) mod public_key;
2
3use crate::{Error, OidNames};
4use pkcs1::der::Decode;
5use x509_parser::{asn1_rs::ToDer, x509::AlgorithmIdentifier};
6
7/// Parameter inspection state, separate from algorithm recognition and trust.
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9#[cfg_attr(feature = "serde", derive(serde::Serialize))]
10#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
11#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
12#[non_exhaustive]
13pub enum ParameterStatus {
14    /// No parameters were encoded; does not imply they may legally be absent.
15    Absent,
16    /// Parameters were decoded within the documented supported representation.
17    Decoded,
18    /// Encoded parameters are retained but not interpreted by this library.
19    Unparsed,
20    /// Parameters violate a checked encoding rule or exceed the decoder's representation.
21    /// For PSS, pkcs1 currently supports salt lengths up to 255 and trailer field 1.
22    DecodeError,
23}
24
25/// Decoded RSA-PSS parameters, including RFC 8017 defaults.
26/// No assertion is made that the parameters are secure or usable with a given key.
27#[derive(Clone, Debug, PartialEq, Eq)]
28#[cfg_attr(feature = "serde", derive(serde::Serialize))]
29#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
30#[non_exhaustive]
31pub struct PssParameters {
32    /// Dotted-decimal message hash OID (SHA-1 when defaulted).
33    pub hash_oid: String,
34    /// Dotted-decimal mask-generation algorithm OID (MGF1 when defaulted).
35    pub mask_gen_oid: String,
36    /// MGF1 hash OID, if the mask-generation algorithm is MGF1 and includes it.
37    pub mask_gen_hash_oid: Option<String>,
38    /// Salt length in octets, including the default of 20.
39    pub salt_length: u32,
40    /// Trailer field, currently 1; other values produce DecodeError.
41    pub trailer_field: u32,
42}
43
44/// Algorithm information with an optional common label and preserved parameters.
45#[derive(Clone, Debug, PartialEq, Eq)]
46#[cfg_attr(feature = "serde", derive(serde::Serialize))]
47#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
48#[non_exhaustive]
49pub struct AlgorithmInfo {
50    /// Dotted-decimal algorithm OID; always retained even if unrecognized.
51    pub oid: String,
52    /// Common algorithm label. Recognition does not imply validity or support for use.
53    pub name: Option<String>,
54    /// DER encoding of parameters, when present. Original certificate DER is retained too.
55    pub parameters_der: Option<Vec<u8>>,
56    /// Whether the parameters were absent, decoded, unparsed or failed decoding.
57    pub parameter_status: ParameterStatus,
58    /// Decoded RSA-PSS parameters, when available.
59    pub pss: Option<PssParameters>,
60}
61
62pub(crate) fn inspect(
63    alg: &AlgorithmIdentifier<'_>,
64    signature: bool,
65    names: &OidNames,
66) -> Result<AlgorithmInfo, Error> {
67    let oid = alg.algorithm.to_id_string();
68    let name = names.get(&oid).map(str::to_owned);
69    let parameters_der = alg
70        .parameters
71        .as_ref()
72        .map(|p| p.to_der_vec().map_err(|_| Error::InvalidCertificate))
73        .transpose()?;
74    let mut parameter_status = if parameters_der.is_some() {
75        ParameterStatus::Unparsed
76    } else {
77        ParameterStatus::Absent
78    };
79    let mut pss = None;
80    if oid == "1.2.840.113549.1.1.10" {
81        if let Some(bytes) = parameters_der.as_deref() {
82            match pkcs1::RsaPssParams::from_der(bytes) {
83                Ok(p) => {
84                    let mask_gen_oid = p.mask_gen.oid.to_string();
85                    let mask_gen_hash_oid = if mask_gen_oid == "1.2.840.113549.1.1.8" {
86                        p.mask_gen.parameters.as_ref().map(|a| a.oid.to_string())
87                    } else {
88                        None
89                    };
90                    parameter_status = ParameterStatus::Decoded;
91                    pss = Some(PssParameters {
92                        hash_oid: p.hash.oid.to_string(),
93                        mask_gen_oid,
94                        mask_gen_hash_oid,
95                        salt_length: u32::from(p.salt_len),
96                        trailer_field: p.trailer_field as u32,
97                    });
98                }
99                Err(_) => parameter_status = ParameterStatus::DecodeError,
100            }
101        } else if signature {
102            // PSS SPKI parameters may be absent (unrestricted key); signature parameters may not.
103            parameter_status = ParameterStatus::DecodeError;
104        }
105    } else if matches!(
106        oid.as_str(),
107        "1.3.101.110" | "1.3.101.111" | "1.3.101.112" | "1.3.101.113"
108    ) && parameters_der.is_some()
109    {
110        parameter_status = ParameterStatus::DecodeError;
111    }
112    if oid == "1.2.840.10045.2.1" {
113        parameter_status = match alg.parameters.as_ref() {
114            Some(p)
115                if p.class() == x509_parser::asn1_rs::Class::Universal && p.as_oid().is_ok() =>
116            {
117                ParameterStatus::Decoded
118            }
119            Some(_) => ParameterStatus::Unparsed, // Explicit EC parameters are retained, not interpreted.
120            None => ParameterStatus::DecodeError,
121        };
122    }
123    Ok(AlgorithmInfo {
124        oid,
125        name,
126        parameters_der,
127        parameter_status,
128        pss,
129    })
130}
131
132pub(crate) fn rsa_bits(bytes: &[u8]) -> Option<usize> {
133    let key = pkcs1::RsaPublicKey::from_der(bytes).ok()?;
134    let modulus = key.modulus.as_bytes();
135    let Some(first) = modulus.iter().position(|b| *b != 0) else {
136        return Some(0);
137    };
138    Some((modulus.len() - first) * 8 - modulus[first].leading_zeros() as usize)
139}
140
141/// Inspection state for public-key bytes, separate from algorithm parameters.
142#[derive(Clone, Copy, Debug, PartialEq, Eq)]
143#[cfg_attr(feature = "serde", derive(serde::Serialize))]
144#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
145#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
146#[non_exhaustive]
147pub enum KeyDataStatus {
148    /// The algorithm or curve has no supported key representation here.
149    Unparsed,
150    /// Supported encoding/length was decoded; mathematical key validity is not checked.
151    Decoded,
152    /// A supported key encoding has invalid structure, length or unused bits.
153    DecodeError,
154}
155
156pub(crate) fn key_details(
157    oid: &str,
158    curve: Option<&str>,
159    bytes: &[u8],
160    unused: u8,
161) -> (Option<usize>, KeyDataStatus) {
162    let decoded = |bits| (Some(bits), KeyDataStatus::Decoded);
163    let failed = (None, KeyDataStatus::DecodeError);
164    match oid {
165        "1.2.840.113549.1.1.1" | "1.2.840.113549.1.1.10" => {
166            if unused != 0 {
167                return failed;
168            }
169            rsa_bits(bytes).map(decoded).unwrap_or(failed)
170        }
171        "1.3.101.110" | "1.3.101.112" => {
172            if bytes.len() == 32 && unused == 0 {
173                decoded(255)
174            } else {
175                failed
176            }
177        }
178        "1.3.101.111" | "1.3.101.113" => {
179            let encoded_length = if oid == "1.3.101.111" { 56 } else { 57 };
180            if bytes.len() == encoded_length && unused == 0 {
181                decoded(448)
182            } else {
183                failed
184            }
185        }
186        "1.2.840.10045.2.1" => {
187            let bits: usize = match curve {
188                Some(
189                    "1.2.840.10045.3.1.7"
190                    | "1.3.132.0.10"
191                    | "1.2.156.10197.1.301"
192                    | "1.3.36.3.3.2.8.1.1.7",
193                ) => 256,
194                Some("1.2.840.10045.3.1.1") => 192,
195                Some("1.3.132.0.33") => 224,
196                Some("1.3.36.3.3.2.8.1.1.13") => 512,
197                Some("1.3.132.0.34" | "1.3.36.3.3.2.8.1.1.11") => 384,
198                Some("1.3.132.0.35") => 521,
199                _ => return (None, KeyDataStatus::Unparsed),
200            };
201            let width = bits.div_ceil(8);
202            // Only SEC1 compressed/uncompressed encodings; no curve arithmetic.
203            let expected = match bytes.first() {
204                Some(2 | 3) => 1 + width,
205                Some(4) => 1 + 2 * width,
206                _ => return failed,
207            };
208            if bytes.len() == expected && unused == 0 {
209                decoded(bits)
210            } else {
211                failed
212            }
213        }
214        _ => (None, KeyDataStatus::Unparsed),
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221    use x509_parser::asn1_rs::{Any, FromDer, Oid};
222
223    fn pss(params: Option<&[u8]>, signature: bool) -> AlgorithmInfo {
224        let oid = Oid::from(&[1, 2, 840, 113549, 1, 1, 10]).unwrap();
225        let params = params.map(|p| Any::from_der(p).unwrap().1);
226        inspect(
227            &AlgorithmIdentifier::new(oid, params),
228            signature,
229            &OidNames::default(),
230        )
231        .unwrap()
232    }
233
234    #[test]
235    fn pss_defaults_absent_restrictions_and_decode_failures_are_distinct() {
236        let default = pss(Some(&[0x30, 0]), true);
237        assert_eq!(default.parameter_status, ParameterStatus::Decoded);
238        let p = default.pss.unwrap();
239        assert_eq!(p.hash_oid, "1.3.14.3.2.26");
240        assert_eq!(p.mask_gen_hash_oid.as_deref(), Some("1.3.14.3.2.26"));
241        assert_eq!(p.salt_length, 20);
242        assert_eq!(pss(None, false).parameter_status, ParameterStatus::Absent);
243        assert_eq!(
244            pss(None, true).parameter_status,
245            ParameterStatus::DecodeError
246        );
247        for bytes in [
248            &[5, 0][..],
249            &[0x30, 3, 0xa2, 1, 0],
250            &[0x30, 6, 0xa2, 4, 2, 2, 1, 0], // Salt 256 exceeds pkcs1's representation.
251            &[0x30, 2, 5, 0],                // Unconsumed inner data.
252        ] {
253            let info = pss(Some(bytes), true);
254            assert_eq!(info.parameter_status, ParameterStatus::DecodeError);
255            assert_eq!(info.parameters_der.as_deref(), Some(bytes));
256            assert_eq!(info.pss, None);
257        }
258    }
259
260    #[test]
261    fn montgomery_and_additional_prime_curves_use_their_actual_encodings() {
262        assert_eq!(
263            key_details("1.3.101.110", None, &[0; 32], 0),
264            (Some(255), KeyDataStatus::Decoded)
265        );
266        assert_eq!(
267            key_details("1.3.101.111", None, &[0; 56], 0),
268            (Some(448), KeyDataStatus::Decoded)
269        );
270        assert_eq!(
271            key_details("1.3.101.111", None, &[0; 57], 0),
272            (None, KeyDataStatus::DecodeError)
273        );
274        for (oid, bits) in [
275            ("1.2.840.10045.3.1.1", 192usize),
276            ("1.3.132.0.33", 224),
277            ("1.3.36.3.3.2.8.1.1.7", 256),
278            ("1.3.36.3.3.2.8.1.1.11", 384),
279            ("1.3.36.3.3.2.8.1.1.13", 512),
280        ] {
281            let bytes = vec![2; 1 + bits.div_ceil(8)];
282            assert_eq!(
283                key_details("1.2.840.10045.2.1", Some(oid), &bytes, 0),
284                (Some(bits), KeyDataStatus::Decoded)
285            );
286        }
287        // A registry label for ML-DSA does not imply key-format or signature support.
288        assert_eq!(
289            key_details("2.16.840.1.101.3.4.3.17", None, &[0; 32], 0),
290            (None, KeyDataStatus::Unparsed)
291        );
292    }
293
294    #[test]
295    fn key_encoding_failures_do_not_become_unknown_algorithms_or_fake_sizes() {
296        assert_eq!(
297            key_details("1.3.101.112", None, &[0; 31], 0),
298            (None, KeyDataStatus::DecodeError)
299        );
300        assert_eq!(
301            key_details("1.3.101.113", None, &[0; 57], 1),
302            (None, KeyDataStatus::DecodeError)
303        );
304        assert_eq!(
305            key_details("1.2.3.4", None, &[0; 32], 0),
306            (None, KeyDataStatus::Unparsed)
307        );
308        assert_eq!(
309            key_details("1.2.840.10045.2.1", Some("1.2.3.4"), &[4; 65], 0),
310            (None, KeyDataStatus::Unparsed)
311        );
312        assert_eq!(
313            key_details(
314                "1.2.840.10045.2.1",
315                Some("1.2.840.10045.3.1.7"),
316                &[4; 64],
317                0
318            ),
319            (None, KeyDataStatus::DecodeError)
320        );
321        assert_eq!(
322            key_details("1.2.840.113549.1.1.1", None, &[0x30, 0], 0),
323            (None, KeyDataStatus::DecodeError)
324        );
325    }
326}