1use x509_parser::asn1_rs::{Any, Class, FromDer, Tag};
2
3#[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 InvalidEncoding,
12 UnsupportedEncoding,
14 RepresentationLimit,
16 UnknownType,
18 Unclassified,
20}
21
22#[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 pub issue: DecodeIssue,
31 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#[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 pub content_hex: String,
54 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 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 #[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}