ctclient_async/
google_log_list.rs

1//! Downloading of log list from Google.
2
3use crate::Error;
4use crate::internal::new_http_client;
5
6use serde::Deserialize;
7use std::collections::HashMap;
8
9#[derive(Debug, Deserialize, Clone)]
10struct ResponseJSON {
11    operators: Vec<OperatorJSON>,
12}
13
14#[derive(Debug, Deserialize, Clone)]
15struct OperatorJSON {
16    name: String,
17    email: Vec<String>,
18    logs: Vec<LogJson>,
19}
20
21#[derive(Debug, Deserialize, Clone)]
22struct LogJson {
23    key: String,
24    log_id: String,
25    mmd: u64,
26    url: String,
27    state: HashMap<String, serde_json::Value>,
28    description: String,
29}
30
31/// A downloaded log list.
32#[derive(Debug, Clone)]
33pub struct LogList {
34    pub map_id_to_log: HashMap<Vec<u8>, Log>,
35}
36
37/// A log in [`LogList`].
38#[derive(Debug, Clone)]
39pub struct Log {
40    pub pub_key: Vec<u8>,
41    pub base_url: String,
42    pub state: LogState,
43    pub description: String,
44}
45
46#[derive(Debug, PartialEq, Eq, Copy, Clone)]
47pub enum LogState {
48    Pending,
49    Qualified,
50    Usable,
51    Readonly,
52    Retired,
53    Rejected,
54}
55
56impl LogList {
57    /// Download the log list at runtime from [`https://www.gstatic.com/ct/log_list/v3/log_list.json`](https://www.gstatic.com/ct/log_list/v3/log_list.json).
58    pub async fn get() -> Result<LogList, Error> {
59        LogList::get_with_url("https://www.gstatic.com/ct/log_list/v3/log_list.json").await
60    }
61
62    /// Download the log list at runtime.
63    pub async fn get_with_url(url: &str) -> Result<LogList, Error> {
64        let client = new_http_client()?;
65        let json: ResponseJSON = client
66            .get(url)
67            .send()
68            .await
69            .map_err(Error::NetIO)?
70            .json()
71            .await
72            .map_err(|e| Error::MalformedResponseBody(format!("{}", e)))?;
73        let mut hm: HashMap<Vec<u8>, Log> =
74            HashMap::with_capacity(json.operators.iter().map(|x| x.logs.len()).sum());
75        fn b64_dec_err(e: base64::DecodeError) -> Error {
76            Error::MalformedResponseBody(format!("Unable to decode base64: {}", e))
77        }
78        for op in json.operators.iter() {
79            for log in op.logs.iter() {
80                let log_id = base64::decode(&log.log_id).map_err(b64_dec_err)?;
81                let pub_key = base64::decode(&log.key).map_err(b64_dec_err)?;
82                let base_url = log.url.to_owned();
83                if hm.contains_key(&log_id) {
84                    return Err(Error::MalformedResponseBody(
85                        "Multiple logs returned with the same id.".to_owned(),
86                    ));
87                }
88                let state_keys: Vec<&str> = log.state.keys().map(|x| &x[..]).collect();
89                use LogState::*;
90                let log_state = match &state_keys[..] {
91                    ["pending"] => Pending,
92                    ["qualified"] => Qualified,
93                    ["usable"] => Usable,
94                    ["readonly"] => Readonly,
95                    ["retired"] => Retired,
96                    ["rejected"] => Rejected,
97                    _ => {
98                        return Err(Error::MalformedResponseBody(format!(
99                            "Invalid log state object: {:?}",
100                            &log.state
101                        )));
102                    }
103                };
104                hm.insert(
105                    log_id,
106                    Log {
107                        pub_key,
108                        base_url,
109                        state: log_state,
110                        description: log.description.clone(),
111                    },
112                );
113            }
114        }
115
116        Ok(LogList { map_id_to_log: hm })
117    }
118
119    /// Lookup a [`Log`] by its 32-byte `log_id`.
120    pub fn find_by_id<'a>(&'a self, id: &[u8]) -> Option<&'a Log> {
121        self.map_id_to_log.get(id)
122    }
123}
124
125#[cfg(test)]
126mod tests {
127    use super::*;
128    #[tokio::test]
129    async fn test() {
130        let ll = LogList::get().await.unwrap();
131        let nb_logs = ll.map_id_to_log.len();
132        assert!(nb_logs > 0);
133        assert_eq!(
134            ll.find_by_id(&base64::decode("sh4FzIuizYogTodm+Su5iiUgZ2va+nDnsklTLe+LkF4=").unwrap())
135                .unwrap()
136                .base_url,
137            "https://ct.googleapis.com/logs/argon2020/"
138        );
139    }
140}