1mod 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#[derive(Debug, PartialEq)]
36pub enum CertState {
37 Pending,
38 Rejected,
39 NotFound,
40 Downloaded(Vec<u8>),
41}
42
43struct CurlReply {
45 status_code: u32,
46 data: Vec<u8>,
47}
48
49fn 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 {
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
85pub fn fetch_root_cert(server: &str) -> Result<Vec<u8>, CcError> {
94 let url = format!("https://{}/root.crt", server);
100 debug!("Fetching CA certificate from '{}'", server);
101
102 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
114fn curl_get_handle(server: &str, ca_cert: &Path) -> Result<Easy, curl::Error> {
121 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 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
161fn curl_get_crt(handle: &mut Easy, url: &str) -> Result<CurlReply, curl::Error> {
169 let mut data = Vec::<u8>::with_capacity(4096);
172
173 handle.url(&url)?;
174 handle.post(false)?;
175 {
178 let mut transfer = handle.transfer();
179 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
192fn 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#[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
241fn 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 {
262 let mut transfer = handle.transfer();
263 transfer.read_function(|to_server| {
264 let len = csr_data.read(to_server).unwrap_or(0);
267 Ok(len)
268 })?;
269
270 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
283fn inner_post_csr(url: &str, res: &CurlReply) -> Result<CertState, CcError> {
290 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 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#[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
337fn calculate_backoff(count: usize) -> std::time::Duration {
339 use std::cmp::{max, min};
340 use std::convert::TryInto;
341 use std::time::Duration;
342 const MAX: Duration = Duration::from_secs(23);
346 const BASE: Duration = Duration::from_millis(870);
347 const TWO: u32 = 2;
348
349 let count = max(0, count);
351 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 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
363pub 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 Ok(CertState::Pending) => {
393 let delay = calculate_backoff(attempt);
394 info!("Request pending. Sleeping for {:?}", delay);
395 sleep(delay);
396 }
397 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 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 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 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 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 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 assert_eq!(Some(CcError::Network), res.err());
561 }
562}
563
564#[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 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 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 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 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}