Skip to main content

alexa_verifier/
sync.rs

1use crate::{
2    constants::*,
3    error::{log_error, VerificationError},
4    normalize,
5};
6use failure::{bail, Error, ResultExt};
7use std::{collections::HashMap, path::Path, sync::Mutex};
8use time::Duration;
9use url::{Host, Url};
10use x509_parser::objects::Nid;
11
12/// Exposes verify method and caches new certificates synchronously on the first request
13pub struct RequestVerifier {
14    cert_cache: Mutex<HashMap<String, Vec<u8>>>,
15}
16
17/// ```rust
18/// impl Default for RequestVerifier {
19///     fn default() -> Self {
20///         RequestVerifier {
21///             cert_cache: Mutex::new(HashMap::new()),
22///         }
23///     }
24/// }
25/// ```
26impl Default for RequestVerifier {
27    fn default() -> Self {
28        RequestVerifier {
29            cert_cache: Mutex::new(HashMap::new()),
30        }
31    }
32}
33
34impl RequestVerifier {
35    /// Create default instance with an empty cache
36    pub fn new() -> Self {
37        RequestVerifier::default()
38    }
39
40    /// Verify that the request came from Alexa.  
41    ///
42    /// - `SignatureCertChainUrl` and `Signature` are headers of the request
43    ///
44    /// - Pass the entire body of the request for signature verification
45    ///
46    /// - Timestamp comes from the body, `{ "request" : { "timestamp": "" } }`. If deserialized using [alexa_sdk](https://github.com/tarkah/alexa_rust) then timestamp can be taken from `alexa_sdk::Request.body.timestamp`
47    ///
48    /// - A tolerance value in milliseconds can be passed to verify the request was received within that tolerance (default is `150_000`)
49    pub fn verify(
50        &self,
51        signature_cert_chain_url: &str,
52        signature: &str,
53        body: &[u8],
54        timestamp: &str,
55        timestamp_tolerance_millis: Option<u64>,
56    ) -> Result<(), Error> {
57        if let Err(e) = self.retrieve_and_validate_cert(signature_cert_chain_url, signature, body) {
58            log_error(e)?;
59        };
60
61        if let Err(e) = self.validate_timestamp(timestamp, timestamp_tolerance_millis) {
62            log_error(e)?;
63        };
64
65        Ok(())
66    }
67
68    fn retrieve_and_validate_cert(
69        &self,
70        signature_cert_chain_url: &str,
71        signature: &str,
72        body: &[u8],
73    ) -> Result<(), Error> {
74        // First, validate cert url
75        self.validate_cert_url(&signature_cert_chain_url)?;
76
77        // Look for certificate in cache, if not, download using validated url
78        let mut not_exists = false;
79        if !self
80            .cert_cache
81            .lock()
82            .unwrap()
83            .contains_key(&signature_cert_chain_url.to_string())
84        {
85            not_exists = true;
86            self.retrieve_cert(&signature_cert_chain_url)
87                .context(VerificationError::RetrieveCert)?;
88        }
89
90        // Get certificate from cache (shouldn't fail), convert from pem to der,
91        // then parse as x509
92        let cert_cache = self.cert_cache.lock().unwrap();
93        let pem_bytes = cert_cache
94            .get(&signature_cert_chain_url.to_string())
95            .ok_or(VerificationError::MissingCertCache)?;
96        let (_, pem) =
97            x509_parser::pem::pem_to_der(pem_bytes).map_err(|_| VerificationError::PemParse)?;
98        drop(cert_cache);
99        let certificate = pem.parse_x509().map_err(|_| VerificationError::CertParse)?;
100
101        // Make sure cert is not expired
102        let not_before = certificate.tbs_certificate.validity.not_before;
103        let not_after = certificate.tbs_certificate.validity.not_after;
104        let now_utc = time::now_utc();
105        if now_utc < not_before || now_utc > not_after {
106            bail!(VerificationError::ExpiredCert)
107        }
108
109        // Make sure domain is in SAN extension
110        // Only need to validate first time cert is downloaded
111        if not_exists {
112            let mut sans: Vec<&str> = Vec::new();
113            for ext in &certificate.tbs_certificate.extensions {
114                if ext.oid == x509_parser::objects::nid2obj(&Nid::SubjectAltName).unwrap() {
115                    let (_, ber) = der_parser::parse_der(&ext.value)
116                        .map_err(|_| VerificationError::CertExtParse)?;
117                    for b in ber.into_iter() {
118                        if let der_parser::ber::BerObjectContent::Unknown(_, i) = b.content {
119                            sans.push(
120                                std::str::from_utf8(i).context(VerificationError::SanExtension)?,
121                            )
122                        } else {
123                            bail!(VerificationError::SanExtension)
124                        }
125                    }
126                }
127            }
128            if !sans.contains(&CERT_CHAIN_DOMAIN) {
129                bail!(VerificationError::DomainNotInSan)
130            }
131        }
132
133        // Get primary key for signature verification
134        let pkey = certificate
135            .tbs_certificate
136            .subject_pki
137            .subject_public_key
138            .data;
139
140        // Parses the public key and verifies signature is a valid signature of message using it.
141        self.validate_request_body(signature, body, pkey)?;
142
143        Ok(())
144    }
145
146    fn retrieve_cert(&self, signature_cert_chain_url: &str) -> Result<(), Error> {
147        // Get cert using validated SignatureCertChainUrl
148        let mut resp = reqwest::blocking::get(signature_cert_chain_url)?;
149        let mut buf: Vec<u8> = vec![];
150        resp.copy_to(&mut buf)?;
151
152        // Add to cert cache
153        let _ = self
154            .cert_cache
155            .lock()
156            .unwrap()
157            .insert(signature_cert_chain_url.to_string(), buf);
158
159        Ok(())
160    }
161
162    fn validate_cert_url(&self, signature_cert_chain_url: &str) -> Result<(), Error> {
163        let parsed_url = Url::parse(signature_cert_chain_url)?;
164
165        let scheme = parsed_url.scheme();
166        if scheme != CERT_CHAIN_URL_SCHEME {
167            bail!(VerificationError::UrlScheme {
168                scheme: scheme.to_string()
169            })
170        }
171
172        if let Some(hostname) = parsed_url.host() {
173            match hostname {
174                Host::Domain(hostname) => {
175                    if hostname.to_lowercase() != CERT_CHAIN_URL_HOSTNAME {
176                        bail!(VerificationError::UrlHostname {
177                            hostname: hostname.to_string()
178                        });
179                    }
180                }
181                Host::Ipv4(ip) => bail!(VerificationError::UrlHostname {
182                    hostname: format!("{}", ip)
183                }),
184                Host::Ipv6(ip) => bail!(VerificationError::UrlHostname {
185                    hostname: format!("{}", ip)
186                }),
187            }
188        } else {
189            bail!(VerificationError::UrlHostname {
190                hostname: "".to_string()
191            })
192        }
193
194        let path = Path::new(parsed_url.path());
195        let normalized_path = normalize::normalize_path(&path);
196        if !normalized_path.starts_with(CERT_CHAIN_URL_STARTPATH) {
197            bail!(VerificationError::UrlPath {
198                path: format!("{}", normalized_path.display())
199            })
200        }
201
202        if let Some(port) = parsed_url.port() {
203            if port != CERT_CHAIN_URL_PORT {
204                bail!(VerificationError::UrlPort { port })
205            }
206        }
207
208        Ok(())
209    }
210
211    fn validate_request_body(
212        &self,
213        signature: &str,
214        body: &[u8],
215        pkey_bytes: &[u8],
216    ) -> Result<(), Error> {
217        let decoded_signature = base64::decode(&signature)?;
218
219        let pkey = ring::signature::UnparsedPublicKey::new(
220            &ring::signature::RSA_PKCS1_2048_8192_SHA1_FOR_LEGACY_USE_ONLY,
221            pkey_bytes,
222        );
223
224        pkey.verify(body, &decoded_signature)?;
225
226        Ok(())
227    }
228
229    fn validate_timestamp(
230        &self,
231        timestamp: &str,
232        timestamp_tolerance_millis: Option<u64>,
233    ) -> Result<(), Error> {
234        // If no tolerance is provided, use DEFAULT
235        let tolerance_millis = {
236            if let Some(t) = timestamp_tolerance_millis {
237                Duration::milliseconds(t as i64)
238            } else {
239                Duration::milliseconds(DEFAULT_TIMESTAMP_TOLERANCE_IN_MILLIS)
240            }
241        };
242
243        // Make sure tolerance is not higher than max allowed by Alexa
244        if tolerance_millis > Duration::milliseconds(MAX_TIMESTAMP_TOLERANCE_IN_MILLIS) {
245            bail!(VerificationError::TimestampMax {
246                millis: tolerance_millis.num_milliseconds()
247            });
248        }
249
250        // Timestamp is in ISO 8601 format
251        let timestamp =
252            time::strptime(timestamp, "%FT%TZ").context(VerificationError::TimestampParse {
253                timestamp: timestamp.to_owned(),
254            })?;
255        let utc_now = time::now_utc();
256
257        // Ensure request received within tolerance milliseconds
258        let duration_between = utc_now - timestamp;
259        if duration_between > tolerance_millis {
260            bail!(VerificationError::Timestamp);
261        };
262
263        Ok(())
264    }
265}