1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181
//! # FreeTSA Client Library
use simple_asn1::{ASN1Block, BigUint, OID};
use thiserror::Error;
/// Errors that can be generated while interacting with the FreeTSA API
#[derive(Debug, Error)]
pub enum TimestampApiError {
/// HTTP client failed before API request could be made
#[error("http client failure: {}", _0)]
HttpClient(#[source] reqwest::Error),
/// FreeTSA rejected the timestamp request
#[error("api rejected request: {}", _0)]
Remote(#[source] reqwest::Error),
/// Failed to ASN.1/DER encode the timestamp request
#[error("failed to encore timestamp request: {}", _0)]
RequestEncoding(#[from] simple_asn1::ASN1EncodeErr),
/// Failed to process the FreeTSA API response
#[error("failure receiving response: {}", _0)]
Response(#[source] reqwest::Error),
}
/// Errors that can be generated while timestamping a file
///
/// *This type is available only if freetsa is built with the `"file"` feature.*
#[cfg(feature = "file")]
#[derive(Debug, Error)]
pub enum TimestampFileError {
/// I/O failure reading the file to be timestamped
#[error("failed to read file: {}", _0)]
FileIo(#[source] std::io::Error),
/// FreeTSA API failure
#[error("{}", _0)]
Api(#[from] TimestampApiError),
}
/// Timestamp a file.
///
/// This method generates a SHA512 hash of the specified file and submits it
/// to FreeTSA to be timestamped.
///
/// *This method is available only if freetsa is built with the `"file"` feature.*
///
/// __Example__
/// ```rust,no_run
/// use freetsa::prelude::*;
/// use tokio::fs::OpenOptions;
/// use tokio::io::AsyncWriteExt;
///
/// #[tokio::main]
/// async fn main() {
/// // request timestamp with automatically generated file hash
/// let TimestampResponse { reply, .. } = timestamp_file("path/to/file").await.unwrap();
/// // create file where we'll persist the timestamp reply
/// let mut reply_file = OpenOptions::new()
/// .create(true)
/// .write(true)
/// .open("example.tsr")
/// .await
/// .unwrap();
/// // write timestamp reply to file
/// reply_file.write_all(&reply).await.unwrap();
/// // ensure os has completed writing all data
/// reply_file.flush().await.unwrap();
/// }
/// ```
#[cfg(feature = "file")]
pub async fn timestamp_file(
path: impl AsRef<std::path::Path>,
) -> Result<TimestampResponse, TimestampFileError> {
use sha2::{Digest, Sha512};
let file = tokio::fs::read(path)
.await
.map_err(TimestampFileError::FileIo)?;
let mut hasher = Sha512::new();
hasher.update(file);
let hash = hasher.finalize();
Ok(timestamp_hash(hash.to_vec()).await?)
}
/// Timestamp a hash
///
/// This method takes a SHA512 hash and submits it to FreeTSA to be timestamped.
///
/// __Example__
/// ```rust,no_run
/// use freetsa::prelude::*;
/// use futures_util::TryFutureExt;
/// use tokio::try_join;
/// use tokio::fs::OpenOptions;
/// use tokio::io::AsyncWriteExt;
///
/// #[tokio::main]
/// async fn main() {
/// // generate a hash in some manner, here we use a literal as an example
/// let hash = hex_literal::hex!("401b09eab3c013d4ca54922bb802bec8fd5318192b0a75f201d8b3727429080fb337591abd3e44453b954555b7a0812e1081c39b740293f765eae731f5a65ed1").to_vec();
/// // request timestamp with pre-generated hash
/// let TimestampResponse { reply, query } = timestamp_hash(hash).await.unwrap();
/// // create file where we'll persist the timestamp query
/// let mut query_file = OpenOptions::new();
/// let query_file = query_file.create(true).write(true).open("example.tsq");
/// // create file where we'll persist the timestamp reply
/// let mut reply_file = OpenOptions::new();
/// let reply_file = reply_file.create(true).write(true).open("example.tsr");
/// // wait on all data writes
/// try_join!(
/// async move {
/// let mut query_file = query_file.await?;
/// query_file.write_all(&query).await?;
/// query_file.flush().await
/// },
/// async move {
/// let mut reply_file = reply_file.await?;
/// reply_file.write_all(&reply).await?;
/// reply_file.flush().await
/// }
/// ).unwrap();
/// }
/// ```
pub async fn timestamp_hash(hash: Vec<u8>) -> Result<TimestampResponse, TimestampApiError> {
let sha512_oid: Vec<BigUint> = [2u16, 16, 840, 1, 101, 3, 4, 2, 3]
.into_iter()
.map(Into::into)
.collect::<Vec<_>>();
let req = ASN1Block::Sequence(
3,
vec![
ASN1Block::Integer(1, 1.into()),
ASN1Block::Sequence(
2,
vec![
ASN1Block::Sequence(
2,
vec![
ASN1Block::ObjectIdentifier(1, OID::new(sha512_oid)),
ASN1Block::Null(1),
],
),
ASN1Block::OctetString(1, hash),
],
),
ASN1Block::Boolean(1, true),
],
);
let req = simple_asn1::to_der(&req)?;
let client = reqwest::ClientBuilder::new()
.build()
.map_err(TimestampApiError::HttpClient)?;
let response = client
.post("https://freetsa.org/tsr")
.header("content-type", "application/timestamp-query")
.body(req.clone())
.send()
.await
.map_err(TimestampApiError::Remote)?;
let payload = response
.bytes()
.await
.map_err(TimestampApiError::Response)?;
Ok(TimestampResponse {
query: req,
reply: payload.into(),
})
}
/// Timestamp API response
pub struct TimestampResponse {
/// Timestamp query, ASN.1/DER encoded, as sent to FreeTSA API
pub query: Vec<u8>,
/// Timestamp response, ASN.1/DER encoded, as received from FreeTSA API
pub reply: Vec<u8>,
}
pub mod prelude {
pub use super::timestamp_hash;
pub use super::TimestampResponse;
#[cfg(feature = "file")]
pub use super::timestamp_file;
}