cryptographic_message_syntax/
time_stamp_protocol.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Time-Stamp Protocol (TSP) / RFC 3161 client.
6
7use {
8    crate::asn1::{
9        rfc3161::{
10            MessageImprint, PkiStatus, TimeStampReq, TimeStampResp, TstInfo,
11            OID_CONTENT_TYPE_TST_INFO,
12        },
13        rfc5652::{SignedData, OID_ID_SIGNED_DATA},
14    },
15    bcder::{
16        decode::{Constructed, DecodeError, IntoSource, Source},
17        encode::Values,
18        Integer, OctetString,
19    },
20    reqwest::IntoUrl,
21    ring::rand::SecureRandom,
22    std::{convert::Infallible, ops::Deref},
23    x509_certificate::DigestAlgorithm,
24};
25
26pub const HTTP_CONTENT_TYPE_REQUEST: &str = "application/timestamp-query";
27
28pub const HTTP_CONTENT_TYPE_RESPONSE: &str = "application/timestamp-reply";
29
30#[derive(Debug)]
31pub enum TimeStampError {
32    Io(std::io::Error),
33    Reqwest(reqwest::Error),
34    Asn1Decode(DecodeError<Infallible>),
35    Http(&'static str),
36    Random,
37    NonceMismatch,
38    Unsuccessful(TimeStampResp),
39    BadResponse,
40}
41
42impl std::fmt::Display for TimeStampError {
43    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        match self {
45            Self::Io(e) => f.write_fmt(format_args!("I/O error: {}", e)),
46            Self::Reqwest(e) => f.write_fmt(format_args!("HTTP error: {}", e)),
47            Self::Asn1Decode(e) => f.write_fmt(format_args!("ASN.1 decode error: {}", e)),
48            Self::Http(msg) => f.write_str(msg),
49            Self::Random => f.write_str("error generating random nonce"),
50            Self::NonceMismatch => f.write_str("nonce mismatch"),
51            Self::Unsuccessful(r) => f.write_fmt(format_args!(
52                "unsuccessful Time-Stamp Protocol response: {:?}: {:?}",
53                r.status.status, r.status.status_string
54            )),
55            Self::BadResponse => f.write_str("bad server response"),
56        }
57    }
58}
59
60impl std::error::Error for TimeStampError {}
61
62impl From<std::io::Error> for TimeStampError {
63    fn from(e: std::io::Error) -> Self {
64        Self::Io(e)
65    }
66}
67
68impl From<reqwest::Error> for TimeStampError {
69    fn from(e: reqwest::Error) -> Self {
70        Self::Reqwest(e)
71    }
72}
73
74impl From<DecodeError<Infallible>> for TimeStampError {
75    fn from(e: DecodeError<Infallible>) -> Self {
76        Self::Asn1Decode(e)
77    }
78}
79
80/// High-level interface to [TimeStampResp].
81///
82/// This type provides a high-level interface to the low-level ASN.1 response
83/// type from a Time-Stamp Protocol request.
84pub struct TimeStampResponse(TimeStampResp);
85
86impl Deref for TimeStampResponse {
87    type Target = TimeStampResp;
88
89    fn deref(&self) -> &Self::Target {
90        &self.0
91    }
92}
93
94impl TimeStampResponse {
95    /// Whether the time stamp request was successful.
96    pub fn is_success(&self) -> bool {
97        matches!(
98            self.0.status.status,
99            PkiStatus::Granted | PkiStatus::GrantedWithMods
100        )
101    }
102
103    /// Obtain the size of the time-stamp token data.
104    pub fn token_content_size(&self) -> Option<usize> {
105        self.0
106            .time_stamp_token
107            .as_ref()
108            .map(|token| token.content.len())
109    }
110
111    /// Decode the `SignedData` value in the response.
112    pub fn signed_data(&self) -> Result<Option<SignedData>, DecodeError<Infallible>> {
113        if let Some(token) = &self.0.time_stamp_token {
114            let source = token.content.clone();
115
116            if token.content_type == OID_ID_SIGNED_DATA {
117                Ok(Some(source.decode(SignedData::take_from)?))
118            } else {
119                Err(source
120                    .into_source()
121                    .content_err("invalid OID on signed data"))
122            }
123        } else {
124            Ok(None)
125        }
126    }
127
128    pub fn tst_info(&self) -> Result<Option<TstInfo>, DecodeError<Infallible>> {
129        if let Some(signed_data) = self.signed_data()? {
130            if signed_data.content_info.content_type == OID_CONTENT_TYPE_TST_INFO {
131                if let Some(content) = signed_data.content_info.content {
132                    Ok(Some(Constructed::decode(
133                        content.to_bytes(),
134                        bcder::Mode::Der,
135                        TstInfo::take_from,
136                    )?))
137                } else {
138                    Ok(None)
139                }
140            } else {
141                Ok(None)
142            }
143        } else {
144            Ok(None)
145        }
146    }
147}
148
149impl From<TimeStampResp> for TimeStampResponse {
150    fn from(resp: TimeStampResp) -> Self {
151        Self(resp)
152    }
153}
154
155/// Send a [TimeStampReq] to a server via HTTP.
156pub fn time_stamp_request_http(
157    url: impl IntoUrl,
158    request: &TimeStampReq,
159) -> Result<TimeStampResponse, TimeStampError> {
160    let client = reqwest::blocking::Client::new();
161
162    let mut body = Vec::<u8>::new();
163    request
164        .encode_ref()
165        .write_encoded(bcder::Mode::Der, &mut body)?;
166
167    let response = client
168        .post(url)
169        .header("Content-Type", HTTP_CONTENT_TYPE_REQUEST)
170        .body(body)
171        .send()?;
172
173    if response.status().is_success()
174        && response.headers().get("Content-Type")
175            == Some(&reqwest::header::HeaderValue::from_static(
176                HTTP_CONTENT_TYPE_RESPONSE,
177            ))
178    {
179        let response_bytes = response.bytes()?;
180
181        let res = TimeStampResponse(Constructed::decode(
182            response_bytes.as_ref(),
183            bcder::Mode::Der,
184            TimeStampResp::take_from,
185        )?);
186
187        // Verify nonce was reflected, if present.
188        if res.is_success() {
189            if let Some(tst_info) = res.tst_info()? {
190                if tst_info.nonce != request.nonce {
191                    return Err(TimeStampError::NonceMismatch);
192                }
193            }
194        }
195
196        Ok(res)
197    } else {
198        Err(TimeStampError::Http("bad HTTP response"))
199    }
200}
201
202/// Send a Time-Stamp request for a given message to an HTTP URL.
203///
204/// This is a wrapper around [time_stamp_request_http] that constructs the low-level
205/// ASN.1 request object with reasonable defaults.
206pub fn time_stamp_message_http(
207    url: impl IntoUrl,
208    message: &[u8],
209    digest_algorithm: DigestAlgorithm,
210) -> Result<TimeStampResponse, TimeStampError> {
211    let mut h = digest_algorithm.digester();
212    h.update(message);
213    let digest = h.finish();
214
215    let mut random = [0u8; 8];
216    ring::rand::SystemRandom::new()
217        .fill(&mut random)
218        .map_err(|_| TimeStampError::Random)?;
219
220    let request = TimeStampReq {
221        version: Integer::from(1),
222        message_imprint: MessageImprint {
223            hash_algorithm: digest_algorithm.into(),
224            hashed_message: OctetString::new(bytes::Bytes::copy_from_slice(digest.as_ref())),
225        },
226        req_policy: None,
227        nonce: Some(Integer::from(u64::from_le_bytes(random))),
228        cert_req: Some(true),
229        extensions: None,
230    };
231
232    time_stamp_request_http(url, &request)
233}
234
235#[cfg(test)]
236mod test {
237    use super::*;
238
239    const DIGICERT_TIMESTAMP_URL: &str = "http://timestamp.digicert.com";
240
241    #[test]
242    fn verify_static() {
243        let signed_data =
244            crate::SignedData::parse_ber(include_bytes!("testdata/tsp-signed-data.der")).unwrap();
245
246        for signer in signed_data.signers() {
247            signer
248                .verify_message_digest_with_signed_data(&signed_data)
249                .unwrap();
250            signer
251                .verify_signature_with_signed_data(&signed_data)
252                .unwrap();
253        }
254    }
255
256    #[test]
257    fn simple_request() {
258        let message = b"hello, world";
259
260        let res = time_stamp_message_http(DIGICERT_TIMESTAMP_URL, message, DigestAlgorithm::Sha256)
261            .unwrap();
262
263        let signed_data = res.signed_data().unwrap().unwrap();
264        assert_eq!(
265            signed_data.content_info.content_type,
266            OID_CONTENT_TYPE_TST_INFO
267        );
268        let tst_info = res.tst_info().unwrap().unwrap();
269        assert_eq!(tst_info.version, Integer::from(1));
270
271        let parsed = crate::SignedData::try_from(&signed_data).unwrap();
272        for signer in parsed.signers() {
273            signer
274                .verify_message_digest_with_signed_data(&parsed)
275                .unwrap();
276            signer.verify_signature_with_signed_data(&parsed).unwrap();
277        }
278    }
279}