Skip to content

Commit

Permalink
Refactor Ber/DerParser for GeneralizedTime and UtcTime, and add more …
Browse files Browse the repository at this point in the history
…tests
  • Loading branch information
chifflier committed Feb 27, 2025
1 parent 3cb77b7 commit 244e978
Show file tree
Hide file tree
Showing 2 changed files with 241 additions and 8 deletions.
133 changes: 129 additions & 4 deletions src/asn1_types/generalizedtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use alloc::format;
use alloc::string::String;
use core::convert::TryFrom;
use core::fmt;
use nom::{AsBytes, Input};
use nom::{AsBytes, Input as _};
#[cfg(feature = "datetime")]
use time::OffsetDateTime;

Expand Down Expand Up @@ -185,7 +185,6 @@ impl<'a, 'b> TryFrom<&'b Any<'a>> for GeneralizedTime {

fn try_from(any: &'b Any<'a>) -> Result<GeneralizedTime> {
any.tag().assert_eq(Self::TAG)?;
#[allow(clippy::trivially_copy_pass_by_ref)]
fn is_visible(b: u8) -> bool {
(0x20..=0x7f).contains(&b)
}
Expand All @@ -197,8 +196,82 @@ impl<'a, 'b> TryFrom<&'b Any<'a>> for GeneralizedTime {
}
}

impl DeriveBerParserFromTryFrom for GeneralizedTime {}
impl DeriveDerParserFromTryFrom for GeneralizedTime {}
impl<'i> BerParser<'i> for GeneralizedTime {
type Error = BerError<Input<'i>>;

fn check_tag(tag: Tag) -> bool {
tag == Tag::GeneralizedTime
}

fn from_any_ber(input: Input<'i>, header: Header<'i>) -> IResult<Input<'i>, Self, Self::Error> {
// GeneralizedTime is encoded as a VisibleString (X.680: 42.3) and can be constructed
// TODO: constructed GeneralizedTime not supported
if header.is_constructed() {
return Err(BerError::nom_err_input(&input, InnerError::Unsupported));
}

fn is_visible(b: u8) -> bool {
(0x20..=0x7f).contains(&b)
}
if !input.iter_elements().all(is_visible) {
return Err(BerError::nom_err_input(
&input,
InnerError::StringInvalidCharset,
));
}

let (rem, data) = input.take_split(input.len());
let time = GeneralizedTime::from_bytes(data.as_bytes2())
.map_err(|e| BerError::nom_err_input(&data, e.into()))?;
Ok((rem, time))
}
}

impl<'i> DerParser<'i> for GeneralizedTime {
type Error = BerError<Input<'i>>;

fn check_tag(tag: Tag) -> bool {
tag == Tag::GeneralizedTime
}

fn from_any_der(input: Input<'i>, header: Header<'i>) -> IResult<Input<'i>, Self, Self::Error> {
// Encoding shall be primitive (X.690: 10.2)
header.assert_primitive_input(&input).map_err(Err::Error)?;

fn is_visible(b: u8) -> bool {
(0x20..=0x7f).contains(&b)
}
if !input.iter_elements().all(is_visible) {
return Err(BerError::nom_err_input(
&input,
InnerError::StringInvalidCharset,
));
}

let (rem, data) = input.take_split(input.len());

// X.690 section 11.7.1: The encoding shall terminate with a "Z"
if data.as_bytes2().last() != Some(&b'Z') {
return Err(BerError::nom_err_input(
&data,
InnerError::DerConstraintFailed(DerConstraint::MissingTimeZone),
));
}
// The seconds element shall always be present (X.690: 11.7.2)
// XXX
// The decimal point element, if present, shall be the point option "." (X.690: 11.7.4)
if data.iter_elements().any(|b| b == b',') {
return Err(BerError::nom_err_input(
&data,
InnerError::DerConstraintFailed(DerConstraint::MissingSeconds),
));
}

let time = GeneralizedTime::from_bytes(data.as_bytes2())
.map_err(|e| BerError::nom_err_input(&data, e.into()))?;
Ok((rem, time))
}
}

impl fmt::Display for GeneralizedTime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Expand Down Expand Up @@ -305,3 +378,55 @@ impl ToDer for GeneralizedTime {
Ok(15 + num_digits)
}
}

#[cfg(test)]
mod tests {
use hex_literal::hex;

use crate::{ASN1TimeZone, DerConstraint, DerParser, GeneralizedTime, InnerError};

#[test]
fn parse_der_generalizedtime() {
let input = &hex!("18 0F 32 30 30 32 31 32 31 33 31 34 32 39 32 33 5A FF");
let (rem, result) = GeneralizedTime::parse_der(input.into()).expect("parsing failed");
assert_eq!(rem.as_bytes2(), &[0xff]);
#[cfg(feature = "datetime")]
{
use time::macros::datetime;
let datetime = datetime! {2002-12-13 14:29:23 UTC};
assert_eq!(result.utc_datetime(), Ok(datetime));
}
let _ = result;
// local time with fractional seconds (should fail: no 'Z' at end)
let input = b"\x18\x1019851106210627.3";
let result = GeneralizedTime::parse_der(input.into()).expect_err("should not parse");
assert!(matches!(
result,
nom::Err::Error(e) if *e.inner() == InnerError::DerConstraintFailed(DerConstraint::MissingTimeZone)

));
// coordinated universal time with fractional seconds
let input = b"\x18\x1119851106210627.3Z";
let (rem, result) = GeneralizedTime::parse_der(input.into()).expect("parsing failed");
assert!(rem.is_empty());
assert_eq!(result.0.millisecond, Some(300));
assert_eq!(result.0.tz, ASN1TimeZone::Z);
#[cfg(feature = "datetime")]
{
use time::macros::datetime;
let datetime = datetime! {1985-11-06 21:06:27.3 UTC};
assert_eq!(result.utc_datetime(), Ok(datetime));
}
#[cfg(feature = "std")]
let _ = result.to_string();
// local time with fractional seconds, and with local time 5 hours retarded in relation to coordinated universal time.
// (should fail: no 'Z' at end)
let input = b"\x18\x1519851106210627.3-0500";
let result = GeneralizedTime::parse_der(input.into()).expect_err("should not parse");
assert!(matches!(
result,
nom::Err::Error(e) if *e.inner() == InnerError::DerConstraintFailed(DerConstraint::MissingTimeZone)

));
}
}
116 changes: 112 additions & 4 deletions src/asn1_types/utctime.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate::*;
use core::convert::TryFrom;
use core::fmt;
use nom::Input;
use nom::Input as _;
#[cfg(feature = "datetime")]
use time::OffsetDateTime;

Expand Down Expand Up @@ -144,7 +144,6 @@ impl<'a, 'b> TryFrom<&'b Any<'a>> for UtcTime {

fn try_from(any: &'b Any<'a>) -> Result<UtcTime> {
any.tag().assert_eq(Self::TAG)?;
#[allow(clippy::trivially_copy_pass_by_ref)]
fn is_visible(b: u8) -> bool {
(0x20..=0x7f).contains(&b)
}
Expand All @@ -156,8 +155,65 @@ impl<'a, 'b> TryFrom<&'b Any<'a>> for UtcTime {
}
}

impl DeriveBerParserFromTryFrom for UtcTime {}
impl DeriveDerParserFromTryFrom for UtcTime {}
impl<'i> BerParser<'i> for UtcTime {
type Error = BerError<Input<'i>>;

fn check_tag(tag: Tag) -> bool {
tag == Tag::UtcTime
}

fn from_any_ber(input: Input<'i>, header: Header<'i>) -> IResult<Input<'i>, Self, Self::Error> {
// UtcTime is encoded as a VisibleString (X.680: 43.3) and can be constructed
// TODO: constructed UtcTime not supported
if header.is_constructed() {
return Err(BerError::nom_err_input(&input, InnerError::Unsupported));
}

fn is_visible(b: u8) -> bool {
(0x20..=0x7f).contains(&b)
}
if !input.iter_elements().all(is_visible) {
return Err(BerError::nom_err_input(
&input,
InnerError::StringInvalidCharset,
));
}

let (rem, data) = input.take_split(input.len());
let time = UtcTime::from_bytes(data.as_bytes2())
.map_err(|e| BerError::nom_err_input(&data, e.into()))?;
Ok((rem, time))
}
}

impl<'i> DerParser<'i> for UtcTime {
type Error = BerError<Input<'i>>;

fn check_tag(tag: Tag) -> bool {
tag == Tag::UtcTime
}

fn from_any_der(input: Input<'i>, header: Header<'i>) -> IResult<Input<'i>, Self, Self::Error> {
// Encoding shall be primitive (X.690: 10.2)
header.assert_primitive_input(&input).map_err(Err::Error)?;

fn is_visible(b: u8) -> bool {
(0x20..=0x7f).contains(&b)
}
if !input.iter_elements().all(is_visible) {
return Err(BerError::nom_err_input(
&input,
InnerError::StringInvalidCharset,
));
}

let (rem, data) = input.take_split(input.len());

let time = UtcTime::from_bytes(data.as_bytes2())
.map_err(|e| BerError::nom_err_input(&data, e.into()))?;
Ok((rem, time))
}
}

impl fmt::Display for UtcTime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Expand Down Expand Up @@ -223,3 +279,55 @@ impl ToDer for UtcTime {
Ok(13)
}
}

#[cfg(test)]
mod tests {
use hex_literal::hex;

use crate::{DerParser, Input, UtcTime};

#[test]
fn parse_der_utctime() {
let input = &hex!("17 0D 30 32 31 32 31 33 31 34 32 39 32 33 5A FF");
let (rem, result) = UtcTime::parse_der(Input::from(input)).expect("parsing failed");
assert_eq!(rem.as_bytes2(), &[0xff]);
#[cfg(feature = "datetime")]
{
use time::macros::datetime;
let datetime = datetime! {2-12-13 14:29:23 UTC};

assert_eq!(result.utc_datetime(), Ok(datetime));
}
#[cfg(feature = "std")]
let _ = result.to_string();
let _ = result;
//
let input = &hex!("17 11 30 32 31 32 31 33 31 34 32 39 32 33 2b 30 33 30 30 FF");
let (rem, result) = UtcTime::parse_der(Input::from(input)).expect("parsing failed");
assert_eq!(rem.as_bytes2(), &[0xff]);
#[cfg(feature = "datetime")]
{
use time::macros::datetime;
let datetime = datetime! {2-12-13 14:29:23 +03:00};

assert_eq!(result.utc_datetime(), Ok(datetime));
}
#[cfg(feature = "std")]
let _ = result.to_string();
let _ = result;
//
let input = &hex!("17 11 30 32 31 32 31 33 31 34 32 39 32 33 2d 30 33 30 30 FF");
let (rem, result) = UtcTime::parse_der(Input::from(input)).expect("parsing failed");
assert_eq!(rem.as_bytes2(), &[0xff]);
#[cfg(feature = "datetime")]
{
use time::macros::datetime;
let datetime = datetime! {2-12-13 14:29:23 -03:00};

assert_eq!(result.utc_datetime(), Ok(datetime));
}
#[cfg(feature = "std")]
let _ = result.to_string();
let _ = result;
}
}

0 comments on commit 244e978

Please sign in to comment.