1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
//! This crate provides the ability to create a new `ACME` certificate. It therefore follows the implementation details
//! specified in [RFC8555](https://tools.ietf.org/html/rfc8555).
//!
//! ## Features
//! - `acme-rs` in its current state does only support the http challenge. The port 80 must not be blocked as this tool opens a http server in order to complete the challenge <br>
//! - You have the option to generate you keypair for the certificate first before executing the client. <br>
//! - By default, acme-rs will send the request to the URL https://acme-v02.api.letsencrypt.org/directory. However, you can manually change the ACME Server URL by using the `--server` flag. Just make sure you pass in the URL pointing to the _directory_ information. The client then fetches all paths for further requests from the endpoint.
//!
//! # Usage
//! This crate currently only exposes a few methods. The main method `generate_cert_for_domain` exposes the functionality of the full working process of requesting a SSL/TLS certificate.
//! It therefore completes the following steps:
//! - Create a new account for a specialized `email` address.
//! - Create a new order with that account for a certificate over the specified `domain`.
//! - Fetch the list of available challenges from the order.
//! - Complete the http challenge by opening a webserver on port `80`.
//! - Download the certificate from the server and return it.
//!
//! The method takes a RSA keypair, the domain, the email and the ACME server url as an input.
//!
//! This method is also used by the binary cli that ships with this crate. Usage instructions for the cli and information about the project in general can be found [here](https://github.com/kariustobias/acme-rs).
//!
//! ## Example
//! ```rust
//! use acme_rs::{generate_cert_for_domain, util::{generate_rsa_keypair, save_certificates, save_keypair}};
//!
//! // create a keypair and request the certificate for it
//! let keypair = generate_rsa_keypair().expect("Error during key creation");
//! let cert_chain = generate_cert_for_domain(
//!            &keypair,
//!            "www.example.org",
//!            "https://acme-v02.api.letsencrypt.org/directory",
//!            "max@mustermann.de",
//!            false,
//!        ).expect("Error while requesting the certificate.")
//!
//! // save the certificate in two files called my_cert.crt and cert_chain.crt
//! save_certificates(cert_chain).expect("Unable to save certificate");
//! ```

use error::Error;
use log::info;
use openssl::{
    pkey::{Private, Public},
    rsa::Rsa,
};
use reqwest::blocking::Client;
use types::{Certificate, Directory};
use util::generate_rsa_key;

/// The module which encapsulates the error enumeration
/// and related code and types.
pub mod error;
/// All types concerning the ACME context. All of the types are
/// serializable for easy communication.
mod types;
/// A module that contains utility methods used in the acme-rs context. This
/// module heavily uses the `serde_json` and `openssl` libaries.
pub mod util;

const KEY_WIDTH: u32 = 2048;

/// Generates a certificate for a certain domain. This method contains the logic for communicating with
/// the server in order to authenticate for the certificate. The keypair that's passed to this method is
/// used to sign the certificate signing request (CSR).
/// # Example
/// ```rust
/// use acme_rs::{generate_cert_for_domain, util::{generate_rsa_keypair, save_certificates, save_keypair}};
///
/// // create a keypair and request the certificate for it
/// let keypair = generate_rsa_keypair().expect("Error during key creation");
/// let cert_chain = generate_cert_for_domain(
///            &keypair,
///            "www.example.org",
///            "https://acme-v02.api.letsencrypt.org/directory",
///            "max@mustermann.de",
///            false,
///        ).expect("Error while requesting the certificate.")
///
/// // save the certificate in two files called my_cert.crt and cert_chain.crt
/// save_certificates(cert_chain).expect("Unable to save certificate");
/// ```
pub fn generate_cert_for_domain<T: AsRef<str>>(
    keypair_for_cert: &(Rsa<Private>, Rsa<Public>),
    domain: T,
    server: T,
    email: T,
    verbose: bool,
) -> Result<Certificate, Error> {
    // this keypair is used for authentificating the requests, but does not matter afterwards
    let keypair = generate_rsa_key()?;
    // create a new client (passed through to each step to make use of the keep-alive function)
    let client = Client::new();

    // fetch the directory infos an create a new account
    let dir_infos = Directory::fetch_dir(&client, server.as_ref())?;
    let new_acc = dir_infos.create_account(&client, &keypair, email.as_ref())?;
    if verbose {
        info!("Created account: {:#?}", new_acc);
    }

    // create a new order
    let order =
        new_acc.create_new_order(&client, &dir_infos.new_order, &keypair, domain.as_ref())?;
    if verbose {
        info!(
            "Opened new order for domain {}: {:#?}",
            domain.as_ref(),
            &order
        );
    }

    // fetch the auth challenges
    let challenge = order.fetch_auth_challenges(&client, &new_acc.account_location, &keypair)?;
    if verbose {
        info!(
            "Got the following authorization challenges: {:#?}",
            &challenge
        );
    }

    // complete the challenge and save the nonce that's needed for further authentification
    let new_nonce =
        challenge.complete_http_challenge(&client, &new_acc.account_location, &keypair)?;
    if verbose {
        info!("Succesfully completed the http challenge");
    }

    // finalize the order to retrieve location of the final cert
    let updated_order = order.finalize_order(
        &client,
        &new_acc.account_location,
        new_nonce,
        &keypair,
        keypair_for_cert,
        domain.as_ref(),
    )?;

    // download the certificate
    let cert_chain =
        updated_order.download_certificate(&client, &new_acc.account_location, &keypair)?;
    if verbose {
        info!("Received the following certificate chain: {}", cert_chain);
    }

    Ok(cert_chain)
}