caramel_client/
network.rs

1// SPDX-License-Identifier: Apache-2.0 OR MIT
2// Copyright 2020 Modio AB
3
4//! Network handling API for a Caramel Client.
5
6mod hexsum;
7
8use curl::easy::Easy;
9use log::{debug, error, info, warn};
10use rand::prelude::*;
11use std::path::Path;
12
13use crate::CcError;
14
15impl From<curl::Error> for CcError {
16    fn from(error: curl::Error) -> Self {
17        let desc = error.description();
18        let code = error.code();
19        let extra = error.extra_description();
20        error!(
21            "Error from libcurl. code='{}', description='{}', extra_description='{}'",
22            code,
23            desc,
24            extra.unwrap_or("")
25        );
26        CcError::LibCurl
27    }
28}
29
30/// Enumeration reflecting the current state of this CSR.
31///
32/// Pending: data has been posted to the server, but there is no signed certificate to fetch.
33/// Rejected: Server has rejected our certificate, thus our key and csr are invalid and we should regenerate them.
34/// Downloaded: We got a certificate from the server that can be used.
35#[derive(Debug, PartialEq)]
36pub enum CertState {
37    Pending,
38    Rejected,
39    NotFound,
40    Downloaded(Vec<u8>),
41}
42
43/// Struct for Curl replies.
44struct CurlReply {
45    status_code: u32,
46    data: Vec<u8>,
47}
48
49/// Inner function that uses the curl api enr errors for `fetch_root_cert`.
50/// * `url` is a complete url.
51/// * `content` is where the resulting data will be saved.
52/// Result is a status code and the same `content` that got passed in.
53///
54/// # Errors
55/// * Passes all `curl::Error` through.
56fn curl_fetch_root_cert(url: &str, mut data: Vec<u8>) -> Result<CurlReply, curl::Error> {
57    let mut handle = Easy::new();
58    handle.url(&url)?;
59    handle.ssl_verify_host(true)?;
60    handle.ssl_verify_peer(true)?;
61    handle.ssl_min_max_version(
62        curl::easy::SslVersion::Tlsv11,
63        curl::easy::SslVersion::Tlsv13,
64    )?;
65
66    // Start a new block scope here, that allows it to access our buffer `content` exclusive or
67    // not, and then we can once more use it after the block scope.
68    // Lifetimes are fun, but this basically means that even if curl sends our buffers into a
69    // thread or similar, the compiler can track it and know that once we're out of this block,
70    // it's safe to access it again.
71    // At least that's how I'm sort of currently understanding how this works.
72    {
73        let mut transfer = handle.transfer();
74        transfer.write_function(|from_server| {
75            data.extend_from_slice(from_server);
76            Ok(from_server.len())
77        })?;
78        transfer.perform()?;
79    }
80    let status_code = handle.response_code()?;
81    debug!("GET {}, status={}", url, status_code);
82    Ok(CurlReply { status_code, data })
83}
84
85/// Fetch the CA certificate from the server.
86///
87/// Will fail if the server is not valid against our default CA-store.
88///
89/// # Errors
90/// * `CcError::Network` for most HTTP status-codes that we do not know what to do with.
91/// * `CcError::LibCurl` for curl internal errors. (DNS, timeout, etc.)
92/// * `CcError::CaNotFound` this server has no CA file and it is probably not running caramel.
93pub fn fetch_root_cert(server: &str) -> Result<Vec<u8>, CcError> {
94    // 1. Connect to server.
95    // 2. Verify that TLS checks are _enabled_.
96    // 3. Fail if not using _public_ (ie, LetsEncrypt or other public PKI infra) certificate for this
97    //    server.
98    // 4. Download the cert, return it.
99    let url = format!("https://{}/root.crt", server);
100    debug!("Fetching CA certificate from '{}'", server);
101
102    // Certificates are usually around 2100-2300 bytes
103    // A 4k allocation should be good for this
104    let content = Vec::<u8>::with_capacity(4096);
105
106    let res = curl_fetch_root_cert(&url, content)?;
107    match res.status_code {
108        200 => Ok(res.data),
109        404 => Err(CcError::CaNotFound),
110        _ => Err(CcError::Network),
111    }
112}
113
114/// Creates a curl handle, attempting connections to the server using both public PKI keys and if
115/// that fails, the local  `ca_cert` from the path.
116/// Returns either a handle, or the last connection error from curl.
117///
118/// # Errors
119/// * Passes all `curl::Error` through.
120fn curl_get_handle(server: &str, ca_cert: &Path) -> Result<Easy, curl::Error> {
121    // First we start by getting https://{server}/
122    // Then, if that succeeds, we are done and return the handle
123    // If that _fails_ because fex. SSL certificate failure, we add the `ca_cert` to the SSL
124    // connection path, and try again.
125    // If that succeeds, we return success.
126    // Otherwise, fail hard as we cannot continue.
127    let url = format!("https://{}/", server);
128    let mut handle = Easy::new();
129    handle.ssl_verify_host(true)?;
130    handle.ssl_verify_peer(true)?;
131    handle.ssl_min_max_version(
132        curl::easy::SslVersion::Tlsv11,
133        curl::easy::SslVersion::Tlsv13,
134    )?;
135    handle.url(&url)?;
136    debug!("Probing: '{}' using default TLS settings", &server);
137    match handle.perform() {
138        Ok(_) => return Ok(handle),
139        Err(e) => debug!("Failed to connect with default TLS settings.\n {}", e),
140    };
141    // Force a re-connect on the next run
142    handle.fresh_connect(true)?;
143    handle.cainfo(ca_cert)?;
144
145    debug!(
146        "Probing '{}' using '{:?}' as CA certificate",
147        &server, ca_cert
148    );
149    match handle.perform() {
150        Ok(_) => Ok(handle),
151        Err(e) => {
152            error!(
153                "Failed to connect to server '{}' with {:?} as CA certificate.\n {}",
154                &server, ca_cert, e
155            );
156            Err(e)
157        }
158    }
159}
160
161/// Internal function that downloads the certificate.
162/// Using `handle` and assumes that our setup is complete.
163///
164/// Result is the status code and a vector of data.
165///
166/// # Errors
167/// * Passes all `curl::Error` through.
168fn curl_get_crt(handle: &mut Easy, url: &str) -> Result<CurlReply, curl::Error> {
169    // Certificates are usually around 2100-2300 bytes
170    // A 4k allocation should be good for this.
171    let mut data = Vec::<u8>::with_capacity(4096);
172
173    handle.url(&url)?;
174    handle.post(false)?;
175    // Start a new block scope here, that allows it to access our buffer `content` exclusive or
176    // not, and then we can once more use it after the block scope.
177    {
178        let mut transfer = handle.transfer();
179        // See https://docs.rs/curl/0.4.33/curl/easy/struct.Easy.html#method.write_function
180        transfer.write_function(|from_server| {
181            data.extend_from_slice(from_server);
182            Ok(from_server.len())
183        })?;
184
185        transfer.perform()?;
186    }
187    let status_code = handle.response_code()?;
188    debug!("GET {}, status={}", url, status_code);
189    Ok(CurlReply { status_code, data })
190}
191
192/// Internal function that is responsible for consuming `CurlReply` (Status code and data) into
193/// useful error statuses, log lines and other data we may require.
194///
195/// # Errors
196/// * `CcError::Rejected` when CSR was rejected by server.
197/// * `CcError::Network`  when failed to fetch from server.
198fn inner_get_crt(url: &str, res: CurlReply) -> Result<CertState, CcError> {
199    match res.status_code {
200        200 => Ok(CertState::Downloaded(res.data)),
201        202 | 304 => Ok(CertState::Pending),
202        404 => Ok(CertState::NotFound),
203        403 => {
204            warn!(
205                "Rejected CSR from server when fetching '{}':\n {:?}",
206                url, res.data
207            );
208            Ok(CertState::Rejected)
209        }
210        _ => {
211            error!(
212                "Error from server when fetching '{}':\n {:?}",
213                url, res.data
214            );
215            Err(CcError::Network)
216        }
217    }
218}
219
220/// Get crt _only_ attempts to fetch the certificate, and only attempts to do so once.
221///
222/// # Ok
223/// * `CertState::NotFound`   Means that you need to POST this CSR first.
224/// * `CertState::Downloaded<Vec<u8>>` Contains the fresh certificate.
225/// * `CertState::Pending`    Means we need to wait for unknown time for the server to sign our CSR.
226/// * `CertState::Rejected`   The server has rejected our CSR, and we may need to re-generate both our Key and CSR.
227///
228/// # Errors
229/// * `CcError::Network` for most HTTP status-codes that we do not know what to do with.
230/// * `CcError::LibCurl` for curl internal errors. (DNS, timeout, etc.).
231#[allow(dead_code)]
232pub fn get_crt(server: &str, ca_cert: &Path, csr_data: &[u8]) -> Result<CertState, CcError> {
233    let hexname = hexsum::sha256hex(csr_data);
234    let url = format!("https://{}/{}", server, hexname);
235    info!("Fetching certificate from '{}'", server);
236    let mut handle = curl_get_handle(&server, &ca_cert)?;
237    let get_res = curl_get_crt(&mut handle, &url)?;
238    inner_get_crt(&url, get_res)
239}
240
241/// Internal function that posts a CSR to the url.
242/// Returns status code `CurlReply`.
243///
244/// # Errors
245/// * Passes all `curl::Error` through.
246fn curl_post_csr(
247    handle: &mut Easy,
248    url: &str,
249    mut csr_data: &[u8],
250) -> Result<CurlReply, curl::Error> {
251    use std::io::Read;
252    handle.url(&url)?;
253    handle.post(true)?;
254    handle.post_field_size(csr_data.len() as u64)?;
255
256    let mut data = Vec::new();
257    // Start a scope here. Since the `transfer` is created inside this scope, and then transfer
258    // gets the closure which posts the data, and after this block, `transfer` is no more.
259    // For the compiler, that means that `csr_data` is no longer accessed outside this block, and
260    // the lifetime is thus managed.
261    {
262        let mut transfer = handle.transfer();
263        transfer.read_function(|to_server| {
264            // "as_slice" means that we can use the Reader protocol on a vector
265            // https://doc.rust-lang.org/std/io/trait.Read.html
266            let len = csr_data.read(to_server).unwrap_or(0);
267            Ok(len)
268        })?;
269
270        // See https://docs.rs/curl/0.4.33/curl/easy/struct.Easy.html#method.write_function
271        transfer.write_function(|from_server| {
272            data.extend_from_slice(from_server);
273            Ok(from_server.len())
274        })?;
275
276        transfer.perform()?;
277    }
278    let status_code = handle.response_code()?;
279    debug!("POST {}, status={}", url, status_code);
280    Ok(CurlReply { status_code, data })
281}
282
283/// Internal function to handle replies from a curl POST CSR transaction.
284/// It is responsible for decoding status codes and messages into useful Error and Result states.
285///
286/// # Errors
287/// * `CcError::NetworkPost`  for error during Post, with reason.
288/// * `CcError::Network`      for unknown Network Error.
289fn inner_post_csr(url: &str, res: &CurlReply) -> Result<CertState, CcError> {
290    // The server will return HTTP Bad Request in the following _known_ situations:
291    // 1. POST of CSR to an URL that does not match the CSR.
292    //    Ie, if the sha256 and the data do not match.
293    // 2. Subject of CSR does not match Subject of the Server's CA
294    //     (Posting a CSR to a different server)
295    // 3. Posting the same CSR twice.
296    // However, other than looking at the string output in the error message,
297    // there is no way for us to know which of those errors we got.
298    //
299    // Other errors:
300    // 1. Posting a too large file  => HTTP 413, RequestEntityTooLarge
301    // 2. Posting without passing a Content-Length header, => HTTP 411, Length Required
302    //
303    match res.status_code {
304        200 | 202 => Ok(CertState::Pending),
305        400 | 411 | 413 => {
306            error!("Error during POST of CSR to '{}': \n{:?}", url, res.data);
307            // from_utf8_lossy converts bytes and replaces unknown data with "safe" utf8 code.
308            // It is slow and may cause copies, but we aren't doing that very often.
309            let msg = String::from_utf8_lossy(&res.data).to_string();
310            Err(CcError::NetworkPost(msg))
311        }
312        _ => {
313            error!("Unknown error POST of CSR to '{}': \n{:?}", url, res.data);
314            Err(CcError::Network)
315        }
316    }
317}
318
319/// Assuming that a certificate file does not exist on the `server`, post `csr_data` to a name
320/// calculated by the contents of `csr_data`.
321///
322/// # Errors
323/// * `CcError::LibCurl` for various curl internal errors (DNS, timeout, typoed hostname, etc).
324/// * `CcError::Network` for various status codes from the CA server.
325#[allow(dead_code)]
326pub fn post_csr(server: &str, ca_cert: &Path, csr_data: &[u8]) -> Result<CertState, CcError> {
327    let hexname = hexsum::sha256hex(csr_data);
328    let url = format!("https://{}/{}", server, hexname);
329
330    let mut handle = curl_get_handle(&server, &ca_cert)?;
331
332    info!("Posting CSR to '{}'", server);
333    let post_res = curl_post_csr(&mut handle, &url, csr_data)?;
334    inner_post_csr(&url, &post_res)
335}
336
337/// Calculate an exponential backoff.
338fn calculate_backoff(count: usize) -> std::time::Duration {
339    use std::cmp::{max, min};
340    use std::convert::TryInto;
341    use std::time::Duration;
342    // Note, this could be improved by adding a jitter to it
343    // https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
344
345    const MAX: Duration = Duration::from_secs(23);
346    const BASE: Duration = Duration::from_millis(870);
347    const TWO: u32 = 2;
348
349    // Note that 2^0 = 1
350    let count = max(0, count);
351    // If count overflows into a u32, attempt it's a big number.
352    let attempt: u32 = count.try_into().unwrap_or(100);
353    let duration: u32 = TWO.saturating_pow(attempt);
354
355    let delay: Duration = BASE * duration;
356    let bounded_delay = min(MAX, delay);
357    // Add jitter, select a point at random, [+15%, -15%] of the bounded delay
358    let mut generator = rand::thread_rng();
359    let between_0_and_1: f64 = generator.gen();
360    bounded_delay.mul_f64(1.0 + 0.3 * (between_0_and_1 - 0.5))
361}
362
363/// Tries to ensure we can get a certificate.
364///
365/// 1. A get attempt is made to the server, if succesful, early exit.
366/// 2. If not found, POST it to the server.
367/// 3. If POST was succesful, iterate forever.
368/// 4  Attempt to download and return the certificate.
369/// 5. If all attempts fail (no signed certificate exists) error out.
370///
371/// # Errors
372/// * `CcError::LibCurl` for various curl internal errors (dns, timeout, typoed hostname, etc).
373/// * `CcError::Network` for various status codes from the CA server.
374pub fn post_and_get_crt(
375    server: &str,
376    ca_cert: &Path,
377    csr_data: &[u8],
378) -> Result<CertState, CcError> {
379    use std::thread::sleep;
380
381    let hexname = hexsum::sha256hex(csr_data);
382    let url = format!("https://{}/{}", server, hexname);
383
384    let mut handle = curl_get_handle(&server, &ca_cert)?;
385
386    let mut attempt = 0;
387    loop {
388        attempt += 1;
389        let get_res = curl_get_crt(&mut handle, &url)?;
390        match inner_get_crt(&url, get_res) {
391            // Pending, We sleep for a bit and try again
392            Ok(CertState::Pending) => {
393                let delay = calculate_backoff(attempt);
394                info!("Request pending. Sleeping for {:?}", delay);
395                sleep(delay);
396            }
397            // Certificate not found? Attempt to upload it.
398            Ok(CertState::NotFound) => {
399                info!("CSR not found on server, posting to server '{}'", &server);
400                let post_res = curl_post_csr(&mut handle, &url, csr_data)?;
401                let _discard_post_status = inner_post_csr(&url, &post_res)?;
402            }
403            // all other Ok states ( Rejected, Downloaded, etc..  are passed out of this function
404            Ok(c) => break Ok(c),
405            Err(e) => break Err(e),
406        }
407    }
408}
409
410#[cfg(test)]
411mod tests {
412    use super::*;
413    use std::time::Duration;
414
415    const BIG_DUR: Duration = Duration::from_secs(60);
416    const SMALL_DUR: Duration = Duration::from_millis(500);
417
418    #[must_use]
419    pub fn convert_string_to_vec8(text: &str) -> Vec<u8> {
420        text.as_bytes().to_vec()
421    }
422
423    #[test]
424    fn test_backoff_zero() {
425        let zero = calculate_backoff(0);
426        assert!(zero < BIG_DUR);
427        assert!(zero > SMALL_DUR);
428    }
429
430    #[test]
431    fn test_backoff_one() {
432        let one = calculate_backoff(1);
433        assert!(one < BIG_DUR);
434        assert!(one > SMALL_DUR);
435    }
436
437    #[test]
438    fn test_backoff_increasing() {
439        const LIMIT_FOR_INCREMENT: Duration = Duration::from_secs(15);
440        let mut previous = calculate_backoff(0);
441        let mut count = 1;
442        while previous < LIMIT_FOR_INCREMENT {
443            let current = calculate_backoff(count);
444
445            assert!(current >= previous);
446            count += 1;
447            previous = current;
448        }
449    }
450
451    #[test]
452    fn test_backoff_large_values() {
453        let thousand = calculate_backoff(1000);
454        assert!(thousand < BIG_DUR);
455        assert!(thousand > SMALL_DUR);
456    }
457
458    #[test]
459    fn test_backoff_bignum() {
460        // number is larger than u32, make sure wrap logic works
461        let bignum = calculate_backoff(8_589_934_592);
462        assert!(bignum < BIG_DUR);
463        assert!(bignum > SMALL_DUR);
464    }
465
466    fn make_reply(status_code: u32, msg: &str) -> CurlReply {
467        let data = msg.as_bytes().to_vec();
468        CurlReply { status_code, data }
469    }
470
471    #[test]
472    fn test_post_csr_200_ok() {
473        let reply = make_reply(200, "");
474
475        let res = inner_post_csr("", &reply);
476        assert_eq!(Some(CertState::Pending), res.ok());
477    }
478    #[test]
479    fn test_post_csr_202_not_modified() {
480        let reply = make_reply(202, "");
481
482        let res = inner_post_csr("", &reply);
483        assert_eq!(Some(CertState::Pending), res.ok());
484    }
485
486    #[test]
487    fn test_post_csr_error_missing_header() {
488        let reply = make_reply(411, "Length required");
489        match inner_post_csr("", &reply) {
490            Err(CcError::NetworkPost(_)) => (),
491            _ => panic!("We should get a Post error"),
492        }
493    }
494
495    #[test]
496    fn test_post_csr_error_too_large() {
497        let reply = make_reply(413, "Too large 100kb > 12kb");
498
499        match inner_post_csr("", &reply) {
500            Err(CcError::NetworkPost(_)) => (),
501            _ => panic!("We should get a Post error"),
502        }
503    }
504
505    #[test]
506    fn test_post_csr_error() {
507        let err_msg = r#"{"status":400,"title":"Bad Request","detail":"Bad subject: (('ST', '\u00d6sterg\u00f6tland'),) do not match (('O', 'ModioAB'),)"}"#;
508        let reply = make_reply(400, err_msg);
509        match inner_post_csr("", &reply) {
510            Err(CcError::NetworkPost(_)) => (),
511            _ => panic!("We should get a Post error"),
512        }
513    }
514
515    #[test]
516    fn test_post_csr_unknown() {
517        let message = "Cannot connect to database";
518        let reply = make_reply(500, message);
519        let res = inner_post_csr("", &reply);
520        assert_eq!(Some(CcError::Network), res.err());
521    }
522
523    #[test]
524    fn test_get_crt_ok() {
525        let reply = make_reply(200, "");
526        let res = inner_get_crt("", reply);
527        assert_eq!(
528            Some(CertState::Downloaded(convert_string_to_vec8(""))),
529            res.ok()
530        );
531    }
532
533    #[test]
534    fn test_get_crt_pending() {
535        let reply = make_reply(202, "XXXXXXXXXXX");
536        let res = inner_get_crt("", reply);
537        // Should get an OK / Pending on 202
538        assert_eq!(Some(CertState::Pending), res.ok());
539    }
540    #[test]
541    fn test_get_crt_rejected() {
542        let reply = make_reply(403, "Forbidden");
543        let res = inner_get_crt("", reply);
544        // Should get an OK / Rejected on status 403
545        assert_eq!(Some(CertState::Rejected), res.ok());
546    }
547
548    #[test]
549    fn test_get_crt_not_posted() {
550        let reply = make_reply(404, "Not found");
551        let res = inner_get_crt("", reply);
552        // Should get an OK /  NotFound on 404
553        assert_eq!(Some(CertState::NotFound), res.ok());
554    }
555    #[test]
556    fn test_get_crt_error() {
557        let reply = make_reply(500, "Cannot connect to database");
558        let res = inner_get_crt("", reply);
559        // We should get a misc error
560        assert_eq!(Some(CcError::Network), res.err());
561    }
562}
563
564/// Tests that run against "live" data. These tests may randomly fail due to network services being down.
565#[cfg(test)]
566mod integration {
567    use super::{fetch_root_cert, get_crt, CcError, CertState, Path};
568
569    #[test]
570    fn get_cacert_from_log_ca() {
571        // ca.log.modio.se runs on a publicly signed PKI
572        let res = fetch_root_cert("ca.log.modio.se");
573        assert!(res.is_ok());
574    }
575
576    #[test]
577    fn get_cacert_from_ca_modio() {
578        // ca.modio.se runs on a self-signed PKI
579        let res = fetch_root_cert("ca.modio.se");
580        if res.is_ok() {
581            panic!("Should not succeed due to being signed by others");
582        } else if res.err() == Some(CcError::CaNotFound) {
583            panic!("Should not get 404 from this server.");
584        } else {
585            println!("Correct, should be a TLS connection error.");
586        }
587    }
588
589    #[test]
590    fn get_cacert_from_www_modio() {
591        // www.modio.se does not run a caramel server.
592        let res = fetch_root_cert("www.modio.se");
593        if res.err() == Some(CcError::CaNotFound) {
594            println!("Should 404 from a web server");
595        } else {
596            panic!("Wrong return from www.modio.se");
597        }
598    }
599
600    #[test]
601    fn get_crt_from_ca_modio() {
602        // This is a well-known test-certificate CSR that is valid for 'ca.modio.se'
603        // It is expected to be able to download this.
604        let fffbeec0ffee_csr: &str = "-----BEGIN CERTIFICATE REQUEST-----
605MIICvDCCAaQCAQAwdzELMAkGA1UEBhMCU0UxFzAVBgNVBAgMDsOWc3RlcmfDtnRs
606YW5kMRMwEQYDVQQHDApMaW5rw7ZwaW5nMREwDwYDVQQKDAhNb2RpbyBBQjEQMA4G
607A1UECwwHQ2FyYW1lbDEVMBMGA1UEAwwMZmZmYmVlYzBmZmVlMIIBIjANBgkqhkiG
6089w0BAQEFAAOCAQ8AMIIBCgKCAQEAx0XvZX2qZn0oijLw2YptgP2dgPOXiV74LWYT
6094LLtQwTzgLE+3sHt9Hrk/nBtZtTTYqDGpKdOEEbnx/SV5E4QiGiAPR03LUKVprhD
610v3/uCz7GnzJLjBT6H5JaV0xi7zMYOdSqkJfi2nG0cShqD7PkXym1WODDPfRjAZ1c
611g1pjeGH0dfGuKe7bQlO2i9gsC/x1J7nWDdS/E8kffkDWamsWzb/a2iuHALp3IKnJ
612xc+IxmhdTCGzAqTEcasYERpUSPjTZ5O0ky0rIqS/97pT8TZjJ4jFLd7OEXv6hXK+
6132TOhZEGbmXLlOiXqRzVN+AoRPcBwLNE5MdVOxuoO+20jBMSgnQIDAQABoAAwDQYJ
614KoZIhvcNAQELBQADggEBAC+KY6lE8+cLTfKj9260om7atPcS8qQiywOeWNzyhp9F
615Ov7vWNCoh89vCiD4VWPRj7fPGiyB4oIY3M+cXUD3zW8Gi3IbwdnUoyrN9MzGALzQ
6166zBLcxUIEt6TgQLbLNBCjqNEy4gV9qmn/XmN+J8r0orRt66S9rxYjxhIKLkuQ9xa
617LixKAxaIJ58bLH0W3/+dBDTeugt2zR+bJrJXbf6n4A+wFqJnhn8uGH2dkRxhxGK8
618L4CRL0Y1CrLO2Rl/ukqN9Fvdpy3RVrjQQ4jERVzc8n+QaKtrPcJsVX9wP0IYLqPO
619aq69O+gq+AO+jX+8xQHnSIp6pxocIxaufeSaXCgVysM=
620-----END CERTIFICATE REQUEST-----
621";
622        let res = get_crt(
623            "ca.modio.se",
624            Path::new("certs/ca.modio.se.cacert"),
625            fffbeec0ffee_csr.as_bytes(),
626        );
627        match res {
628            Ok(CertState::Downloaded(_)) => println!("Is a valid csr, should have valid crt"),
629            _ => panic!("Failure for unknown reason"),
630        }
631    }
632}