use x509_parser::asn1_rs::{Any, Class, FromDer, Tag};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
#[non_exhaustive]
pub enum DecodeIssue {
InvalidEncoding,
UnsupportedEncoding,
RepresentationLimit,
UnknownType,
Unclassified,
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("{field}: {issue:?}")]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub struct DecodeDiagnostic {
pub issue: DecodeIssue,
pub field: String,
}
impl DecodeDiagnostic {
pub(crate) fn new(issue: DecodeIssue, field: &str) -> Self {
Self {
issue,
field: field.into(),
}
}
pub(crate) fn invalid(field: &str) -> Self {
Self::new(DecodeIssue::InvalidEncoding, field)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[non_exhaustive]
pub struct IntegerValue {
pub content_hex: String,
pub value: Option<i64>,
}
pub(crate) fn any(bytes: &[u8]) -> Result<Any<'_>, DecodeDiagnostic> {
let (rest, value) = Any::from_der(bytes).map_err(|_| DecodeDiagnostic::invalid("DER"))?;
if !rest.is_empty() {
return Err(DecodeDiagnostic::invalid("trailing DER"));
}
Ok(value)
}
pub(crate) fn children<'a>(value: &Any<'a>, tag: Tag) -> Result<Vec<Any<'a>>, DecodeDiagnostic> {
if value.class() != Class::Universal || value.tag() != tag || !value.header.is_constructed() {
return Err(DecodeDiagnostic::invalid("sequence/set"));
}
let mut input = value.data;
let mut result = Vec::new();
while !input.is_empty() {
let (rest, item) =
Any::from_der(input).map_err(|_| DecodeDiagnostic::invalid("nested DER"))?;
result.push(item);
input = rest;
}
Ok(result)
}
pub(crate) fn integer(value: &Any<'_>) -> Result<IntegerValue, DecodeDiagnostic> {
if value.class() != Class::Universal
|| value.tag() != Tag::Integer
|| value.header.is_constructed()
|| value.data.is_empty()
{
return Err(DecodeDiagnostic::invalid("INTEGER"));
}
if value.data.len() > 1
&& ((value.data[0] == 0 && value.data[1] & 0x80 == 0)
|| (value.data[0] == 255 && value.data[1] & 0x80 != 0))
{
return Err(DecodeDiagnostic::invalid("INTEGER encoding"));
}
Ok(IntegerValue {
content_hex: hex::encode(value.data),
value: value.as_i64().ok(),
})
}
pub(crate) fn text(value: &Any<'_>) -> Result<String, DecodeDiagnostic> {
use x509_parser::asn1_rs::{
Ia5String, NumericString, PrintableString, UniversalString, Utf8String, VisibleString,
};
if value.class() != Class::Universal || value.header.is_constructed() {
return Err(DecodeDiagnostic::invalid("string tag"));
}
let bad = || DecodeDiagnostic::invalid("string encoding");
match value.tag() {
Tag::Utf8String => Utf8String::try_from(value)
.map(|v| v.as_ref().to_owned())
.map_err(|_| bad()),
Tag::PrintableString => PrintableString::try_from(value)
.map(|v| v.as_ref().to_owned())
.map_err(|_| bad()),
Tag::NumericString => NumericString::try_from(value)
.map(|v| v.as_ref().to_owned())
.map_err(|_| bad()),
Tag::Ia5String => Ia5String::try_from(value)
.map(|v| v.as_ref().to_owned())
.map_err(|_| bad()),
Tag::VisibleString => VisibleString::try_from(value)
.map(|v| v.as_ref().to_owned())
.map_err(|_| bad()),
Tag::UniversalString => UniversalString::try_from(value)
.map(|v| v.string())
.map_err(|_| bad()),
Tag::BmpString => x509_cert::der::asn1::BmpString::from_ucs2(value.data)
.map(|v| v.to_string())
.map_err(|_| bad()),
_ => Err(DecodeDiagnostic::new(
DecodeIssue::UnsupportedEncoding,
"string tag",
)),
}
}
pub(crate) fn der_error(error: x509_cert::der::Error, field: &str) -> DecodeDiagnostic {
use x509_cert::der::ErrorKind;
let issue = match error.kind() {
ErrorKind::Overlength | ErrorKind::NestingDepth => DecodeIssue::RepresentationLimit,
ErrorKind::TagUnknown { .. } => DecodeIssue::UnsupportedEncoding,
_ => DecodeIssue::InvalidEncoding,
};
DecodeDiagnostic::new(issue, field)
}
impl crate::ExtensionInfo {
#[allow(clippy::unnecessary_map_or)]
pub fn diagnostic(&self) -> Option<DecodeDiagnostic> {
use crate::ExtensionDetails;
if matches!(self.details, ExtensionDetails::Unsupported) {
return Some(DecodeDiagnostic::new(
DecodeIssue::UnknownType,
"extension OID",
));
}
if !matches!(self.details, ExtensionDetails::Malformed) {
return None;
}
if let Err(e) = crate::extensions::device::decode(&self.oid, &self.value_der) {
return Some(e);
}
let value = match any(&self.value_der) {
Ok(v) => v,
Err(e) => return Some(e),
};
if self.oid == "2.5.29.54" {
return Some(match integer(&value) {
Ok(v)
if !value.data.is_empty()
&& value.data[0] & 0x80 == 0
&& v.value.map_or(true, |n| n > i64::from(u32::MAX)) =>
{
DecodeDiagnostic::new(DecodeIssue::RepresentationLimit, "skipCerts u32")
}
_ => DecodeDiagnostic::invalid("skipCerts INTEGER"),
});
}
if self.oid == "2.5.29.30" {
use x509_cert::der::{Decode, ErrorKind, Tag, TagNumber};
if let Err(e) = x509_cert::ext::pkix::NameConstraints::from_der(&self.value_der) {
if matches!(
e.kind(),
ErrorKind::TagUnexpected {
actual: Tag::ContextSpecific {
number: TagNumber(3),
..
},
..
}
) {
return Some(DecodeDiagnostic::new(
DecodeIssue::UnsupportedEncoding,
"X.400 constraint",
));
}
return Some(der_error(e, "name constraints"));
}
}
Some(DecodeDiagnostic::new(
DecodeIssue::Unclassified,
"extension fields",
))
}
}