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}