Skip to main content

x509_info/
decoding.rs

1use x509_parser::asn1_rs::{Any, Class, FromDer, Tag};
2
3/// Why a field could not be decoded; none of these categories is a trust verdict.
4#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5#[cfg_attr(feature = "serde", derive(serde::Serialize))]
6#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
7#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
8#[non_exhaustive]
9pub enum DecodeIssue {
10    /// Invalid structure, length, character encoding, or trailing data.
11    InvalidEncoding,
12    /// The field uses a recognized but unimplemented encoding/choice.
13    UnsupportedEncoding,
14    /// A backend cannot represent the encoded value or resource depth.
15    RepresentationLimit,
16    /// The OID/algorithm has no registered field decoder.
17    UnknownType,
18    /// An older backend path does not expose a more precise failure category.
19    Unclassified,
20}
21
22/// Caller-owned field decoding diagnostic; not a certificate validation error.
23#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
24#[error("{field}: {issue:?}")]
25#[cfg_attr(feature = "serde", derive(serde::Serialize))]
26#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
27#[non_exhaustive]
28pub struct DecodeDiagnostic {
29    /// Stable category suitable for matching without parsing Display text.
30    pub issue: DecodeIssue,
31    /// Field or encoding being inspected; contains no input data.
32    pub field: String,
33}
34impl DecodeDiagnostic {
35    pub(crate) fn new(issue: DecodeIssue, field: &str) -> Self {
36        Self {
37            issue,
38            field: field.into(),
39        }
40    }
41    pub(crate) fn invalid(field: &str) -> Self {
42        Self::new(DecodeIssue::InvalidEncoding, field)
43    }
44}
45
46/// ASN.1 INTEGER value without a machine-integer truncation or sign guess.
47#[derive(Clone, Debug, PartialEq, Eq)]
48#[cfg_attr(feature = "serde", derive(serde::Serialize))]
49#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
50#[non_exhaustive]
51pub struct IntegerValue {
52    /// Complete two's-complement content octets in lowercase hex, retaining padding.
53    pub content_hex: String,
54    /// Value when representable as i64; None otherwise, with bytes still available.
55    pub value: Option<i64>,
56}
57
58pub(crate) fn any(bytes: &[u8]) -> Result<Any<'_>, DecodeDiagnostic> {
59    let (rest, value) = Any::from_der(bytes).map_err(|_| DecodeDiagnostic::invalid("DER"))?;
60    if !rest.is_empty() {
61        return Err(DecodeDiagnostic::invalid("trailing DER"));
62    }
63    Ok(value)
64}
65pub(crate) fn children<'a>(value: &Any<'a>, tag: Tag) -> Result<Vec<Any<'a>>, DecodeDiagnostic> {
66    if value.class() != Class::Universal || value.tag() != tag || !value.header.is_constructed() {
67        return Err(DecodeDiagnostic::invalid("sequence/set"));
68    }
69    let mut input = value.data;
70    let mut result = Vec::new();
71    while !input.is_empty() {
72        let (rest, item) =
73            Any::from_der(input).map_err(|_| DecodeDiagnostic::invalid("nested DER"))?;
74        result.push(item);
75        input = rest;
76    }
77    Ok(result)
78}
79pub(crate) fn integer(value: &Any<'_>) -> Result<IntegerValue, DecodeDiagnostic> {
80    if value.class() != Class::Universal
81        || value.tag() != Tag::Integer
82        || value.header.is_constructed()
83        || value.data.is_empty()
84    {
85        return Err(DecodeDiagnostic::invalid("INTEGER"));
86    }
87    // Reject redundant sign octets, but do not impose positivity or a value range.
88    if value.data.len() > 1
89        && ((value.data[0] == 0 && value.data[1] & 0x80 == 0)
90            || (value.data[0] == 255 && value.data[1] & 0x80 != 0))
91    {
92        return Err(DecodeDiagnostic::invalid("INTEGER encoding"));
93    }
94    Ok(IntegerValue {
95        content_hex: hex::encode(value.data),
96        value: value.as_i64().ok(),
97    })
98}
99pub(crate) fn text(value: &Any<'_>) -> Result<String, DecodeDiagnostic> {
100    use x509_parser::asn1_rs::{
101        Ia5String, NumericString, PrintableString, UniversalString, Utf8String, VisibleString,
102    };
103    if value.class() != Class::Universal || value.header.is_constructed() {
104        return Err(DecodeDiagnostic::invalid("string tag"));
105    }
106    let bad = || DecodeDiagnostic::invalid("string encoding");
107    match value.tag() {
108        Tag::Utf8String => Utf8String::try_from(value)
109            .map(|v| v.as_ref().to_owned())
110            .map_err(|_| bad()),
111        Tag::PrintableString => PrintableString::try_from(value)
112            .map(|v| v.as_ref().to_owned())
113            .map_err(|_| bad()),
114        Tag::NumericString => NumericString::try_from(value)
115            .map(|v| v.as_ref().to_owned())
116            .map_err(|_| bad()),
117        Tag::Ia5String => Ia5String::try_from(value)
118            .map(|v| v.as_ref().to_owned())
119            .map_err(|_| bad()),
120        Tag::VisibleString => VisibleString::try_from(value)
121            .map(|v| v.as_ref().to_owned())
122            .map_err(|_| bad()),
123        Tag::UniversalString => UniversalString::try_from(value)
124            .map(|v| v.string())
125            .map_err(|_| bad()),
126        Tag::BmpString => x509_cert::der::asn1::BmpString::from_ucs2(value.data)
127            .map(|v| v.to_string())
128            .map_err(|_| bad()),
129        _ => Err(DecodeDiagnostic::new(
130            DecodeIssue::UnsupportedEncoding,
131            "string tag",
132        )),
133    }
134}
135
136pub(crate) fn der_error(error: x509_cert::der::Error, field: &str) -> DecodeDiagnostic {
137    use x509_cert::der::ErrorKind;
138    let issue = match error.kind() {
139        ErrorKind::Overlength | ErrorKind::NestingDepth => DecodeIssue::RepresentationLimit,
140        ErrorKind::TagUnknown { .. } => DecodeIssue::UnsupportedEncoding,
141        _ => DecodeIssue::InvalidEncoding,
142    };
143    DecodeDiagnostic::new(issue, field)
144}
145
146impl crate::ExtensionInfo {
147    /// Explain an unsupported/malformed extension using retained bytes where possible.
148    /// The legacy details enum is unchanged. Unclassified means the older decoding
149    /// path discarded its cause; it must not be interpreted as proven invalid DER.
150    /// No validity, critical-extension policy, or trust checks are performed.
151    #[allow(clippy::unnecessary_map_or)]
152    pub fn diagnostic(&self) -> Option<DecodeDiagnostic> {
153        use crate::ExtensionDetails;
154        if matches!(self.details, ExtensionDetails::Unsupported) {
155            return Some(DecodeDiagnostic::new(
156                DecodeIssue::UnknownType,
157                "extension OID",
158            ));
159        }
160        if !matches!(self.details, ExtensionDetails::Malformed) {
161            return None;
162        }
163        if let Err(e) = crate::extensions::device::decode(&self.oid, &self.value_der) {
164            return Some(e);
165        }
166        let value = match any(&self.value_der) {
167            Ok(v) => v,
168            Err(e) => return Some(e),
169        };
170        if self.oid == "2.5.29.54" {
171            return Some(match integer(&value) {
172                Ok(v)
173                    if !value.data.is_empty()
174                        && value.data[0] & 0x80 == 0
175                        && v.value.map_or(true, |n| n > i64::from(u32::MAX)) =>
176                {
177                    DecodeDiagnostic::new(DecodeIssue::RepresentationLimit, "skipCerts u32")
178                }
179                _ => DecodeDiagnostic::invalid("skipCerts INTEGER"),
180            });
181        }
182        if self.oid == "2.5.29.30" {
183            use x509_cert::der::{Decode, ErrorKind, Tag, TagNumber};
184            if let Err(e) = x509_cert::ext::pkix::NameConstraints::from_der(&self.value_der) {
185                if matches!(
186                    e.kind(),
187                    ErrorKind::TagUnexpected {
188                        actual: Tag::ContextSpecific {
189                            number: TagNumber(3),
190                            ..
191                        },
192                        ..
193                    }
194                ) {
195                    return Some(DecodeDiagnostic::new(
196                        DecodeIssue::UnsupportedEncoding,
197                        "X.400 constraint",
198                    ));
199                }
200                return Some(der_error(e, "name constraints"));
201            }
202        }
203        Some(DecodeDiagnostic::new(
204            DecodeIssue::Unclassified,
205            "extension fields",
206        ))
207    }
208}