tsp_http_client/lib.rs
1//! A simple HTTP client for requesting timestamps from a timestamp authority (TSA) using the [RFC 3161](https://www.rfc-editor.org/rfc/rfc3161.html) standard.
2//!
3//! # Examples
4//!
5//! The following code can be used, if you already have a SHA digest of the data you want to timestamp:
6//!
7//! ```rust
8//! use tsp_http_client::request_timestamp_for_digest;
9//! # use std::fs::File;
10//! # use std::io::prelude::*;
11//!
12//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
13//! // The URI of a timestamp authority (TSA) that supports RFC 3161 timestamps.
14//! let tsa_uri = "http://timestamp.digicert.com";
15//!
16//! // The SHA-256 digest of the data to be timestamped (can also be different SHA lengths like SHA-512).
17//! let digest = "00e3261a6e0d79c329445acd540fb2b07187a0dcf6017065c8814010283ac67f";
18//!
19//! // Request a timestamp for the given digest from the TSA (retrieving a TimeStampResponse object).
20//! let timestamp = request_timestamp_for_digest(tsa_uri, digest)?;
21//!
22//! // The content of the timestamp response can be written to a file then for example.
23//! File::create("/tmp/timestamp-response.tsr")?.write_all(×tamp.as_der_encoded())?;
24//!
25//! // Or the date and time of the timestamp can be accessed.
26//! println!("Timestamped date and time: {}", timestamp.datetime()?);
27//! # Ok(())
28//! # }
29//! ```
30//!
31//! Alternatively, the crate can calculate the digest on the content of a file:
32//!
33//! ```rust
34//! use tsp_http_client::request_timestamp_for_file;
35//! # use std::fs::File;
36//! # use std::io::prelude::*;
37//!
38//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
39//! // The URI of a timestamp authority (TSA) that supports RFC 3161 timestamps.
40//! let tsa_uri = "http://timestamp.digicert.com";
41//!
42//! // The file that should be timestamped.
43//! let filename = "README.md";
44//!
45//! // Request a timestamp for the given digest from the TSA (retrieving a TimeStampResponse object).
46//! let timestamp = request_timestamp_for_file(tsa_uri, filename)?;
47//!
48//! // The content of the timestamp response can be written to a file then for example.
49//! File::create("/tmp/timestamp-response.tsr")?.write_all(×tamp.as_der_encoded())?;
50//!
51//! // Or the date and time of the timestamp can be accessed.
52//! println!("Timestamped date and time: {}", timestamp.datetime()?);
53//! # Ok(())
54//! # }
55//! ```
56//!
57//! # Verification with OpenSSL
58//! Signature verification is not (yet) included in this crate. You can, however, verify the timestamp response using
59//! OpenSSL if you wrote its DER encoding into a file, as shown in the example above.
60//!
61//! ```bash
62//! openssl ts -verify -digest 00e3261a6e0d79c329445acd540fb2b07187a0dcf6017065c8814010283ac67f -in timestamp-response.tsr -CAfile tsa-cert.pem
63//! ```
64//! The `tsa-cert.pem` file must contain the full certificate chain of the timestamp authority (TSA) that issued the
65//! timestamp.
66
67mod tsp;
68
69use sha2::{self, Digest};
70use std::{fs::File, io::Read};
71use tsp::TimeStampRequest;
72pub use tsp::TimeStampResponse;
73
74/// Specific error values of the TSP HTTP client.
75#[derive(Debug, PartialEq)]
76pub enum Error {
77 /// The provided digest is none of SHA-224, SHA-256, SHA-384, or SHA-512.
78 InvalidDigest,
79
80 /// The timestamp request was not accepted by the server.
81 RequestNotAccepted(Option<String>),
82
83 /// The response from the server is not as expected according to the RFC 3161 standard.
84 InvalidServerResponse,
85
86 /// The timestamped digest does not match the provided digest.
87 DigestMismatch,
88}
89
90impl std::error::Error for Error {}
91
92impl std::fmt::Display for Error {
93 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94 match self {
95 Error::InvalidDigest => write!(
96 f,
97 "The provided digest is none of SHA-224, SHA-256, SHA-384, or SHA-512"
98 ),
99 Error::RequestNotAccepted(details) => {
100 // If details are provided, add them to the generic error message; otherwise, use an empty string.
101 let details = details
102 .clone()
103 .map_or(String::from(""), |s| format!(": {}", s));
104 write!(
105 f,
106 "Timestamp request was not accepted by the server{}",
107 details
108 )
109 }
110 Error::InvalidServerResponse => write!(
111 f,
112 "The response from the server is not as expected according to the RFC 3161 standard."
113 ),
114 Error::DigestMismatch => write!(
115 f,
116 "The timestamped digest does not match the provided digest"
117 ),
118 }
119 }
120}
121
122/// Requests a timestamp for the given digest from the specified URI of a timestamp authority (TSA).
123///
124/// * `tsa_uri`: The URI of the timestamp authority.
125/// * `digest`: The SHA-224, SHA-256, SHA-384, or SHA-512 digest of the data to be timestamped, represented as a hexadecimal string.
126pub fn request_timestamp_for_digest(
127 tsa_uri: &str,
128 digest: &str,
129) -> Result<TimeStampResponse, Box<dyn std::error::Error>> {
130 // Create a timestamp request for the given digest.
131 let data = hex::decode(digest).or(Err(Error::InvalidDigest))?;
132 request_timestamp(tsa_uri, data)
133}
134
135/// Requests a timestamp for the given file from the specified URI of a timestamp authority (TSA).
136///
137/// A SHA-256 digest is calculated on the file content and the timestamp is then requested for this digest.
138///
139/// * `tsa_uri`: The URI of the timestamp authority.
140/// * `filename`: The filename (including relative or absolute path) for which the timestamp should be requested.
141pub fn request_timestamp_for_file(
142 tsa_uri: &str,
143 filename: &str,
144) -> Result<TimeStampResponse, Box<dyn std::error::Error>> {
145 let mut file = File::open(filename)?;
146 let mut file_content = vec![];
147 file.read_to_end(&mut file_content)?;
148
149 let digest = sha2::Sha256::digest(file_content);
150 request_timestamp(tsa_uri, digest.to_vec())
151}
152
153/// Internal helper function that does the actual requesting of a timestamp based on a digest.
154///
155/// It contains the common code for the two external functions to request a timestamp for a digest or for a file.
156///
157/// * `tsa_uri`: The URI of the timestamp authority.
158/// * `digest`: The SHA-224, SHA-256, SHA-384, or SHA-512 digest of the data to be timestamped, represented as an array of bytes.
159fn request_timestamp(
160 tsa_uri: &str,
161 digest: Vec<u8>,
162) -> Result<TimeStampResponse, Box<dyn std::error::Error>> {
163 let timestamp_request = TimeStampRequest::new(digest)?;
164
165 let body = ureq::post(tsa_uri)
166 .header("Content-Type", "application/timestamp-query")
167 .send(timestamp_request.to_der()?)?
168 .body_mut()
169 .read_to_vec()?;
170
171 let timestamp = TimeStampResponse::new(body);
172 timestamp.verify(×tamp_request)?;
173
174 Ok(timestamp)
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use cmpv2::status::PkiStatus;
181 use der::Decode;
182
183 #[test]
184 fn timestamp_request_for_file_successful() {
185 // request the timestamp and expect a success response
186 let filename = "Cargo.toml";
187 let response =
188 request_timestamp_for_file("http://timestamp.digicert.com", filename).unwrap();
189
190 // the response should be a valid x509 timestamp message
191 let x509_response = x509_tsp::TimeStampResp::from_der(response.as_der_encoded()).unwrap();
192 assert_eq!(x509_response.status.status, PkiStatus::Accepted);
193
194 // the received date should be todays date
195 assert_eq!(
196 response.datetime().unwrap().date_naive(),
197 chrono::Utc::now().date_naive()
198 );
199 }
200
201 #[test]
202 fn timestamp_for_nonexistent_file_rejected() {
203 assert!(
204 request_timestamp_for_file("http://timestamp.sectigo.com/qualified", "nonexistent")
205 .err()
206 .unwrap()
207 .downcast_ref::<std::io::Error>()
208 .unwrap()
209 .kind()
210 == std::io::ErrorKind::NotFound
211 );
212 }
213
214 #[test]
215 fn timestamp_for_invalid_server_rejected() {
216 assert!(
217 request_timestamp_for_file("http://example.com", "Cargo.toml")
218 .err()
219 .unwrap()
220 .to_string()
221 == "http status: 403"
222 );
223 }
224}