use {
crate::asn1::{
rfc3161::{
MessageImprint, PkiStatus, TimeStampReq, TimeStampResp, TstInfo,
OID_CONTENT_TYPE_TST_INFO,
},
rfc5652::{SignedData, OID_ID_SIGNED_DATA},
},
bcder::{
decode::{Constructed, DecodeError, IntoSource, Source},
encode::Values,
Integer, OctetString,
},
reqwest::IntoUrl,
ring::rand::SecureRandom,
std::{convert::Infallible, ops::Deref},
x509_certificate::DigestAlgorithm,
};
pub const HTTP_CONTENT_TYPE_REQUEST: &str = "application/timestamp-query";
pub const HTTP_CONTENT_TYPE_RESPONSE: &str = "application/timestamp-reply";
#[derive(Debug)]
pub enum TimeStampError {
Io(std::io::Error),
Reqwest(reqwest::Error),
Asn1Decode(DecodeError<Infallible>),
Http(&'static str),
Random,
NonceMismatch,
Unsuccessful(TimeStampResp),
BadResponse,
}
impl std::fmt::Display for TimeStampError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io(e) => f.write_fmt(format_args!("I/O error: {}", e)),
Self::Reqwest(e) => f.write_fmt(format_args!("HTTP error: {}", e)),
Self::Asn1Decode(e) => f.write_fmt(format_args!("ASN.1 decode error: {}", e)),
Self::Http(msg) => f.write_str(msg),
Self::Random => f.write_str("error generating random nonce"),
Self::NonceMismatch => f.write_str("nonce mismatch"),
Self::Unsuccessful(r) => f.write_fmt(format_args!(
"unsuccessful Time-Stamp Protocol response: {:?}: {:?}",
r.status.status, r.status.status_string
)),
Self::BadResponse => f.write_str("bad server response"),
}
}
}
impl std::error::Error for TimeStampError {}
impl From<std::io::Error> for TimeStampError {
fn from(e: std::io::Error) -> Self {
Self::Io(e)
}
}
impl From<reqwest::Error> for TimeStampError {
fn from(e: reqwest::Error) -> Self {
Self::Reqwest(e)
}
}
impl From<DecodeError<Infallible>> for TimeStampError {
fn from(e: DecodeError<Infallible>) -> Self {
Self::Asn1Decode(e)
}
}
pub struct TimeStampResponse(TimeStampResp);
impl Deref for TimeStampResponse {
type Target = TimeStampResp;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl TimeStampResponse {
pub fn is_success(&self) -> bool {
matches!(
self.0.status.status,
PkiStatus::Granted | PkiStatus::GrantedWithMods
)
}
pub fn token_content_size(&self) -> Option<usize> {
self.0
.time_stamp_token
.as_ref()
.map(|token| token.content.len())
}
pub fn signed_data(&self) -> Result<Option<SignedData>, DecodeError<Infallible>> {
if let Some(token) = &self.0.time_stamp_token {
let source = token.content.clone();
if token.content_type == OID_ID_SIGNED_DATA {
Ok(Some(source.decode(SignedData::take_from)?))
} else {
Err(source
.into_source()
.content_err("invalid OID on signed data"))
}
} else {
Ok(None)
}
}
pub fn tst_info(&self) -> Result<Option<TstInfo>, DecodeError<Infallible>> {
if let Some(signed_data) = self.signed_data()? {
if signed_data.content_info.content_type == OID_CONTENT_TYPE_TST_INFO {
if let Some(content) = signed_data.content_info.content {
Ok(Some(Constructed::decode(
content.to_bytes(),
bcder::Mode::Der,
TstInfo::take_from,
)?))
} else {
Ok(None)
}
} else {
Ok(None)
}
} else {
Ok(None)
}
}
}
impl From<TimeStampResp> for TimeStampResponse {
fn from(resp: TimeStampResp) -> Self {
Self(resp)
}
}
pub fn time_stamp_request_http(
url: impl IntoUrl,
request: &TimeStampReq,
) -> Result<TimeStampResponse, TimeStampError> {
let client = reqwest::blocking::Client::new();
let mut body = Vec::<u8>::new();
request
.encode_ref()
.write_encoded(bcder::Mode::Der, &mut body)?;
let response = client
.post(url)
.header("Content-Type", HTTP_CONTENT_TYPE_REQUEST)
.body(body)
.send()?;
if response.status().is_success()
&& response.headers().get("Content-Type")
== Some(&reqwest::header::HeaderValue::from_static(
HTTP_CONTENT_TYPE_RESPONSE,
))
{
let response_bytes = response.bytes()?;
let res = TimeStampResponse(Constructed::decode(
response_bytes.as_ref(),
bcder::Mode::Der,
TimeStampResp::take_from,
)?);
if res.is_success() {
if let Some(tst_info) = res.tst_info()? {
if tst_info.nonce != request.nonce {
return Err(TimeStampError::NonceMismatch);
}
}
}
Ok(res)
} else {
Err(TimeStampError::Http("bad HTTP response"))
}
}
pub fn time_stamp_message_http(
url: impl IntoUrl,
message: &[u8],
digest_algorithm: DigestAlgorithm,
) -> Result<TimeStampResponse, TimeStampError> {
let mut h = digest_algorithm.digester();
h.update(message);
let digest = h.finish();
let mut random = [0u8; 8];
ring::rand::SystemRandom::new()
.fill(&mut random)
.map_err(|_| TimeStampError::Random)?;
let request = TimeStampReq {
version: Integer::from(1),
message_imprint: MessageImprint {
hash_algorithm: digest_algorithm.into(),
hashed_message: OctetString::new(bytes::Bytes::copy_from_slice(digest.as_ref())),
},
req_policy: None,
nonce: Some(Integer::from(u64::from_le_bytes(random))),
cert_req: Some(true),
extensions: None,
};
time_stamp_request_http(url, &request)
}
#[cfg(test)]
mod test {
use super::*;
const DIGICERT_TIMESTAMP_URL: &str = "http://timestamp.digicert.com";
#[test]
fn verify_static() {
let signed_data =
crate::SignedData::parse_ber(include_bytes!("testdata/tsp-signed-data.der")).unwrap();
for signer in signed_data.signers() {
signer
.verify_message_digest_with_signed_data(&signed_data)
.unwrap();
signer
.verify_signature_with_signed_data(&signed_data)
.unwrap();
}
}
#[test]
fn simple_request() {
let message = b"hello, world";
let res = time_stamp_message_http(DIGICERT_TIMESTAMP_URL, message, DigestAlgorithm::Sha256)
.unwrap();
let signed_data = res.signed_data().unwrap().unwrap();
assert_eq!(
signed_data.content_info.content_type,
OID_CONTENT_TYPE_TST_INFO
);
let tst_info = res.tst_info().unwrap().unwrap();
assert_eq!(tst_info.version, Integer::from(1));
let parsed = crate::SignedData::try_from(&signed_data).unwrap();
for signer in parsed.signers() {
signer
.verify_message_digest_with_signed_data(&parsed)
.unwrap();
signer.verify_signature_with_signed_data(&parsed).unwrap();
}
}
}