1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
use simple_asn1::{ASN1Block, BigUint, OID};
use thiserror::Error;

#[derive(Debug, Error)]
pub enum TimestampApiError {
    #[error("http client failure: {}", _0)]
    HttpClient(#[source] reqwest::Error),
    #[error("api rejected request: {}", _0)]
    Remote(#[source] reqwest::Error),
    #[error("failed to encore timestamp request: {}", _0)]
    RequestEncoding(#[from] simple_asn1::ASN1EncodeErr),
    #[error("failure receiving response: {}", _0)]
    Response(#[source] reqwest::Error),
}

#[cfg(feature = "file")]
#[derive(Debug, Error)]
pub enum TimestampFileError {
    #[error("failed to read file: {}", _0)]
    FileIo(#[source] std::io::Error),
    #[error("{}", _0)]
    Api(#[from] TimestampApiError),
}

#[cfg(feature = "file")]
pub async fn timestamp_file(
    path: impl AsRef<std::path::Path>,
) -> Result<TimestampResponse, TimestampFileError> {
    use sha2::{Digest, Sha512};
    let file = tokio::fs::read(path)
        .await
        .map_err(TimestampFileError::FileIo)?;
    let mut hasher = Sha512::new();
    hasher.update(file);
    let hash = hasher.finalize();

    Ok(timestamp_hash(hash.to_vec()).await?)
}

pub async fn timestamp_hash(hash: Vec<u8>) -> Result<TimestampResponse, TimestampApiError> {
    let sha512_oid: Vec<BigUint> = [2u16, 16, 840, 1, 101, 3, 4, 2, 3]
        .into_iter()
        .map(Into::into)
        .collect::<Vec<_>>();
    let req = ASN1Block::Sequence(
        3,
        vec![
            ASN1Block::Integer(1, 1.into()),
            ASN1Block::Sequence(
                2,
                vec![
                    ASN1Block::Sequence(
                        2,
                        vec![
                            ASN1Block::ObjectIdentifier(1, OID::new(sha512_oid)),
                            ASN1Block::Null(1),
                        ],
                    ),
                    ASN1Block::OctetString(1, hash),
                ],
            ),
            ASN1Block::Boolean(1, true),
        ],
    );
    let req = simple_asn1::to_der(&req)?;
    let client = reqwest::ClientBuilder::new()
        .build()
        .map_err(TimestampApiError::HttpClient)?;
    let response = client
        .post("https://freetsa.org/tsr")
        .header("content-type", "application/timestamp-query")
        .body(req.clone())
        .send()
        .await
        .map_err(TimestampApiError::Remote)?;
    let payload = response
        .bytes()
        .await
        .map_err(TimestampApiError::Response)?;
    Ok(TimestampResponse {
        query: req,
        reply: payload.into(),
    })
}

pub struct TimestampResponse {
    pub query: Vec<u8>,
    pub reply: Vec<u8>,
}

pub mod prelude {
    pub use super::timestamp_hash;
    pub use super::TimestampResponse;

    #[cfg(feature = "file")]
    pub use super::timestamp_file;
}