freetsa/
lib.rs

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