Skip to main content

isideload_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, OID_CONTENT_TYPE_TST_INFO, PkiStatus, TimeStampReq, TimeStampResp,
11            TstInfo,
12        },
13        rfc5652::{OID_ID_SIGNED_DATA, SignedData},
14    },
15    aws_lc_rs::rand::SecureRandom,
16    bcder::{
17        Integer, OctetString,
18        decode::{Constructed, DecodeError, IntoSource, Source},
19        encode::Values,
20    },
21    reqwest::IntoUrl,
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        match self.signed_data()? {
130            Some(signed_data) => {
131                if signed_data.content_info.content_type == OID_CONTENT_TYPE_TST_INFO {
132                    if let Some(content) = signed_data.content_info.content {
133                        Ok(Some(Constructed::decode(
134                            content.to_bytes(),
135                            bcder::Mode::Der,
136                            TstInfo::take_from,
137                        )?))
138                    } else {
139                        Ok(None)
140                    }
141                } else {
142                    Ok(None)
143                }
144            }
145            _ => Ok(None),
146        }
147    }
148}
149
150impl From<TimeStampResp> for TimeStampResponse {
151    fn from(resp: TimeStampResp) -> Self {
152        Self(resp)
153    }
154}
155
156/// Send a [TimeStampReq] to a server via HTTP.
157pub fn time_stamp_request_http(
158    url: impl IntoUrl,
159    request: &TimeStampReq,
160) -> Result<TimeStampResponse, TimeStampError> {
161    let client = reqwest::blocking::Client::new();
162
163    let mut body = Vec::<u8>::new();
164    request
165        .encode_ref()
166        .write_encoded(bcder::Mode::Der, &mut body)?;
167
168    let response = client
169        .post(url)
170        .header("Content-Type", HTTP_CONTENT_TYPE_REQUEST)
171        .body(body)
172        .send()?;
173
174    if response.status().is_success()
175        && response.headers().get("Content-Type")
176            == Some(&reqwest::header::HeaderValue::from_static(
177                HTTP_CONTENT_TYPE_RESPONSE,
178            ))
179    {
180        let response_bytes = response.bytes()?;
181
182        let res = TimeStampResponse(Constructed::decode(
183            response_bytes.as_ref(),
184            bcder::Mode::Der,
185            TimeStampResp::take_from,
186        )?);
187
188        // Verify nonce was reflected, if present.
189        if res.is_success() {
190            if let Some(tst_info) = res.tst_info()? {
191                if tst_info.nonce != request.nonce {
192                    return Err(TimeStampError::NonceMismatch);
193                }
194            }
195        }
196
197        Ok(res)
198    } else {
199        Err(TimeStampError::Http("bad HTTP response"))
200    }
201}
202
203/// Send a Time-Stamp request for a given message to an HTTP URL.
204///
205/// This is a wrapper around [time_stamp_request_http] that constructs the low-level
206/// ASN.1 request object with reasonable defaults.
207pub fn time_stamp_message_http(
208    url: impl IntoUrl,
209    message: &[u8],
210    digest_algorithm: DigestAlgorithm,
211) -> Result<TimeStampResponse, TimeStampError> {
212    let mut h = digest_algorithm.digester();
213    h.update(message);
214    let digest = h.finish();
215
216    let mut random = [0u8; 8];
217    aws_lc_rs::rand::SystemRandom::new()
218        .fill(&mut random)
219        .map_err(|_| TimeStampError::Random)?;
220
221    let request = TimeStampReq {
222        version: Integer::from(1),
223        message_imprint: MessageImprint {
224            hash_algorithm: digest_algorithm.into(),
225            hashed_message: OctetString::new(bytes::Bytes::copy_from_slice(digest.as_ref())),
226        },
227        req_policy: None,
228        nonce: Some(Integer::from(u64::from_le_bytes(random))),
229        cert_req: Some(true),
230        extensions: None,
231    };
232
233    time_stamp_request_http(url, &request)
234}
235
236#[cfg(test)]
237mod test {
238    use super::*;
239
240    const DIGICERT_TIMESTAMP_URL: &str = "http://timestamp.digicert.com";
241
242    #[test]
243    fn verify_static() {
244        let signed_data =
245            crate::SignedData::parse_ber(include_bytes!("testdata/tsp-signed-data.der")).unwrap();
246
247        for signer in signed_data.signers() {
248            signer
249                .verify_message_digest_with_signed_data(&signed_data)
250                .unwrap();
251            signer
252                .verify_signature_with_signed_data(&signed_data)
253                .unwrap();
254        }
255    }
256
257    #[test]
258    fn simple_request() {
259        let message = b"hello, world";
260
261        let res = time_stamp_message_http(DIGICERT_TIMESTAMP_URL, message, DigestAlgorithm::Sha256)
262            .unwrap();
263
264        let signed_data = res.signed_data().unwrap().unwrap();
265        assert_eq!(
266            signed_data.content_info.content_type,
267            OID_CONTENT_TYPE_TST_INFO
268        );
269        let tst_info = res.tst_info().unwrap().unwrap();
270        assert_eq!(tst_info.version, Integer::from(1));
271
272        let parsed = crate::SignedData::try_from(&signed_data).unwrap();
273        for signer in parsed.signers() {
274            signer
275                .verify_message_digest_with_signed_data(&parsed)
276                .unwrap();
277            signer.verify_signature_with_signed_data(&parsed).unwrap();
278        }
279    }
280}