rust_cfzt_validator/
api.rs

1use crate::{
2    errors::{UnpackError, UnpackResult},
3    keys::{self, AccessKey},
4    unpack, StdResult,
5};
6
7use std::collections::HashMap;
8
9use serde_json::Value;
10use ureq;
11
12pub(crate) fn extract_latest_key_id(payload: &Value) -> UnpackResult<String> {
13    // get string value at payload["public_cert"]["kid"]
14    unpack::as_string(unpack::as_object_get_key(
15        unpack::as_object_get_key(payload, "public_cert")?,
16        "kid",
17    )?)
18    .cloned()
19}
20
21pub(crate) fn extract_current_keys(payload: &Value) -> UnpackResult<keys::AccessKeyMap> {
22    // get array value at payload["keys"]
23    let cert_objs = unpack::as_array(unpack::as_object_get_key(payload, "keys")?)?;
24
25    if cert_objs.len() == 0 {
26        return Err(UnpackError::empty_container("array"));
27    }
28
29    let mut map: keys::AccessKeyMap = HashMap::new();
30
31    for val in cert_objs {
32        let obj = unpack::as_object(val)?;
33
34        // will need refactoring if/when cf aupports new key types
35        let access_key = keys::RsaAccessKey::new(
36            unpack::as_string(unpack::get_key(obj, "kid")?)?,
37            unpack::as_string(unpack::get_key(obj, "alg")?)?,
38            unpack::as_string(unpack::get_key(obj, "use")?)?,
39            unpack::as_string(unpack::get_key(obj, "e")?)?,
40            unpack::as_string(unpack::get_key(obj, "n")?)?,
41        );
42
43        map.insert(access_key.get_key_id(), Box::new(access_key));
44    }
45
46    Ok(map)
47}
48
49fn get_team_key_uri(team_name: &str) -> String {
50    format!("https://{team_name}.cloudflareaccess.com/cdn-cgi/access/certs")
51}
52
53fn get_json_payload(uri: &str, agent: &ureq::Agent) -> Result<Value, ureq::Error> {
54    let payload = agent.get(uri).call()?.body_mut().read_json::<Value>()?;
55
56    Ok(payload)
57}
58
59fn get_team_keys(team_name: &str, agent: &ureq::Agent) -> StdResult<(String, keys::AccessKeyMap)> {
60    let uri = get_team_key_uri(team_name);
61    let payload = get_json_payload(&uri, agent)?;
62    Ok((
63        extract_latest_key_id(&payload)?,
64        extract_current_keys(&payload)?,
65    ))
66}
67
68/// Represents a set of trusted signing keys for a specific CFZT Team
69pub struct TeamKeys {
70    pub team_name: String,
71    pub latest_key_id: String,
72    pub keys: keys::AccessKeyMap,
73}
74
75impl TeamKeys {
76    fn new(team_name: &str, latest_key_id: &str, keys: keys::AccessKeyMap) -> Self {
77        TeamKeys {
78            team_name: team_name.to_string(),
79            latest_key_id: latest_key_id.to_string(),
80            keys,
81        }
82    }
83
84    /// Attempts to load signing keys for a given team using a HTTP request.
85    pub fn from_team_name(team_name: &str, agent: &ureq::Agent) -> StdResult<Self> {
86        let (latest_key_id, keys) = get_team_keys(team_name, agent)?;
87        Ok(TeamKeys::new(team_name, &latest_key_id, keys))
88    }
89
90    // Attempts to load signing keys from a given serde_json::Value struct.
91    pub fn from_json(team_name: &str, json_val: Value) -> StdResult<Self> {
92        let latest_key_id = extract_latest_key_id(&json_val)?;
93        let keys = extract_current_keys(&json_val)?;
94        Ok(TeamKeys::new(team_name, &latest_key_id, keys))
95    }
96
97    // Attempts to load signing keys from a given JSON string slice.
98    pub fn from_str(team_name: &str, json_str: &str) -> StdResult<Self> {
99        let json_val = serde_json::from_str(json_str)?;
100        TeamKeys::from_json(team_name, json_val)
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use jsonwebtoken::jwk;
108    use serde_json;
109
110    const DUMMY_PAYLOAD: &str = include_str!("../test_data/dummy_signing_keys.json");
111
112    const EXPECTED_LATEST_KEY_ID: &str = "foo";
113    const EXPECTED_LATEST_KEY_CONTENT: &str = "bar";
114    const EXPECTED_ADDITIONAL_KEY_ID: &str = "baz";
115    const EXPECTED_ADDITIONAL_KEY_CONTENT: &str = "bin";
116
117    const TEST_TEAM: &str = "example";
118
119    fn get_payload_value() -> Value {
120        serde_json::from_str(DUMMY_PAYLOAD).unwrap()
121    }
122
123    fn assert_access_key_content(key: &Box<dyn AccessKey>, expect: &str) {
124        match key.get_jwk().algorithm {
125            jwk::AlgorithmParameters::RSA(params) => {
126                assert_eq!(params.e, expect);
127                assert_eq!(params.n, expect);
128            }
129            _ => panic!(
130                "incorrect AlgorithmParameters for kid '{}'",
131                key.get_key_id()
132            ),
133        }
134    }
135
136    #[test]
137    fn test_unpack_latest_key_id() {
138        let payload = get_payload_value();
139        let actual_key_id = extract_latest_key_id(&payload);
140
141        assert!(actual_key_id.is_ok());
142        assert_eq!(actual_key_id.unwrap(), EXPECTED_LATEST_KEY_ID);
143    }
144
145    #[test]
146    fn test_unpack_current_keys() {
147        let payload = get_payload_value();
148        let actual_keys = extract_current_keys(&payload).unwrap();
149
150        assert!(actual_keys.contains_key(EXPECTED_LATEST_KEY_ID));
151        assert!(actual_keys.contains_key(EXPECTED_ADDITIONAL_KEY_ID));
152
153        let latest_key = actual_keys.get(EXPECTED_LATEST_KEY_ID).unwrap();
154        let additional_key = actual_keys.get(EXPECTED_ADDITIONAL_KEY_ID).unwrap();
155
156        assert_eq!(latest_key.get_key_id(), EXPECTED_LATEST_KEY_ID);
157        assert_eq!(additional_key.get_key_id(), EXPECTED_ADDITIONAL_KEY_ID);
158
159        assert_access_key_content(latest_key, EXPECTED_LATEST_KEY_CONTENT);
160        assert_access_key_content(additional_key, EXPECTED_ADDITIONAL_KEY_CONTENT);
161    }
162
163    #[test]
164    fn test_get_team_keys() {
165        let agent = ureq::agent();
166        let result = get_team_keys(TEST_TEAM, &agent);
167        assert!(result.is_ok());
168    }
169}