mod tsp;
use sha2::{self, Digest};
use std::{fs::File, io::Read};
use tsp::TimeStampRequest;
pub use tsp::TimeStampResponse;
#[derive(Debug, PartialEq)]
pub enum Error {
InvalidDigest,
RequestNotAccepted(Option<String>),
InvalidServerResponse,
DigestMismatch,
}
impl std::error::Error for Error {}
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Error::InvalidDigest => write!(
f,
"The provided digest is none of SHA-224, SHA-256, SHA-384, or SHA-512"
),
Error::RequestNotAccepted(details) => {
let details = details
.clone()
.map_or(String::from(""), |s| format!(": {}", s));
write!(
f,
"Timestamp request was not accepted by the server{}",
details
)
}
Error::InvalidServerResponse => write!(
f,
"The response from the server is not as expected according to the RFC 3161 standard."
),
Error::DigestMismatch => write!(
f,
"The timestamped digest does not match the provided digest"
),
}
}
}
pub fn request_timestamp_for_digest(
tsa_uri: &str,
digest: &str,
) -> Result<TimeStampResponse, Box<dyn std::error::Error>> {
let data = hex::decode(digest).or(Err(Error::InvalidDigest))?;
request_timestamp(tsa_uri, data)
}
pub fn request_timestamp_for_file(
tsa_uri: &str,
filename: &str,
) -> Result<TimeStampResponse, Box<dyn std::error::Error>> {
let mut file = File::open(filename)?;
let mut file_content = vec![];
file.read_to_end(&mut file_content)?;
let digest = sha2::Sha256::digest(file_content);
request_timestamp(tsa_uri, digest.to_vec())
}
fn request_timestamp(
tsa_uri: &str,
digest: Vec<u8>,
) -> Result<TimeStampResponse, Box<dyn std::error::Error>> {
let timestamp_request = TimeStampRequest::new(digest)?;
let body = ureq::post(tsa_uri)
.header("Content-Type", "application/timestamp-query")
.send(timestamp_request.to_der()?)?
.body_mut()
.read_to_vec()?;
let timestamp = TimeStampResponse::new(body);
timestamp.verify(×tamp_request)?;
Ok(timestamp)
}
#[cfg(test)]
mod tests {
use super::*;
use cmpv2::status::PkiStatus;
use der::Decode;
#[test]
fn timestamp_request_for_file_successful() {
let filename = "Cargo.toml";
let response =
request_timestamp_for_file("http://timestamp.digicert.com", filename).unwrap();
let x509_response = x509_tsp::TimeStampResp::from_der(response.as_der_encoded()).unwrap();
assert_eq!(x509_response.status.status, PkiStatus::Accepted);
assert_eq!(
response.datetime().unwrap().date_naive(),
chrono::Utc::now().date_naive()
);
}
#[test]
fn timestamp_for_nonexistent_file_rejected() {
assert!(
request_timestamp_for_file("http://timestamp.sectigo.com/qualified", "nonexistent")
.err()
.unwrap()
.downcast_ref::<std::io::Error>()
.unwrap()
.kind()
== std::io::ErrorKind::NotFound
);
}
#[test]
fn timestamp_for_invalid_server_rejected() {
assert!(
request_timestamp_for_file("http://example.com", "Cargo.toml")
.err()
.unwrap()
.to_string()
== "http status: 403"
);
}
}