use core::error::Error as StdError;
use core::fmt;
use core::marker::PhantomData;
use untrusted::{Input, Reader};
pub(crate) struct SctParser<'a> {
reader: Reader<'a>,
}
impl<'a> SctParser<'a> {
pub(crate) fn new(input: Option<Input<'a>>) -> Result<Self, Error> {
Ok(SctParser {
reader: match input {
Some(input) => Reader::new(
input.read_all(Error::MalformedSct, |rd| read_field(rd, non_zero_u16_len))?,
),
None => Reader::new(Input::from(&[])),
},
})
}
}
impl<'a> Iterator for SctParser<'a> {
type Item = Result<SignedCertificateTimestamp<'a>, Error>;
fn next(&mut self) -> Option<Self::Item> {
if self.reader.at_end() {
return None;
}
Some(
read_field(&mut self.reader, non_zero_u16_len)
.and_then(|item| SignedCertificateTimestamp::try_from(item.as_slice_less_safe())),
)
}
}
#[derive(Debug, PartialEq)]
pub(crate) struct SignedCertificateTimestamp<'a> {
log_id: [u8; 32],
timestamp_ms: u64,
_future_lifetime_for_signature: PhantomData<&'a ()>,
}
impl SignedCertificateTimestamp<'_> {
pub(crate) fn log_id_and_timestamp(&self) -> LogIdAndTimestamp {
LogIdAndTimestamp {
log_id: self.log_id,
timestamp_ms: self.timestamp_ms,
}
}
}
impl<'a> TryFrom<&'a [u8]> for SignedCertificateTimestamp<'a> {
type Error = Error;
fn try_from(bytes: &'a [u8]) -> Result<Self, Self::Error> {
let input = Input::from(bytes);
input.read_all(Error::MalformedSct, |rd| {
match read_array(rd)? {
[0] => {}
_ => return Err(Error::UnsupportedSctVersion),
};
let log_id = read_array(rd)?;
let timestamp_ms = u64::from_be_bytes(read_array(rd)?);
let _extensions = read_field(rd, any_u16_len)?;
let _signature_algorithm = u16::from_be_bytes(read_array(rd)?);
let _signature = read_field(rd, non_zero_u16_len)?;
Ok(SignedCertificateTimestamp {
log_id,
timestamp_ms,
_future_lifetime_for_signature: PhantomData,
})
})
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct LogIdAndTimestamp {
pub log_id: [u8; 32],
pub timestamp_ms: u64,
}
fn read_field<'a, const N: usize>(
rd: &mut Reader<'a>,
into_size: fn([u8; N]) -> Result<usize, Error>,
) -> Result<Input<'a>, Error> {
let len = into_size(read_array::<N>(rd)?)?;
rd.read_bytes(len).map_err(|_| Error::MalformedSct)
}
fn read_array<const N: usize>(rd: &mut Reader<'_>) -> Result<[u8; N], Error> {
rd.read_bytes(N)
.map_err(|_| Error::MalformedSct)?
.as_slice_less_safe()
.try_into()
.map_err(|_| Error::MalformedSct)
}
fn any_u16_len(bytes: [u8; 2]) -> Result<usize, Error> {
Ok(usize::from(u16::from_be_bytes(bytes)))
}
fn non_zero_u16_len(bytes: [u8; 2]) -> Result<usize, Error> {
match any_u16_len(bytes)? {
0 => Err(Error::MalformedSct),
len => Ok(len),
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum Error {
MalformedSct,
UnsupportedSctVersion,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MalformedSct => write!(f, "malformed SCT"),
Self::UnsupportedSctVersion => write!(f, "unsupported SCT version"),
}
}
}
impl StdError for Error {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn absent_sct_sequence() {
assert!(SctParser::new(None).unwrap().next().is_none());
}
#[test]
fn empty_sct_sequence() {
assert_eq!(
Some(Error::MalformedSct),
SctParser::new(Some(Input::from(&[]))).err()
);
}
#[test]
fn truncated_sct_length_in_sequence() {
assert_eq!(
Some(Error::MalformedSct),
SctParser::new(Some(Input::from(&[0]))).err()
);
}
#[test]
fn empty_sct_in_sequence() {
assert_eq!(
Some(Error::MalformedSct),
SctParser::new(Some(Input::from(&[0, 0]))).err()
);
}
#[test]
fn truncated_sct_in_sequence() {
assert_eq!(
Some(Error::MalformedSct),
SctParser::new(Some(Input::from(&[0, 1]))).err()
);
}
#[test]
fn sample_sct() {
let bytes = [
0x0, 0x32, 0x0, 0x30, 0x0, b'l', b'l', b'l', b'l', b'l', b'l', b'l', b'l', b'l', b'l', b'l', b'l', b'l', b'l',
b'l', b'l', b'l', b'l', b'l', b'l', b'l', b'l', b'l', b'l', b'l', b'l', b'l', b'l',
b'l', b'l', b'l', b'l', b't', b't', b't', b't', b't', b't', b't', b't', 0x0, 0x0, b's', b'a', 0x0, 0x1, b's', ];
SignedCertificateTimestamp::try_from(&bytes[4..]).unwrap();
assert_eq!(
SignedCertificateTimestamp {
log_id: [b'l'; 32],
timestamp_ms: 0x74747474_74747474,
_future_lifetime_for_signature: PhantomData
},
SctParser::new(Some(Input::from(&bytes)))
.unwrap()
.next()
.unwrap()
.unwrap()
);
}
#[test]
fn illegal_empty_signature() {
let bytes = [
0x0, b'l', b'l', b'l', b'l', b'l', b'l', b'l', b'l', b'l', b'l', b'l', b'l', b'l', b'l',
b'l', b'l', b'l', b'l', b'l', b'l', b'l', b'l', b'l', b'l', b'l', b'l', b'l', b'l',
b'l', b'l', b'l', b'l', b't', b't', b't', b't', b't', b't', b't', b't', 0x0, 0x0, b's', b'a', 0x0, 0x0, ];
assert_eq!(
Some(Error::MalformedSct),
SignedCertificateTimestamp::try_from(&bytes[..]).err()
);
}
#[test]
fn illegal_unknown_version() {
let bytes = [
0x1, b'l', b'l', b'l', b'l', b'l', b'l', b'l', b'l', b'l', b'l', b'l', b'l', b'l', b'l',
b'l', b'l', b'l', b'l', b'l', b'l', b'l', b'l', b'l', b'l', b'l', b'l', b'l', b'l',
b'l', b'l', b'l', b'l', b't', b't', b't', b't', b't', b't', b't', b't', 0x0, 0x0, b's', b'a', 0x0, 0x1, b's', ];
assert_eq!(
Some(Error::UnsupportedSctVersion),
SignedCertificateTimestamp::try_from(&bytes[..]).err()
);
}
#[test]
fn illegal_trailing_extension_data() {
let bytes = [
0x0, 0x1, b'?', b'x', ];
assert_eq!(
Some(Error::MalformedSct),
SctParser::new(Some(Input::from(&bytes))).err()
);
}
}