├── .gitignore ├── Cargo.toml ├── LICENSE.txt ├── README.md └── src ├── acc ├── akey.rs └── mod.rs ├── api.rs ├── cert.rs ├── dir.rs ├── error.rs ├── jwt.rs ├── lib.rs ├── order ├── auth.rs └── mod.rs ├── persist.rs ├── req.rs ├── test └── mod.rs ├── trans.rs └── util.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | *.key 4 | *.crt 5 | Cargo.lock 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "acme-lib" 3 | description = "Library for requesting certificates from an ACME provider." 4 | license = "MIT" 5 | repository = "https://github.com/algesten/acme-lib" 6 | readme = "README.md" 7 | version = "0.9.1" 8 | authors = ["Martin Algesten "] 9 | keywords = ["letsencrypt", "acme"] 10 | categories = ["web-programming", "api-bindings"] 11 | edition = "2018" 12 | 13 | [dependencies] 14 | base64 = "0.22" 15 | jiff = "0.1.20" 16 | lazy_static = "1" 17 | log = "0.4" 18 | openssl = "0.10" 19 | serde = { version = "1", features = ["derive"] } 20 | serde_json = "1" 21 | ureq = "3.0.0-rc4" 22 | 23 | [dev-dependencies] 24 | env_logger = { version = "0.11", default-features = false } 25 | futures = "0.1.25" 26 | hyper = "0.12" 27 | regex = "1.3" 28 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | ## License (MIT) 2 | 3 | Copyright (c) 2020 Martin Algesten 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # acme-lib 2 | 3 | acme-lib is a library for accessing ACME (Automatic Certificate Management Environment) 4 | services such as [Let's Encrypt](https://letsencrypt.org/). 5 | 6 | Uses ACME v2 to issue/renew certificates. 7 | 8 | **Note:** This repo could use help with maintenance. The code is old, and somewhat embarrasing. 9 | It would be simple enough to rearchitecture this from the ground up into something better. 10 | 11 | ## Example 12 | 13 | ```rust 14 | use acme_lib::{Error, Directory, DirectoryUrl}; 15 | use acme_lib::persist::FilePersist; 16 | use acme_lib::create_p384_key; 17 | 18 | fn request_cert() -> Result<(), Error> { 19 | 20 | // Use DirectoryUrl::LetsEncrypStaging for dev/testing. 21 | let url = DirectoryUrl::LetsEncrypt; 22 | 23 | // Save/load keys and certificates to current dir. 24 | let persist = FilePersist::new("."); 25 | 26 | // Create a directory entrypoint. 27 | let dir = Directory::from_url(persist, url)?; 28 | 29 | // Reads the private account key from persistence, or 30 | // creates a new one before accessing the API to establish 31 | // that it's there. 32 | let acc = dir.account("foo@bar.com")?; 33 | 34 | // Order a new TLS certificate for a domain. 35 | let mut ord_new = acc.new_order("mydomain.io", &[])?; 36 | 37 | // If the ownership of the domain(s) have already been 38 | // authorized in a previous order, you might be able to 39 | // skip validation. The ACME API provider decides. 40 | let ord_csr = loop { 41 | // are we done? 42 | if let Some(ord_csr) = ord_new.confirm_validations() { 43 | break ord_csr; 44 | } 45 | 46 | // Get the possible authorizations (for a single domain 47 | // this will only be one element). 48 | let auths = ord_new.authorizations()?; 49 | 50 | // For HTTP, the challenge is a text file that needs to 51 | // be placed in your web server's root: 52 | // 53 | // /var/www/.well-known/acme-challenge/ 54 | // 55 | // The important thing is that it's accessible over the 56 | // web for the domain(s) you are trying to get a 57 | // certificate for: 58 | // 59 | // http://mydomain.io/.well-known/acme-challenge/ 60 | let chall = auths[0].http_challenge(); 61 | 62 | // The token is the filename. 63 | let token = chall.http_token(); 64 | let path = format!(".well-known/acme-challenge/{}", token); 65 | 66 | // The proof is the contents of the file 67 | let proof = chall.http_proof(); 68 | 69 | // Here you must do "something" to place 70 | // the file/contents in the correct place. 71 | // update_my_web_server(&path, &proof); 72 | 73 | // After the file is accessible from the web, the calls 74 | // this to tell the ACME API to start checking the 75 | // existence of the proof. 76 | // 77 | // The order at ACME will change status to either 78 | // confirm ownership of the domain, or fail due to the 79 | // not finding the proof. To see the change, we poll 80 | // the API with 5000 milliseconds wait between. 81 | chall.validate(5000)?; 82 | 83 | // Update the state against the ACME API. 84 | ord_new.refresh()?; 85 | }; 86 | 87 | // Ownership is proven. Create a private key for 88 | // the certificate. These are provided for convenience, you 89 | // can provide your own keypair instead if you want. 90 | let pkey_pri = create_p384_key(); 91 | 92 | // Submit the CSR. This causes the ACME provider to enter a 93 | // state of "processing" that must be polled until the 94 | // certificate is either issued or rejected. Again we poll 95 | // for the status change. 96 | let ord_cert = 97 | ord_csr.finalize_pkey(pkey_pri, 5000)?; 98 | 99 | // Now download the certificate. Also stores the cert in 100 | // the persistence. 101 | let cert = ord_cert.download_and_save_cert()?; 102 | 103 | Ok(()) 104 | } 105 | ``` 106 | 107 | ### Domain ownership 108 | 109 | Most website TLS certificates tries to prove ownership/control over the domain they 110 | are issued for. For ACME, this means proving you control either a web server answering 111 | HTTP requests to the domain, or the DNS server answering name lookups against the domain. 112 | 113 | To use this library, there are points in the flow where you would need to modify either 114 | the web server or DNS server before progressing to get the certificate. 115 | 116 | See [`http_challenge`] and [`dns_challenge`]. 117 | 118 | #### Multiple domains 119 | 120 | When creating a new order, it's possible to provide multiple alt-names that will also 121 | be part of the certificate. The ACME API requires you to prove ownership of each such 122 | domain. See [`authorizations`]. 123 | 124 | [`http_challenge`]: https://docs.rs/acme-lib/latest/acme_lib/order/struct.Auth.html#method.http_challenge 125 | [`dns_challenge`]: https://docs.rs/acme-lib/latest/acme_lib/order/struct.Auth.html#method.dns_challenge 126 | [`authorizations`]: https://docs.rs/acme-lib/latest/acme_lib/order/struct.NewOrder.html#method.authorizations 127 | 128 | ### Rate limits 129 | 130 | The ACME API provider Let's Encrypt uses [rate limits] to ensure the API i not being 131 | abused. It might be tempting to put the `delay_millis` really low in some of this 132 | libraries' polling calls, but balance this against the real risk of having access 133 | cut off. 134 | 135 | [rate limits]: https://letsencrypt.org/docs/rate-limits/ 136 | 137 | #### Use staging for dev! 138 | 139 | Especially take care to use the Let`s Encrypt staging environment for development 140 | where the rate limits are more relaxed. 141 | 142 | See [`DirectoryUrl::LetsEncryptStaging`]. 143 | 144 | [`DirectoryUrl::LetsEncryptStaging`]: enum.DirectoryUrl.html#variant.LetsEncryptStaging 145 | 146 | ### Implementation details 147 | 148 | The library tries to pull in as few dependencies as possible. (For now) that means using 149 | synchronous I/O and blocking cals. This doesn't rule out a futures based version later. 150 | 151 | It is written by following the 152 | [ACME draft spec 18](https://tools.ietf.org/html/draft-ietf-acme-acme-18), and relies 153 | heavily on the [openssl](https://docs.rs/openssl/) crate to make JWK/JWT and sign requests 154 | to the API. 155 | 156 | 157 | License: MIT 158 | -------------------------------------------------------------------------------- /src/acc/akey.rs: -------------------------------------------------------------------------------- 1 | use openssl::ec::EcKey; 2 | use openssl::pkey; 3 | 4 | use crate::cert::EC_GROUP_P256; 5 | use crate::Result; 6 | 7 | #[derive(Clone, Debug)] 8 | pub(crate) struct AcmeKey { 9 | private_key: EcKey, 10 | /// set once we contacted the ACME API to figure out the key id 11 | key_id: Option, 12 | } 13 | 14 | impl AcmeKey { 15 | pub(crate) fn new() -> AcmeKey { 16 | let pri_key = EcKey::generate(&*EC_GROUP_P256).expect("EcKey"); 17 | Self::from_key(pri_key) 18 | } 19 | 20 | pub(crate) fn from_pem(pem: &[u8]) -> Result { 21 | let pri_key = 22 | EcKey::private_key_from_pem(pem).map_err(|e| format!("Failed to read PEM: {}", e))?; 23 | Ok(Self::from_key(pri_key)) 24 | } 25 | 26 | fn from_key(private_key: EcKey) -> AcmeKey { 27 | AcmeKey { 28 | private_key, 29 | key_id: None, 30 | } 31 | } 32 | 33 | pub(crate) fn to_pem(&self) -> Vec { 34 | self.private_key 35 | .private_key_to_pem() 36 | .expect("private_key_to_pem") 37 | } 38 | 39 | pub(crate) fn private_key(&self) -> &EcKey { 40 | &self.private_key 41 | } 42 | 43 | pub(crate) fn key_id(&self) -> &str { 44 | self.key_id.as_ref().unwrap() 45 | } 46 | 47 | pub(crate) fn set_key_id(&mut self, kid: String) { 48 | self.key_id = Some(kid) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/acc/mod.rs: -------------------------------------------------------------------------------- 1 | // 2 | use std::sync::Arc; 3 | 4 | use crate::api::{ApiAccount, ApiDirectory, ApiIdentifier, ApiOrder, ApiRevocation}; 5 | use crate::cert::Certificate; 6 | use crate::order::{NewOrder, Order}; 7 | use crate::persist::{Persist, PersistKey, PersistKind}; 8 | use crate::req::req_expect_header; 9 | use crate::trans::Transport; 10 | use crate::util::{base64url, read_json}; 11 | use crate::Result; 12 | 13 | mod akey; 14 | 15 | pub(crate) use self::akey::AcmeKey; 16 | 17 | #[derive(Clone, Debug)] 18 | pub(crate) struct AccountInner { 19 | pub persist: P, 20 | pub transport: Transport, 21 | pub realm: String, 22 | pub api_account: ApiAccount, 23 | pub api_directory: ApiDirectory, 24 | } 25 | 26 | /// Account with an ACME provider. 27 | /// 28 | /// Accounts are created using [`Directory::account`] and consist of a contact 29 | /// email address and a private key for signing requests to the ACME API. 30 | /// 31 | /// acme-lib uses elliptic curve P-256 for accessing the account. This 32 | /// does not affect which key algorithms that can be used for the 33 | /// issued certificates. 34 | /// 35 | /// The advantage of using elliptic curve cryptography is that the signed 36 | /// requests against the ACME lib are kept small and that the public key 37 | /// can be derived from the private. 38 | /// 39 | /// [`Directory::account`]: struct.Directory.html#method.account 40 | #[derive(Clone)] 41 | pub struct Account { 42 | inner: Arc>, 43 | } 44 | 45 | impl Account

{ 46 | pub(crate) fn new( 47 | persist: P, 48 | transport: Transport, 49 | realm: &str, 50 | api_account: ApiAccount, 51 | api_directory: ApiDirectory, 52 | ) -> Self { 53 | Account { 54 | inner: Arc::new(AccountInner { 55 | persist, 56 | transport, 57 | realm: realm.to_string(), 58 | api_account, 59 | api_directory, 60 | }), 61 | } 62 | } 63 | 64 | /// Private key for this account. 65 | /// 66 | /// The key is an elliptic curve private key. 67 | pub fn acme_private_key_pem(&self) -> String { 68 | String::from_utf8(self.inner.transport.acme_key().to_pem()).expect("from_utf8") 69 | } 70 | 71 | /// Get an already issued and [downloaded] certificate. 72 | /// 73 | /// Every time a certificate is downloaded, the certificate and corresponding 74 | /// private key are persisted. This method returns an already existing certificate 75 | /// from the local storage (no API calls involved). 76 | /// 77 | /// This can form the basis for implemeting automatic renewal of 78 | /// certificates where the [valid days left] are running low. 79 | /// 80 | /// [downloaded]: order/struct.CertOrder.html#method.download_and_save_cert 81 | /// [valid days left]: struct.Certificate.html#method.valid_days_left 82 | pub fn certificate(&self, primary_name: &str) -> Result> { 83 | // details needed for persistence 84 | let realm = &self.inner.realm; 85 | let persist = &self.inner.persist; 86 | 87 | // read primary key 88 | let pk_key = PersistKey::new(realm, PersistKind::PrivateKey, primary_name); 89 | debug!("Read private key: {}", pk_key); 90 | let private_key = persist 91 | .get(&pk_key)? 92 | .and_then(|s| String::from_utf8(s).ok()); 93 | 94 | // read certificate 95 | let pk_crt = PersistKey::new(realm, PersistKind::Certificate, primary_name); 96 | debug!("Read certificate: {}", pk_crt); 97 | let certificate = persist 98 | .get(&pk_crt)? 99 | .and_then(|s| String::from_utf8(s).ok()); 100 | 101 | Ok(match (private_key, certificate) { 102 | (Some(k), Some(c)) => Some(Certificate::new(k, c)), 103 | _ => None, 104 | }) 105 | } 106 | 107 | /// Create a new order to issue a certificate for this account. 108 | /// 109 | /// Each order has a required `primary_name` (which will be set as the certificates `CN`) 110 | /// and a variable number of `alt_names`. 111 | /// 112 | /// This library doesn't constrain the number of `alt_names`, but it is limited by the ACME 113 | /// API provider. Let's Encrypt sets a max of [100 names] per certificate. 114 | /// 115 | /// Every call creates a new order with the ACME API provider, even when the domain 116 | /// names supplied are exactly the same. 117 | /// 118 | /// [100 names]: https://letsencrypt.org/docs/rate-limits/ 119 | pub fn new_order(&self, primary_name: &str, alt_names: &[&str]) -> Result> { 120 | // construct the identifiers 121 | let prim_arr = [primary_name]; 122 | let domains = prim_arr.iter().chain(alt_names); 123 | let order = ApiOrder { 124 | identifiers: domains 125 | .map(|s| ApiIdentifier { 126 | _type: "dns".into(), 127 | value: s.to_string(), 128 | }) 129 | .collect(), 130 | ..Default::default() 131 | }; 132 | 133 | let new_order_url = &self.inner.api_directory.newOrder; 134 | 135 | let res = self.inner.transport.call(new_order_url, &order)?; 136 | let order_url = req_expect_header(&res, "location")?; 137 | let api_order: ApiOrder = read_json(res)?; 138 | 139 | let order = Order::new(&self.inner, api_order, order_url); 140 | Ok(NewOrder { order }) 141 | } 142 | 143 | /// Revoke a certificate for the reason given. 144 | /// 145 | /// This calls the ACME API revoke endpoint, but does not affect the locally persisted 146 | /// certs, the revoked certificate will still be available using [`certificate`]. 147 | /// 148 | /// [`certificate`]: struct.Account.html#method.certificate 149 | pub fn revoke_certificate(&self, cert: &Certificate, reason: RevocationReason) -> Result<()> { 150 | // convert to base64url of the DER (which is not PEM). 151 | let certificate = base64url(&cert.certificate_der()); 152 | 153 | let revoc = ApiRevocation { 154 | certificate, 155 | reason: reason as usize, 156 | }; 157 | 158 | let url = &self.inner.api_directory.revokeCert; 159 | self.inner.transport.call(url, &revoc)?; 160 | 161 | Ok(()) 162 | } 163 | 164 | /// Access the underlying JSON object for debugging. 165 | pub fn api_account(&self) -> &ApiAccount { 166 | &self.inner.api_account 167 | } 168 | } 169 | 170 | /// Enumeration of reasons for revocation. 171 | /// 172 | /// The reason codes are taken from [rfc5280](https://tools.ietf.org/html/rfc5280#section-5.3.1). 173 | pub enum RevocationReason { 174 | Unspecified = 0, 175 | KeyCompromise = 1, 176 | CACompromise = 2, 177 | AffiliationChanged = 3, 178 | Superseded = 4, 179 | CessationOfOperation = 5, 180 | CertificateHold = 6, 181 | // value 7 is not used 182 | RemoveFromCRL = 8, 183 | PrivilegeWithdrawn = 9, 184 | AACompromise = 10, 185 | } 186 | 187 | #[cfg(test)] 188 | mod test { 189 | use crate::persist::*; 190 | use crate::*; 191 | 192 | #[test] 193 | fn test_create_order() -> Result<()> { 194 | let server = crate::test::with_directory_server(); 195 | let url = DirectoryUrl::Other(&server.dir_url); 196 | let persist = MemoryPersist::new(); 197 | let dir = Directory::from_url(persist, url)?; 198 | let acc = dir.account("foo@bar.com")?; 199 | let _ = acc.new_order("acmetest.example.com", &[])?; 200 | Ok(()) 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/api.rs: -------------------------------------------------------------------------------- 1 | //! Low level API JSON objects. 2 | //! 3 | //! Unstable and not to be used directly. Provided to aid debugging. 4 | #![allow(non_snake_case)] 5 | #![allow(non_camel_case_types)] 6 | 7 | use serde::{ 8 | ser::{SerializeMap, Serializer}, 9 | Deserialize, Serialize, 10 | }; 11 | 12 | /// Serializes to `""` 13 | pub struct ApiEmptyString; 14 | impl Serialize for ApiEmptyString { 15 | fn serialize(&self, serializer: S) -> Result 16 | where 17 | S: Serializer, 18 | { 19 | serializer.serialize_str("") 20 | } 21 | } 22 | 23 | /// Serializes to `{}` 24 | pub struct ApiEmptyObject; 25 | impl Serialize for ApiEmptyObject { 26 | fn serialize(&self, serializer: S) -> Result 27 | where 28 | S: Serializer, 29 | { 30 | let m = serializer.serialize_map(Some(0))?; 31 | m.end() 32 | } 33 | } 34 | 35 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] 36 | pub struct ApiProblem { 37 | #[serde(rename = "type")] 38 | pub _type: String, 39 | #[serde(skip_serializing_if = "Option::is_none")] 40 | pub detail: Option, 41 | #[serde(skip_serializing_if = "Option::is_none")] 42 | pub subproblems: Option>, 43 | } 44 | 45 | impl ApiProblem { 46 | pub fn is_bad_nonce(&self) -> bool { 47 | self._type == "badNonce" 48 | } 49 | pub fn is_jwt_verification_error(&self) -> bool { 50 | (self._type == "urn:acme:error:malformed" 51 | || self._type == "urn:ietf:params:acme:error:malformed") 52 | && self 53 | .detail 54 | .as_ref() 55 | .map(|s| s == "JWS verification error") 56 | .unwrap_or(false) 57 | } 58 | } 59 | 60 | impl std::fmt::Display for ApiProblem { 61 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 62 | if let Some(detail) = &self.detail { 63 | write!(f, "{}: {}", self._type, detail) 64 | } else { 65 | write!(f, "{}", self._type) 66 | } 67 | } 68 | } 69 | 70 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] 71 | pub struct ApiSubproblem { 72 | #[serde(rename = "type")] 73 | pub _type: String, 74 | pub detail: Option, 75 | pub identifier: Option, 76 | } 77 | 78 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] 79 | pub struct ApiDirectory { 80 | pub newNonce: String, 81 | pub newAccount: String, 82 | pub newOrder: String, 83 | #[serde(skip_serializing_if = "Option::is_none")] 84 | pub newAuthz: Option, 85 | pub revokeCert: String, 86 | pub keyChange: String, 87 | #[serde(skip_serializing_if = "Option::is_none")] 88 | pub meta: Option, 89 | } 90 | 91 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] 92 | pub struct ApiDirectoryMeta { 93 | #[serde(skip_serializing_if = "Option::is_none")] 94 | pub termsOfService: Option, 95 | #[serde(skip_serializing_if = "Option::is_none")] 96 | pub website: Option, 97 | #[serde(skip_serializing_if = "Option::is_none")] 98 | pub caaIdentities: Option>, 99 | #[serde(skip_serializing_if = "Option::is_none")] 100 | pub externalAccountRequired: Option, 101 | } 102 | 103 | impl ApiDirectoryMeta { 104 | pub fn externalAccountRequired(&self) -> bool { 105 | self.externalAccountRequired.unwrap_or(false) 106 | } 107 | } 108 | 109 | // { 110 | // "status": "valid", 111 | // "contact": [ 112 | // "mailto:cert-admin@example.com", 113 | // "mailto:admin@example.com" 114 | // ], 115 | // "termsOfServiceAgreed": true, 116 | // "orders": "https://example.com/acme/acct/evOfKhNU60wg/orders" 117 | // } 118 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] 119 | pub struct ApiAccount { 120 | #[serde(skip_serializing_if = "Option::is_none")] 121 | pub status: Option, 122 | #[serde(skip_serializing_if = "Option::is_none")] 123 | pub contact: Option>, 124 | #[serde(skip_serializing_if = "Option::is_none")] 125 | pub termsOfServiceAgreed: Option, 126 | #[serde(skip_serializing_if = "Option::is_none")] 127 | pub orders: Option, 128 | } 129 | 130 | impl ApiAccount { 131 | pub fn is_status_valid(&self) -> bool { 132 | self.status.as_ref().map(|s| s.as_ref()) == Some("valid") 133 | } 134 | pub fn is_status_deactivated(&self) -> bool { 135 | self.status.as_ref().map(|s| s.as_ref()) == Some("deactivated") 136 | } 137 | pub fn is_status_revoked(&self) -> bool { 138 | self.status.as_ref().map(|s| s.as_ref()) == Some("revoked") 139 | } 140 | pub fn termsOfServiceAgreed(&self) -> bool { 141 | self.termsOfServiceAgreed.unwrap_or(false) 142 | } 143 | } 144 | 145 | // { 146 | // "status": "pending", 147 | // "expires": "2019-01-09T08:26:43.570360537Z", 148 | // "identifiers": [ 149 | // { 150 | // "type": "dns", 151 | // "value": "acmetest.algesten.se" 152 | // } 153 | // ], 154 | // "authorizations": [ 155 | // "https://example.com/acme/authz/YTqpYUthlVfwBncUufE8IRA2TkzZkN4eYWWLMSRqcSs" 156 | // ], 157 | // "finalize": "https://example.com/acme/finalize/7738992/18234324" 158 | // } 159 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] 160 | pub struct ApiOrder { 161 | #[serde(skip_serializing_if = "Option::is_none")] 162 | pub status: Option, 163 | #[serde(skip_serializing_if = "Option::is_none")] 164 | pub expires: Option, 165 | pub identifiers: Vec, 166 | pub notBefore: Option, 167 | pub notAfter: Option, 168 | pub error: Option, 169 | pub authorizations: Option>, 170 | pub finalize: String, 171 | pub certificate: Option, 172 | } 173 | 174 | impl ApiOrder { 175 | /// As long as there are outstanding authorizations. 176 | pub fn is_status_pending(&self) -> bool { 177 | self.status.as_ref().map(|s| s.as_ref()) == Some("pending") 178 | } 179 | /// When all authorizations are finished, and we need to call 180 | /// "finalize". 181 | pub fn is_status_ready(&self) -> bool { 182 | self.status.as_ref().map(|s| s.as_ref()) == Some("ready") 183 | } 184 | /// On "finalize" the server is processing to sign CSR. 185 | pub fn is_status_processing(&self) -> bool { 186 | self.status.as_ref().map(|s| s.as_ref()) == Some("processing") 187 | } 188 | /// Once the certificate is issued and can be downloaded. 189 | pub fn is_status_valid(&self) -> bool { 190 | self.status.as_ref().map(|s| s.as_ref()) == Some("valid") 191 | } 192 | /// If the order failed and can't be used again. 193 | pub fn is_status_invalid(&self) -> bool { 194 | self.status.as_ref().map(|s| s.as_ref()) == Some("invalid") 195 | } 196 | /// Return all domains 197 | pub fn domains(&self) -> Vec<&str> { 198 | self.identifiers.iter().map(|i| i.value.as_ref()).collect() 199 | } 200 | } 201 | 202 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 203 | pub struct ApiIdentifier { 204 | #[serde(rename = "type")] 205 | pub _type: String, 206 | pub value: String, 207 | } 208 | 209 | impl ApiIdentifier { 210 | pub fn is_type_dns(&self) -> bool { 211 | self._type == "dns" 212 | } 213 | } 214 | 215 | // { 216 | // "identifier": { 217 | // "type": "dns", 218 | // "value": "acmetest.algesten.se" 219 | // }, 220 | // "status": "pending", 221 | // "expires": "2019-01-09T08:26:43Z", 222 | // "challenges": [ 223 | // { 224 | // "type": "http-01", 225 | // "status": "pending", 226 | // "url": "https://example.com/acme/challenge/YTqpYUthlVfwBncUufE8IRA2TkzZkN4eYWWLMSRqcSs/216789597", 227 | // "token": "MUi-gqeOJdRkSb_YR2eaMxQBqf6al8dgt_dOttSWb0w" 228 | // }, 229 | // { 230 | // "type": "tls-alpn-01", 231 | // "status": "pending", 232 | // "url": "https://example.com/acme/challenge/YTqpYUthlVfwBncUufE8IRA2TkzZkN4eYWWLMSRqcSs/216789598", 233 | // "token": "WCdRWkCy4THTD_j5IH4ISAzr59lFIg5wzYmKxuOJ1lU" 234 | // }, 235 | // { 236 | // "type": "dns-01", 237 | // "status": "pending", 238 | // "url": "https://example.com/acme/challenge/YTqpYUthlVfwBncUufE8IRA2TkzZkN4eYWWLMSRqcSs/216789599", 239 | // "token": "RRo2ZcXAEqxKvMH8RGcATjSK1KknLEUmauwfQ5i3gG8" 240 | // } 241 | // ] 242 | // } 243 | 244 | // on incorrect challenge, something like: 245 | // 246 | // "challenges": [ 247 | // { 248 | // "type": "dns-01", 249 | // "status": "invalid", 250 | // "error": { 251 | // "type": "urn:ietf:params:acme:error:dns", 252 | // "detail": "DNS problem: NXDOMAIN looking up TXT for _acme-challenge.martintest.foobar.com", 253 | // "status": 400 254 | // }, 255 | // "url": "https://example.com/acme/challenge/afyChhlFB8GLLmIqEnqqcXzX0Ss3GBw6oUlKAGDG6lY/221695600", 256 | // "token": "YsNqBWZnyYjDun3aUC2CkCopOaqZRrI5hp3tUjxPLQU" 257 | // }, 258 | // "Incorrect TXT record \"caOh44dp9eqXNRkd0sYrKVF8dBl0L8h8-kFpIBje-2c\" found at _acme-challenge.martintest.foobar.com 259 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 260 | pub struct ApiAuth { 261 | pub identifier: ApiIdentifier, 262 | pub status: Option, 263 | pub expires: Option, 264 | pub challenges: Vec, 265 | pub wildcard: Option, 266 | } 267 | 268 | impl ApiAuth { 269 | pub fn is_status_pending(&self) -> bool { 270 | self.status.as_ref().map(|s| s.as_ref()) == Some("pending") 271 | } 272 | pub fn is_status_valid(&self) -> bool { 273 | self.status.as_ref().map(|s| s.as_ref()) == Some("valid") 274 | } 275 | pub fn is_status_invalid(&self) -> bool { 276 | self.status.as_ref().map(|s| s.as_ref()) == Some("invalid") 277 | } 278 | pub fn is_status_deactivated(&self) -> bool { 279 | self.status.as_ref().map(|s| s.as_ref()) == Some("deactivated") 280 | } 281 | pub fn is_status_expired(&self) -> bool { 282 | self.status.as_ref().map(|s| s.as_ref()) == Some("expired") 283 | } 284 | pub fn is_status_revoked(&self) -> bool { 285 | self.status.as_ref().map(|s| s.as_ref()) == Some("revoked") 286 | } 287 | pub fn wildcard(&self) -> bool { 288 | self.wildcard.unwrap_or(false) 289 | } 290 | pub fn http_challenge(&self) -> Option<&ApiChallenge> { 291 | self.challenges.iter().find(|c| c._type == "http-01") 292 | } 293 | pub fn dns_challenge(&self) -> Option<&ApiChallenge> { 294 | self.challenges.iter().find(|c| c._type == "dns-01") 295 | } 296 | pub fn tls_alpn_challenge(&self) -> Option<&ApiChallenge> { 297 | self.challenges.iter().find(|c| c._type == "tls-alpn-01") 298 | } 299 | } 300 | 301 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 302 | pub struct ApiChallenge { 303 | pub url: String, 304 | #[serde(rename = "type")] 305 | pub _type: String, 306 | pub status: String, 307 | pub token: String, 308 | pub validated: Option, 309 | pub error: Option, 310 | } 311 | 312 | // { 313 | // "type": "http-01", 314 | // "status": "pending", 315 | // "url": "https://acme-staging-v02.api.letsencrypt.org/acme/challenge/YTqpYUthlVfwBncUufE8IRA2TkzZkN4eYWWLMSRqcSs/216789597", 316 | // "token": "MUi-gqeOJdRkSb_YR2eaMxQBqf6al8dgt_dOttSWb0w" 317 | // } 318 | impl ApiChallenge { 319 | pub fn is_status_pending(&self) -> bool { 320 | &self.status == "pending" 321 | } 322 | pub fn is_status_processing(&self) -> bool { 323 | &self.status == "processing" 324 | } 325 | pub fn is_status_valid(&self) -> bool { 326 | &self.status == "valid" 327 | } 328 | pub fn is_status_invalid(&self) -> bool { 329 | &self.status == "invalid" 330 | } 331 | } 332 | 333 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 334 | pub struct ApiFinalize { 335 | pub csr: String, 336 | } 337 | 338 | #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 339 | pub struct ApiRevocation { 340 | pub certificate: String, 341 | pub reason: usize, 342 | } 343 | 344 | #[cfg(test)] 345 | mod test { 346 | use super::*; 347 | 348 | #[test] 349 | fn test_api_empty_string() { 350 | let x = serde_json::to_string(&ApiEmptyString).unwrap(); 351 | assert_eq!("\"\"", x); 352 | } 353 | 354 | #[test] 355 | fn test_api_empty_object() { 356 | let x = serde_json::to_string(&ApiEmptyObject).unwrap(); 357 | assert_eq!("{}", x); 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /src/cert.rs: -------------------------------------------------------------------------------- 1 | use lazy_static::lazy_static; 2 | use openssl::ec::{Asn1Flag, EcGroup, EcKey}; 3 | use openssl::hash::MessageDigest; 4 | use openssl::nid::Nid; 5 | use openssl::pkey::{self, PKey}; 6 | use openssl::rsa::Rsa; 7 | use openssl::stack::Stack; 8 | use openssl::x509::extension::SubjectAlternativeName; 9 | use openssl::x509::{X509Req, X509ReqBuilder, X509}; 10 | 11 | use crate::Result; 12 | 13 | lazy_static! { 14 | pub(crate) static ref EC_GROUP_P256: EcGroup = ec_group(Nid::X9_62_PRIME256V1); 15 | pub(crate) static ref EC_GROUP_P384: EcGroup = ec_group(Nid::SECP384R1); 16 | } 17 | 18 | fn ec_group(nid: Nid) -> EcGroup { 19 | let mut g = EcGroup::from_curve_name(nid).expect("EcGroup"); 20 | // this is required for openssl 1.0.x (but not 1.1.x) 21 | g.set_asn1_flag(Asn1Flag::NAMED_CURVE); 22 | g 23 | } 24 | 25 | /// Make an RSA private key (from which we can derive a public key). 26 | /// 27 | /// This library does not check the number of bits used to create the key pair. 28 | /// For Let's Encrypt, the bits must be between 2048 and 4096. 29 | pub fn create_rsa_key(bits: u32) -> PKey { 30 | let pri_key_rsa = Rsa::generate(bits).expect("Rsa::generate"); 31 | PKey::from_rsa(pri_key_rsa).expect("from_rsa") 32 | } 33 | 34 | /// Make a P-256 private key (from which we can derive a public key). 35 | pub fn create_p256_key() -> PKey { 36 | let pri_key_ec = EcKey::generate(&*EC_GROUP_P256).expect("EcKey"); 37 | PKey::from_ec_key(pri_key_ec).expect("from_ec_key") 38 | } 39 | 40 | /// Make a P-384 private key pair (from which we can derive a public key). 41 | pub fn create_p384_key() -> PKey { 42 | let pri_key_ec = EcKey::generate(&*EC_GROUP_P384).expect("EcKey"); 43 | PKey::from_ec_key(pri_key_ec).expect("from_ec_key") 44 | } 45 | 46 | pub(crate) fn create_csr(pkey: &PKey, domains: &[&str]) -> Result { 47 | // 48 | // the csr builder 49 | let mut req_bld = X509ReqBuilder::new().expect("X509ReqBuilder"); 50 | 51 | // set private/public key in builder 52 | req_bld.set_pubkey(pkey).expect("set_pubkey"); 53 | 54 | // set all domains as alt names 55 | let mut stack = Stack::new().expect("Stack::new"); 56 | let ctx = req_bld.x509v3_context(None); 57 | let mut an = SubjectAlternativeName::new(); 58 | for d in domains { 59 | an.dns(d); 60 | } 61 | 62 | let ext = an.build(&ctx).expect("SubjectAlternativeName::build"); 63 | stack.push(ext).expect("Stack::push"); 64 | req_bld.add_extensions(&stack).expect("add_extensions"); 65 | 66 | // sign it 67 | req_bld 68 | .sign(pkey, MessageDigest::sha256()) 69 | .expect("csr_sign"); 70 | 71 | // the csr 72 | Ok(req_bld.build()) 73 | } 74 | 75 | /// Encapsulated certificate and private key. 76 | #[derive(Debug, Clone, PartialEq, Eq)] 77 | pub struct Certificate { 78 | private_key: String, 79 | certificate: String, 80 | } 81 | 82 | impl Certificate { 83 | pub(crate) fn new(private_key: String, certificate: String) -> Self { 84 | Certificate { 85 | private_key, 86 | certificate, 87 | } 88 | } 89 | 90 | /// The PEM encoded private key. 91 | pub fn private_key(&self) -> &str { 92 | &self.private_key 93 | } 94 | 95 | /// The private key as DER. 96 | pub fn private_key_der(&self) -> Vec { 97 | let pkey = PKey::private_key_from_pem(self.private_key.as_bytes()).expect("from_pem"); 98 | pkey.private_key_to_der().expect("private_key_to_der") 99 | } 100 | 101 | /// The PEM encoded issued certificate. 102 | pub fn certificate(&self) -> &str { 103 | &self.certificate 104 | } 105 | 106 | /// The issued certificate as DER. 107 | pub fn certificate_der(&self) -> Vec { 108 | let x509 = X509::from_pem(self.certificate.as_bytes()).expect("from_pem"); 109 | x509.to_der().expect("to_der") 110 | } 111 | 112 | /// Inspect the certificate to count the number of (whole) valid days left. 113 | /// 114 | /// It's up to the ACME API provider to decide how long an issued certificate is valid. 115 | /// Let's Encrypt sets the validity to 90 days. This function reports 89 days for newly 116 | /// issued cert, since it counts _whole_ days. 117 | /// 118 | /// It is possible to get negative days for an expired certificate. 119 | pub fn valid_days_left(&self) -> i64 { 120 | // the cert used in the tests is not valid to load as x509 121 | if cfg!(test) { 122 | return 89; 123 | } 124 | 125 | // load as x509 126 | let x509 = X509::from_pem(self.certificate.as_bytes()).expect("from_pem"); 127 | 128 | // convert asn1 time to Tm 129 | let not_after = x509.not_after().to_string(); 130 | // Display trait produces this format, which is kinda dumb. 131 | // Apr 19 08:48:46 2019 GMT 132 | let expires = parse_date(¬_after); 133 | let dur = expires - jiff::Timestamp::now(); 134 | 135 | dur.get_days() as i64 136 | } 137 | } 138 | 139 | fn parse_date(s: &str) -> jiff::Timestamp { 140 | let s = s.replace(" GMT", " +0000"); 141 | println!("Parse date/time: {}", s); 142 | jiff::fmt::strtime::parse("%h %e %H:%M:%S %Y %z", s) 143 | .expect("strtime") 144 | .to_timestamp() 145 | .expect("timestamp") 146 | } 147 | 148 | #[cfg(test)] 149 | mod test { 150 | use super::*; 151 | 152 | #[test] 153 | fn test_parse_date() { 154 | let x = parse_date("May 3 07:40:15 2019 GMT"); 155 | assert_eq!(x.to_string(), "2019-05-03T07:40:15Z"); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/dir.rs: -------------------------------------------------------------------------------- 1 | // 2 | use std::sync::Arc; 3 | 4 | use crate::acc::AcmeKey; 5 | use crate::api::{ApiAccount, ApiDirectory}; 6 | use crate::persist::{Persist, PersistKey, PersistKind}; 7 | use crate::req::{req_expect_header, req_get, req_handle_error}; 8 | use crate::trans::{NoncePool, Transport}; 9 | use crate::util::read_json; 10 | use crate::{Account, Result}; 11 | 12 | const LETSENCRYPT: &str = "https://acme-v02.api.letsencrypt.org/directory"; 13 | const LETSENCRYPT_STAGING: &str = "https://acme-staging-v02.api.letsencrypt.org/directory"; 14 | 15 | /// Enumeration of known ACME API directories. 16 | #[derive(Debug, Clone)] 17 | pub enum DirectoryUrl<'a> { 18 | /// The main Let's Encrypt directory. Not appropriate for testing and dev. 19 | LetsEncrypt, 20 | /// The staging Let's Encrypt directory. Use for testing and dev. Doesn't issue 21 | /// "valid" certificates. The root signing certificate is not supposed 22 | /// to be in any trust chains. 23 | LetsEncryptStaging, 24 | /// Provide an arbitrary director URL to connect to. 25 | Other(&'a str), 26 | } 27 | 28 | impl<'a> DirectoryUrl<'a> { 29 | fn to_url(&self) -> &str { 30 | match self { 31 | DirectoryUrl::LetsEncrypt => LETSENCRYPT, 32 | DirectoryUrl::LetsEncryptStaging => LETSENCRYPT_STAGING, 33 | DirectoryUrl::Other(s) => s, 34 | } 35 | } 36 | } 37 | 38 | /// Entry point for accessing an ACME API. 39 | #[derive(Clone)] 40 | pub struct Directory { 41 | persist: P, 42 | nonce_pool: Arc, 43 | api_directory: ApiDirectory, 44 | } 45 | 46 | impl Directory

{ 47 | /// Create a directory over a persistence implementation and directory url. 48 | pub fn from_url(persist: P, url: DirectoryUrl) -> Result> { 49 | let dir_url = url.to_url(); 50 | let res = req_handle_error(req_get(dir_url))?; 51 | let api_directory: ApiDirectory = read_json(res)?; 52 | let nonce_pool = Arc::new(NoncePool::new(&api_directory.newNonce)); 53 | Ok(Directory { 54 | persist, 55 | nonce_pool, 56 | api_directory, 57 | }) 58 | } 59 | 60 | /// Access an account identified by a contact email. 61 | /// 62 | /// If a persisted private key exists for the contact email, it will be read 63 | /// and used for further access. This way we reuse the same ACME API account. 64 | /// 65 | /// If one doesn't exist, it is created and the corresponding public key is 66 | /// uploaded to the ACME API thus creating the account. 67 | /// 68 | /// Either way the `newAccount` API endpoint is called and thereby ensures the 69 | /// account is active and working. 70 | /// 71 | /// This is the same as calling 72 | /// `account_with_realm(contact_email, ["mailto: "]`) 73 | pub fn account(&self, contact_email: &str) -> Result> { 74 | // Contact email is the persistence realm when using this method. 75 | let contact = vec![format!("mailto:{}", contact_email)]; 76 | self.account_with_realm(contact_email, Some(contact)) 77 | } 78 | 79 | /// Access an account using a lower level method. The contact is optional 80 | /// against the ACME API provider and there might be situations where you 81 | /// either don't need it at all, or need it to be something else than 82 | /// an email address. 83 | /// 84 | /// The `realm` parameter is a persistence realm, i.e. a namespace in the 85 | /// persistence where all values belonging to this Account will be stored. 86 | /// 87 | /// If a persisted private key exists for the `realm`, it will be read 88 | /// and used for further access. This way we reuse the same ACME API account. 89 | /// 90 | /// If one doesn't exist, it is created and the corresponding public key is 91 | /// uploaded to the ACME API thus creating the account. 92 | /// 93 | /// Either way the `newAccount` API endpoint is called and thereby ensures the 94 | /// account is active and working. 95 | pub fn account_with_realm( 96 | &self, 97 | realm: &str, 98 | contact: Option>, 99 | ) -> Result> { 100 | // key in persistence for acme account private key 101 | let pem_key = PersistKey::new(realm, PersistKind::AccountPrivateKey, "acme_account"); 102 | 103 | // Get the key from a saved PEM, or from creating a new 104 | let mut is_new = false; 105 | let pem = self.persist().get(&pem_key)?; 106 | let acme_key = if let Some(pem) = pem { 107 | // we got a persisted private key. read it. 108 | debug!("Read persisted acme account key"); 109 | AcmeKey::from_pem(&pem)? 110 | } else { 111 | // create a new key (and new account) 112 | debug!("Create new acme account key"); 113 | is_new = true; 114 | AcmeKey::new() 115 | }; 116 | 117 | // Prepare making a call to newAccount. This is fine to do both for 118 | // new keys and existing. For existing the spec says to return a 200 119 | // with the Location header set to the key id (kid). 120 | let acc = ApiAccount { 121 | contact, 122 | termsOfServiceAgreed: Some(true), 123 | ..Default::default() 124 | }; 125 | 126 | let mut transport = Transport::new(&self.nonce_pool, acme_key); 127 | let res = transport.call_jwk(&self.api_directory.newAccount, &acc)?; 128 | let kid = req_expect_header(&res, "location")?; 129 | debug!("Key id is: {}", kid); 130 | let api_account: ApiAccount = read_json(res)?; 131 | 132 | // fill in the server returned key id 133 | transport.set_key_id(kid); 134 | 135 | // If we did create a new key, save it back to the persistence. 136 | if is_new { 137 | debug!("Persist acme account key"); 138 | let pem = transport.acme_key().to_pem(); 139 | self.persist().put(&pem_key, &pem)?; 140 | } 141 | 142 | // The finished account 143 | Ok(Account::new( 144 | self.persist.clone(), 145 | transport, 146 | realm, 147 | api_account, 148 | self.api_directory.clone(), 149 | )) 150 | } 151 | 152 | /// Access the underlying JSON object for debugging. 153 | pub fn api_directory(&self) -> &ApiDirectory { 154 | &self.api_directory 155 | } 156 | 157 | pub(crate) fn persist(&self) -> &P { 158 | &self.persist 159 | } 160 | } 161 | 162 | #[cfg(test)] 163 | mod test { 164 | use super::*; 165 | use crate::persist::*; 166 | #[test] 167 | fn test_create_directory() -> Result<()> { 168 | let server = crate::test::with_directory_server(); 169 | let url = DirectoryUrl::Other(&server.dir_url); 170 | let persist = MemoryPersist::new(); 171 | let _ = Directory::from_url(persist, url)?; 172 | Ok(()) 173 | } 174 | 175 | #[test] 176 | fn test_create_acount() -> Result<()> { 177 | let server = crate::test::with_directory_server(); 178 | let url = DirectoryUrl::Other(&server.dir_url); 179 | let persist = MemoryPersist::new(); 180 | let dir = Directory::from_url(persist, url)?; 181 | let _ = dir.account("foo@bar.com")?; 182 | Ok(()) 183 | } 184 | 185 | #[test] 186 | fn test_persisted_acount() -> Result<()> { 187 | let server = crate::test::with_directory_server(); 188 | let url = DirectoryUrl::Other(&server.dir_url); 189 | let persist = MemoryPersist::new(); 190 | let dir = Directory::from_url(persist, url)?; 191 | let acc1 = dir.account("foo@bar.com")?; 192 | let acc2 = dir.account("foo@bar.com")?; 193 | let acc3 = dir.account("karlfoo@bar.com")?; 194 | assert_eq!(acc1.acme_private_key_pem(), acc2.acme_private_key_pem()); 195 | assert!(acc1.acme_private_key_pem() != acc3.acme_private_key_pem()); 196 | Ok(()) 197 | } 198 | 199 | // #[test] 200 | // fn test_the_whole_hog() -> Result<()> { 201 | // std::env::set_var("RUST_LOG", "acme_lib=trace"); 202 | // let _ = env_logger::try_init(); 203 | 204 | // use crate::cert::create_p384_key; 205 | 206 | // let url = DirectoryUrl::LetsEncryptStaging; 207 | // let persist = FilePersist::new("."); 208 | // let dir = Directory::from_url(persist, url)?; 209 | // let acc = dir.account("foo@bar.com")?; 210 | 211 | // let mut ord = acc.new_order("myspecialsite.com", &[])?; 212 | 213 | // let ord = loop { 214 | // if let Some(ord) = ord.confirm_validations() { 215 | // break ord; 216 | // } 217 | 218 | // let auths = ord.authorizations()?; 219 | // let chall = auths[0].dns_challenge(); 220 | 221 | // info!("Proof: {}", chall.dns_proof()); 222 | 223 | // use std::thread; 224 | // use std::time::Duration; 225 | // thread::sleep(Duration::from_millis(60_000)); 226 | 227 | // chall.validate(5000)?; 228 | 229 | // ord.refresh()?; 230 | // }; 231 | 232 | // let (pkey_pri, pkey_pub) = create_p384_key(); 233 | 234 | // let ord = ord.finalize_pkey(pkey_pri, pkey_pub, 5000)?; 235 | 236 | // let cert = ord.download_and_save_cert()?; 237 | // println!( 238 | // "{}{}{}", 239 | // cert.private_key(), 240 | // cert.certificate(), 241 | // cert.valid_days_left() 242 | // ); 243 | // Ok(()) 244 | // } 245 | } 246 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | // 2 | use std::fmt; 3 | use std::io; 4 | 5 | use crate::api::ApiProblem; 6 | 7 | /// acme-lib result. 8 | pub type Result = std::result::Result; 9 | 10 | /// acme-lib errors. 11 | #[derive(Debug)] 12 | pub enum Error { 13 | /// An API call failed. 14 | ApiProblem(ApiProblem), 15 | /// An API call failed. 16 | Call(String), 17 | /// Base64 decoding failed. 18 | Base64Decode(base64::DecodeError), 19 | /// JSON serialization/deserialization error. 20 | Json(serde_json::Error), 21 | /// std::io error. 22 | Io(io::Error), 23 | /// Some other error. Notice that `Error` is 24 | /// `From` and `From<&str>` and it becomes `Other`. 25 | Other(String), 26 | } 27 | impl std::error::Error for Error {} 28 | 29 | impl fmt::Display for Error { 30 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 31 | match self { 32 | Error::ApiProblem(a) => write!(f, "{}", a), 33 | Error::Call(s) => write!(f, "{}", s), 34 | Error::Base64Decode(e) => write!(f, "{}", e), 35 | Error::Json(e) => write!(f, "{}", e), 36 | Error::Io(e) => write!(f, "{}", e), 37 | Error::Other(s) => write!(f, "{}", s), 38 | } 39 | } 40 | } 41 | 42 | impl From for Error { 43 | fn from(e: ApiProblem) -> Self { 44 | Error::ApiProblem(e) 45 | } 46 | } 47 | 48 | impl From for Error { 49 | fn from(e: serde_json::Error) -> Self { 50 | Error::Json(e) 51 | } 52 | } 53 | 54 | impl From for Error { 55 | fn from(e: io::Error) -> Self { 56 | Error::Io(e) 57 | } 58 | } 59 | 60 | impl From for Error { 61 | fn from(s: String) -> Self { 62 | Error::Other(s) 63 | } 64 | } 65 | 66 | impl From<&str> for Error { 67 | fn from(s: &str) -> Self { 68 | Error::Other(s.to_string()) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/jwt.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use crate::acc::AcmeKey; 4 | use crate::cert::EC_GROUP_P256; 5 | use crate::util::base64url; 6 | 7 | #[derive(Debug, Serialize, Deserialize, Default)] 8 | pub(crate) struct JwsProtected { 9 | alg: String, 10 | url: String, 11 | nonce: String, 12 | #[serde(skip_serializing_if = "Option::is_none")] 13 | jwk: Option, 14 | #[serde(skip_serializing_if = "Option::is_none")] 15 | kid: Option, 16 | } 17 | 18 | impl JwsProtected { 19 | pub(crate) fn new_jwk(jwk: Jwk, url: &str, nonce: String) -> Self { 20 | JwsProtected { 21 | alg: "ES256".into(), 22 | url: url.into(), 23 | nonce, 24 | jwk: Some(jwk), 25 | ..Default::default() 26 | } 27 | } 28 | pub(crate) fn new_kid(kid: &str, url: &str, nonce: String) -> Self { 29 | JwsProtected { 30 | alg: "ES256".into(), 31 | url: url.into(), 32 | nonce, 33 | kid: Some(kid.into()), 34 | ..Default::default() 35 | } 36 | } 37 | } 38 | 39 | #[derive(Debug, Serialize, Deserialize, Clone)] 40 | pub(crate) struct Jwk { 41 | alg: String, 42 | crv: String, 43 | kty: String, 44 | #[serde(rename = "use")] 45 | _use: String, 46 | x: String, 47 | y: String, 48 | } 49 | 50 | #[derive(Debug, Serialize, Deserialize, Clone)] 51 | // LEXICAL ORDER OF FIELDS MATTER! 52 | pub(crate) struct JwkThumb { 53 | crv: String, 54 | kty: String, 55 | x: String, 56 | y: String, 57 | } 58 | 59 | impl From<&AcmeKey> for Jwk { 60 | fn from(a: &AcmeKey) -> Self { 61 | let mut ctx = openssl::bn::BigNumContext::new().expect("BigNumContext"); 62 | let mut x = openssl::bn::BigNum::new().expect("BigNum"); 63 | let mut y = openssl::bn::BigNum::new().expect("BigNum"); 64 | a.private_key() 65 | .public_key() 66 | .affine_coordinates_gfp(&*EC_GROUP_P256, &mut x, &mut y, &mut ctx) 67 | .expect("affine_coordinates_gfp"); 68 | Jwk { 69 | alg: "ES256".into(), 70 | kty: "EC".into(), 71 | crv: "P-256".into(), 72 | _use: "sig".into(), 73 | x: base64url(&x.to_vec()), 74 | y: base64url(&y.to_vec()), 75 | } 76 | } 77 | } 78 | 79 | impl From<&Jwk> for JwkThumb { 80 | fn from(a: &Jwk) -> Self { 81 | JwkThumb { 82 | crv: a.crv.clone(), 83 | kty: a.kty.clone(), 84 | x: a.x.clone(), 85 | y: a.y.clone(), 86 | } 87 | } 88 | } 89 | 90 | #[derive(Debug, Serialize, Deserialize)] 91 | pub(crate) struct Jws { 92 | protected: String, 93 | payload: String, 94 | signature: String, 95 | } 96 | 97 | impl Jws { 98 | pub(crate) fn new(protected: String, payload: String, signature: String) -> Self { 99 | Jws { 100 | protected, 101 | payload, 102 | signature, 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(clippy::all)] 2 | //! acme-lib is a library for accessing ACME (Automatic Certificate Management Environment) 3 | //! services such as [Let's Encrypt](https://letsencrypt.org/). 4 | //! 5 | //! Uses ACME v2 to issue/renew certificates. 6 | //! 7 | //! # Example 8 | //! 9 | //! ```no_run 10 | //! use acme_lib::{Error, Directory, DirectoryUrl}; 11 | //! use acme_lib::persist::FilePersist; 12 | //! use acme_lib::create_p384_key; 13 | //! 14 | //! fn request_cert() -> Result<(), Error> { 15 | //! 16 | //! // Use DirectoryUrl::LetsEncryptStaging for dev/testing. 17 | //! let url = DirectoryUrl::LetsEncrypt; 18 | //! 19 | //! // Save/load keys and certificates to current dir. 20 | //! let persist = FilePersist::new("."); 21 | //! 22 | //! // Create a directory entrypoint. 23 | //! let dir = Directory::from_url(persist, url)?; 24 | //! 25 | //! // Reads the private account key from persistence, or 26 | //! // creates a new one before accessing the API to establish 27 | //! // that it's there. 28 | //! let acc = dir.account("foo@bar.com")?; 29 | //! 30 | //! // Order a new TLS certificate for a domain. 31 | //! let mut ord_new = acc.new_order("mydomain.io", &[])?; 32 | //! 33 | //! // If the ownership of the domain(s) have already been 34 | //! // authorized in a previous order, you might be able to 35 | //! // skip validation. The ACME API provider decides. 36 | //! let ord_csr = loop { 37 | //! // are we done? 38 | //! if let Some(ord_csr) = ord_new.confirm_validations() { 39 | //! break ord_csr; 40 | //! } 41 | //! 42 | //! // Get the possible authorizations (for a single domain 43 | //! // this will only be one element). 44 | //! let auths = ord_new.authorizations()?; 45 | //! 46 | //! // For HTTP, the challenge is a text file that needs to 47 | //! // be placed in your web server's root: 48 | //! // 49 | //! // /var/www/.well-known/acme-challenge/ 50 | //! // 51 | //! // The important thing is that it's accessible over the 52 | //! // web for the domain(s) you are trying to get a 53 | //! // certificate for: 54 | //! // 55 | //! // http://mydomain.io/.well-known/acme-challenge/ 56 | //! let chall = auths[0].http_challenge(); 57 | //! 58 | //! // The token is the filename. 59 | //! let token = chall.http_token(); 60 | //! let path = format!(".well-known/acme-challenge/{}", token); 61 | //! 62 | //! // The proof is the contents of the file 63 | //! let proof = chall.http_proof(); 64 | //! 65 | //! // Here you must do "something" to place 66 | //! // the file/contents in the correct place. 67 | //! // update_my_web_server(&path, &proof); 68 | //! 69 | //! // After the file is accessible from the web, 70 | //! // this tells the ACME API to start checking the 71 | //! // existence of the proof. 72 | //! // 73 | //! // The order at ACME will change status to either 74 | //! // confirm ownership of the domain, or fail due to the 75 | //! // not finding the proof. To see the change, we poll 76 | //! // the API with 5000 milliseconds wait between. 77 | //! chall.validate(5000)?; 78 | //! 79 | //! // Update the state against the ACME API. 80 | //! ord_new.refresh()?; 81 | //! }; 82 | //! 83 | //! // Ownership is proven. Create a private key for 84 | //! // the certificate. These are provided for convenience, you 85 | //! // can provide your own keypair instead if you want. 86 | //! let pkey_pri = create_p384_key(); 87 | //! 88 | //! // Submit the CSR. This causes the ACME provider to enter a 89 | //! // state of "processing" that must be polled until the 90 | //! // certificate is either issued or rejected. Again we poll 91 | //! // for the status change. 92 | //! let ord_cert = 93 | //! ord_csr.finalize_pkey(pkey_pri, 5000)?; 94 | //! 95 | //! // Now download the certificate. Also stores the cert in 96 | //! // the persistence. 97 | //! let cert = ord_cert.download_and_save_cert()?; 98 | //! 99 | //! Ok(()) 100 | //! } 101 | //! ``` 102 | //! 103 | //! ## Domain ownership 104 | //! 105 | //! Most website TLS certificates tries to prove ownership/control over the domain they 106 | //! are issued for. For ACME, this means proving you control either a web server answering 107 | //! HTTP requests to the domain, or the DNS server answering name lookups against the domain. 108 | //! 109 | //! To use this library, there are points in the flow where you would need to modify either 110 | //! the web server or DNS server before progressing to get the certificate. 111 | //! 112 | //! See [`http_challenge`] and [`dns_challenge`]. 113 | //! 114 | //! ### Multiple domains 115 | //! 116 | //! When creating a new order, it's possible to provide multiple alt-names that will also 117 | //! be part of the certificate. The ACME API requires you to prove ownership of each such 118 | //! domain. See [`authorizations`]. 119 | //! 120 | //! [`http_challenge`]: https://docs.rs/acme-lib/latest/acme_lib/order/struct.Auth.html#method.http_challenge 121 | //! [`dns_challenge`]: https://docs.rs/acme-lib/latest/acme_lib/order/struct.Auth.html#method.dns_challenge 122 | //! [`authorizations`]: https://docs.rs/acme-lib/latest/acme_lib/order/struct.NewOrder.html#method.authorizations 123 | //! 124 | //! ## Rate limits 125 | //! 126 | //! The ACME API provider Let's Encrypt uses [rate limits] to ensure the API i not being 127 | //! abused. It might be tempting to put the `delay_millis` really low in some of this 128 | //! libraries' polling calls, but balance this against the real risk of having access 129 | //! cut off. 130 | //! 131 | //! [rate limits]: https://letsencrypt.org/docs/rate-limits/ 132 | //! 133 | //! ### Use staging for dev! 134 | //! 135 | //! Especially take care to use the Let`s Encrypt staging environment for development 136 | //! where the rate limits are more relaxed. 137 | //! 138 | //! See [`DirectoryUrl::LetsEncryptStaging`]. 139 | //! 140 | //! [`DirectoryUrl::LetsEncryptStaging`]: enum.DirectoryUrl.html#variant.LetsEncryptStaging 141 | //! 142 | //! ## Implementation details 143 | //! 144 | //! The library tries to pull in as few dependencies as possible. (For now) that means using 145 | //! synchronous I/O and blocking cals. This doesn't rule out a futures based version later. 146 | //! 147 | //! It is written by following the 148 | //! [ACME draft spec 18](https://tools.ietf.org/html/draft-ietf-acme-acme-18), and relies 149 | //! heavily on the [openssl](https://docs.rs/openssl/) crate to make JWK/JWT and sign requests 150 | //! to the API. 151 | //! 152 | #[macro_use] 153 | extern crate log; 154 | 155 | mod acc; 156 | mod cert; 157 | mod dir; 158 | mod error; 159 | mod jwt; 160 | mod req; 161 | mod trans; 162 | mod util; 163 | 164 | pub mod api; 165 | pub mod order; 166 | pub mod persist; 167 | 168 | #[cfg(test)] 169 | mod test; 170 | 171 | pub use crate::acc::{Account, RevocationReason}; 172 | pub use crate::cert::{create_p256_key, create_p384_key, create_rsa_key, Certificate}; 173 | pub use crate::dir::{Directory, DirectoryUrl}; 174 | pub use crate::error::{Error, Result}; 175 | -------------------------------------------------------------------------------- /src/order/auth.rs: -------------------------------------------------------------------------------- 1 | // 2 | use openssl::sha::sha256; 3 | use std::sync::Arc; 4 | use std::thread; 5 | use std::time::Duration; 6 | 7 | use crate::acc::AccountInner; 8 | use crate::acc::AcmeKey; 9 | use crate::api::{ApiAuth, ApiChallenge, ApiEmptyObject, ApiEmptyString}; 10 | use crate::jwt::*; 11 | use crate::persist::Persist; 12 | use crate::util::{base64url, read_json}; 13 | use crate::Result; 14 | 15 | /// An authorization ([ownership proof]) for a domain name. 16 | /// 17 | /// Each authorization for an order much be progressed to a valid state before the ACME API 18 | /// will issue a certificate. 19 | /// 20 | /// Authorizations may or may not be required depending on previous orders against the same 21 | /// ACME account. The ACME API decides if the authorization is needed. 22 | /// 23 | /// Currently there are two ways of providing the authorization. 24 | /// 25 | /// * In a text file served using [HTTP] from a web server of the domain being authorized. 26 | /// * A `TXT` [DNS] record under the domain being authorized. 27 | /// 28 | /// [ownership proof]: ../index.html#domain-ownership 29 | /// [HTTP]: #method.http_challenge 30 | /// [DNS]: #method.dns_challenge 31 | #[derive(Debug)] 32 | pub struct Auth { 33 | inner: Arc>, 34 | api_auth: ApiAuth, 35 | auth_url: String, 36 | } 37 | 38 | impl Auth

{ 39 | pub(crate) fn new(inner: &Arc>, api_auth: ApiAuth, auth_url: &str) -> Self { 40 | Auth { 41 | inner: inner.clone(), 42 | api_auth, 43 | auth_url: auth_url.into(), 44 | } 45 | } 46 | 47 | /// Domain name for this authorization. 48 | pub fn domain_name(&self) -> &str { 49 | &self.api_auth.identifier.value 50 | } 51 | 52 | /// Whether we actually need to do the authorization. This might not be needed if we have 53 | /// proven ownership of the domain recently in a previous order. 54 | pub fn need_challenge(&self) -> bool { 55 | !self.api_auth.is_status_valid() 56 | } 57 | 58 | /// Get the http challenge. 59 | /// 60 | /// The http challenge must be placed so it is accessible under: 61 | /// 62 | /// ```text 63 | /// http:///.well-known/acme-challenge/ 64 | /// ``` 65 | /// 66 | /// The challenge will be accessed over HTTP (not HTTPS), for obvious reasons. 67 | /// 68 | /// ```no_run 69 | /// use acme_lib::persist::Persist; 70 | /// use acme_lib::order::Auth; 71 | /// use acme_lib::Error; 72 | /// use std::fs::File; 73 | /// use std::io::Write; 74 | /// 75 | /// fn web_authorize(auth: &Auth

) -> Result<(), Error> { 76 | /// let challenge = auth.http_challenge(); 77 | /// // Assuming our web server's root is under /var/www 78 | /// let path = { 79 | /// let token = challenge.http_token(); 80 | /// format!("/var/www/.well-known/acme-challenge/{}", token) 81 | /// }; 82 | /// let mut file = File::create(&path)?; 83 | /// file.write_all(challenge.http_proof().as_bytes())?; 84 | /// challenge.validate(5000)?; 85 | /// Ok(()) 86 | /// } 87 | /// ``` 88 | pub fn http_challenge(&self) -> Challenge { 89 | self.api_auth 90 | .http_challenge() 91 | .map(|c| Challenge::new(&self.inner, c.clone(), &self.auth_url)) 92 | .expect("http-challenge") 93 | } 94 | 95 | /// Get the dns challenge. 96 | /// 97 | /// The dns challenge is a `TXT` record that must put created under: 98 | /// 99 | /// ```text 100 | /// _acme-challenge.. TXT 101 | /// ``` 102 | /// 103 | /// The contains the signed token proving this account update it. 104 | /// 105 | /// ```no_run 106 | /// use acme_lib::persist::Persist; 107 | /// use acme_lib::order::Auth; 108 | /// use acme_lib::Error; 109 | /// 110 | /// fn dns_authorize(auth: &Auth

) -> Result<(), Error> { 111 | /// let challenge = auth.dns_challenge(); 112 | /// let record = format!("_acme-challenge.{}.", auth.domain_name()); 113 | /// // route_53_set_record(&record, "TXT", challenge.dns_proof()); 114 | /// challenge.validate(5000)?; 115 | /// Ok(()) 116 | /// } 117 | /// ``` 118 | /// 119 | /// The dns proof is not the same as the http proof. 120 | pub fn dns_challenge(&self) -> Challenge { 121 | self.api_auth 122 | .dns_challenge() 123 | .map(|c| Challenge::new(&self.inner, c.clone(), &self.auth_url)) 124 | .expect("dns-challenge") 125 | } 126 | 127 | /// Get the TLS ALPN challenge. 128 | /// 129 | /// The TLS ALPN challenge is a certificate that must be served when a 130 | /// request is made for the ALPN protocol "tls-alpn-01". The certificate 131 | /// must contain a single dNSName SAN containing the domain being 132 | /// validated, as well as an ACME extension containing the SHA256 of the 133 | /// key authorization. 134 | pub fn tls_alpn_challenge(&self) -> Challenge { 135 | self.api_auth 136 | .tls_alpn_challenge() 137 | .map(|c| Challenge::new(&self.inner, c.clone(), &self.auth_url)) 138 | .expect("tls-alpn-challenge") 139 | } 140 | 141 | /// Access the underlying JSON object for debugging. We don't 142 | /// refresh the authorization when the corresponding challenge is validated, 143 | /// so there will be no changes to see here. 144 | pub fn api_auth(&self) -> &ApiAuth { 145 | &self.api_auth 146 | } 147 | } 148 | 149 | /// Marker type for http challenges. 150 | #[doc(hidden)] 151 | pub struct Http; 152 | 153 | /// Marker type for dns challenges. 154 | #[doc(hidden)] 155 | pub struct Dns; 156 | 157 | /// Marker type for tls alpn challenges. 158 | #[doc(hidden)] 159 | pub struct TlsAlpn; 160 | 161 | /// A DNS, HTTP, or TLS-ALPN challenge as obtained from the [`Auth`]. 162 | /// 163 | /// [`Auth`]: struct.Auth.html 164 | pub struct Challenge { 165 | inner: Arc>, 166 | api_challenge: ApiChallenge, 167 | auth_url: String, 168 | _ph: std::marker::PhantomData, 169 | } 170 | 171 | impl Challenge { 172 | /// The `token` is a unique identifier of the challenge. It is the file name in the 173 | /// http challenge like so: 174 | /// 175 | /// ```text 176 | /// http:///.well-known/acme-challenge/ 177 | /// ``` 178 | pub fn http_token(&self) -> &str { 179 | &self.api_challenge.token 180 | } 181 | 182 | /// The `proof` is some text content that is placed in the file named by `token`. 183 | pub fn http_proof(&self) -> String { 184 | let acme_key = self.inner.transport.acme_key(); 185 | key_authorization(&self.api_challenge.token, acme_key, false) 186 | } 187 | } 188 | 189 | impl Challenge { 190 | /// The `proof` is the `TXT` record placed under: 191 | /// 192 | /// ```text 193 | /// _acme-challenge.. TXT 194 | /// ``` 195 | pub fn dns_proof(&self) -> String { 196 | let acme_key = self.inner.transport.acme_key(); 197 | key_authorization(&self.api_challenge.token, acme_key, true) 198 | } 199 | } 200 | 201 | impl Challenge { 202 | /// The `proof` is the contents of the ACME extension to be placed in the 203 | /// certificate used for validation. 204 | pub fn tls_alpn_proof(&self) -> [u8; 32] { 205 | let acme_key = self.inner.transport.acme_key(); 206 | sha256(key_authorization(&self.api_challenge.token, acme_key, false).as_bytes()) 207 | } 208 | } 209 | 210 | impl Challenge { 211 | fn new(inner: &Arc>, api_challenge: ApiChallenge, auth_url: &str) -> Self { 212 | Challenge { 213 | inner: inner.clone(), 214 | api_challenge, 215 | auth_url: auth_url.into(), 216 | _ph: std::marker::PhantomData, 217 | } 218 | } 219 | 220 | /// Check whether this challlenge really need validation. It might already been 221 | /// done in a previous order for the same account. 222 | pub fn need_validate(&self) -> bool { 223 | self.api_challenge.is_status_pending() 224 | } 225 | 226 | /// Tell the ACME API to attempt validating the proof of this challenge. 227 | /// 228 | /// The user must first update the DNS record or HTTP web server depending 229 | /// on the type challenge being validated. 230 | pub fn validate(self, delay_millis: u64) -> Result<()> { 231 | let url_chall = &self.api_challenge.url; 232 | let res = self.inner.transport.call(url_chall, &ApiEmptyObject)?; 233 | let _: ApiChallenge = read_json(res)?; 234 | 235 | let auth = wait_for_auth_status(&self.inner, &self.auth_url, delay_millis)?; 236 | 237 | if !auth.is_status_valid() { 238 | let error = auth 239 | .challenges 240 | .iter() 241 | .filter_map(|c| c.error.as_ref()) 242 | .next(); 243 | let reason = if let Some(error) = error { 244 | format!( 245 | "Failed: {}", 246 | error.detail.clone().unwrap_or_else(|| error._type.clone()) 247 | ) 248 | } else { 249 | "Validation failed and no error found".into() 250 | }; 251 | return Err(reason.into()); 252 | } 253 | 254 | Ok(()) 255 | } 256 | 257 | /// Access the underlying JSON object for debugging. 258 | pub fn api_challenge(&self) -> &ApiChallenge { 259 | &self.api_challenge 260 | } 261 | } 262 | 263 | fn key_authorization(token: &str, key: &AcmeKey, extra_sha256: bool) -> String { 264 | let jwk: Jwk = key.into(); 265 | let jwk_thumb: JwkThumb = (&jwk).into(); 266 | let jwk_json = serde_json::to_string(&jwk_thumb).expect("jwk_thumb"); 267 | let digest = base64url(&sha256(jwk_json.as_bytes())); 268 | let key_auth = format!("{}.{}", token, digest); 269 | if extra_sha256 { 270 | base64url(&sha256(key_auth.as_bytes())) 271 | } else { 272 | key_auth 273 | } 274 | } 275 | 276 | fn wait_for_auth_status( 277 | inner: &Arc>, 278 | auth_url: &str, 279 | delay_millis: u64, 280 | ) -> Result { 281 | let auth = loop { 282 | let res = inner.transport.call(auth_url, &ApiEmptyString)?; 283 | let auth: ApiAuth = read_json(res)?; 284 | if !auth.is_status_pending() { 285 | break auth; 286 | } 287 | thread::sleep(Duration::from_millis(delay_millis)); 288 | }; 289 | Ok(auth) 290 | } 291 | 292 | #[cfg(test)] 293 | mod test { 294 | use crate::persist::*; 295 | use crate::*; 296 | 297 | #[test] 298 | fn test_get_challenges() -> Result<()> { 299 | let server = crate::test::with_directory_server(); 300 | let url = DirectoryUrl::Other(&server.dir_url); 301 | let persist = MemoryPersist::new(); 302 | let dir = Directory::from_url(persist, url)?; 303 | let acc = dir.account("foo@bar.com")?; 304 | let ord = acc.new_order("acmetest.example.com", &[])?; 305 | let authz = ord.authorizations()?; 306 | assert!(authz.len() == 1); 307 | let auth = &authz[0]; 308 | { 309 | let http = auth.http_challenge(); 310 | assert!(http.need_validate()); 311 | } 312 | { 313 | let dns = auth.dns_challenge(); 314 | assert!(dns.need_validate()); 315 | } 316 | Ok(()) 317 | } 318 | } 319 | -------------------------------------------------------------------------------- /src/order/mod.rs: -------------------------------------------------------------------------------- 1 | //! Order life cycle. 2 | //! 3 | //! An order goes through a life cycle of different states that require various actions by 4 | //! the user. To ensure the user only use appropriate actions, this library have simple façade 5 | //! structs that wraps the actual [`ApiOrder`]. 6 | //! 7 | //! 1. First prove ownership: 8 | //! * [`NewOrder`] -> [`Auth`]* -> [`Challenge`] 9 | //! 2. Then submit CSR and download the cert. 10 | //! * [`NewOrder`] -> [`CsrOrder`] -> [`CertOrder`] 11 | //! 12 | //! \* Possibly multiple auths. 13 | //! 14 | //! [`ApiOrder`]: ../api/struct.ApiOrder.html 15 | //! [`NewOrder`]: struct.NewOrder.html 16 | //! [`Auth`]: struct.Auth.html 17 | //! [`Challenge`]: struct.Challenge.html 18 | //! [`CsrOrder`]: struct.CsrOrder.html 19 | //! [`CertOrder`]: struct.CertOrder.html 20 | use openssl::pkey::{self, PKey}; 21 | use std::sync::Arc; 22 | use std::thread; 23 | use std::time::Duration; 24 | use ureq::{http, Body}; 25 | 26 | use crate::acc::AccountInner; 27 | use crate::api::{ApiAuth, ApiEmptyString, ApiFinalize, ApiOrder}; 28 | use crate::cert::{create_csr, Certificate}; 29 | use crate::persist::{Persist, PersistKey, PersistKind}; 30 | use crate::util::{base64url, read_json}; 31 | use crate::Result; 32 | 33 | mod auth; 34 | 35 | pub use self::auth::{Auth, Challenge}; 36 | 37 | /// The order wrapped with an outer façade. 38 | pub(crate) struct Order { 39 | inner: Arc>, 40 | api_order: ApiOrder, 41 | url: String, 42 | } 43 | 44 | impl Order

{ 45 | pub(crate) fn new(inner: &Arc>, api_order: ApiOrder, url: String) -> Self { 46 | Order { 47 | inner: inner.clone(), 48 | api_order, 49 | url, 50 | } 51 | } 52 | } 53 | 54 | /// Helper to refresh an order status (POST-as-GET). 55 | pub(crate) fn refresh_order( 56 | inner: &Arc>, 57 | url: String, 58 | want_status: &'static str, 59 | ) -> Result> { 60 | let res = inner.transport.call(&url, &ApiEmptyString)?; 61 | 62 | // our test rig requires the order to be in `want_status`. 63 | // api_order_of is different for test compilation 64 | let api_order = api_order_of(res, want_status)?; 65 | 66 | Ok(Order { 67 | inner: inner.clone(), 68 | api_order, 69 | url, 70 | }) 71 | } 72 | 73 | #[cfg(not(test))] 74 | fn api_order_of(res: http::Response, _want_status: &str) -> Result { 75 | read_json(res) 76 | } 77 | 78 | #[cfg(test)] 79 | // our test rig requires the order to be in `want_status` 80 | fn api_order_of(mut res: http::Response, want_status: &str) -> Result { 81 | let s = res.body_mut().read_to_string().map_err(|e| e.into_io())?; 82 | #[allow(clippy::trivial_regex)] 83 | let re = regex::Regex::new("").unwrap(); 84 | let b = re.replace_all(&s, want_status).to_string(); 85 | let api_order: ApiOrder = serde_json::from_str(&b)?; 86 | Ok(api_order) 87 | } 88 | 89 | /// A new order created by [`Account::new_order`]. 90 | /// 91 | /// An order is created using one or many domains (a primary `CN` and possible multiple 92 | /// alt names). All domains in the order must have authorizations ([confirmed ownership]) 93 | /// before the order can progress to submitting a [CSR]. 94 | /// 95 | /// This order façade provides calls to provide such authorizations and to progress the order 96 | /// when ready. 97 | /// 98 | /// The ACME API provider might "remember" for a time that you already own a domain, which 99 | /// means you might not need to prove the ownership every time. Use appropriate methods to 100 | /// first check whether you really need to handle authorizations. 101 | /// 102 | /// [`Account::new_order`]: ../struct.Account.html#method.new_order 103 | /// [confirmed ownership]: ../index.html#domain-ownership 104 | /// [CSR]: https://en.wikipedia.org/wiki/Certificate_signing_request 105 | pub struct NewOrder { 106 | pub(crate) order: Order

, 107 | } 108 | 109 | impl NewOrder

{ 110 | /// Tell if the domains in this order have been authorized. 111 | /// 112 | /// This doesn't do any calls against the API. You must manually call [`refresh`]. 113 | /// 114 | /// In ACME API terms, the order can either be `ready` or `valid`, which both would 115 | /// mean we have passed the authorization stage. 116 | /// 117 | /// [`refresh`]: struct.NewOrder.html#method.refresh 118 | pub fn is_validated(&self) -> bool { 119 | self.order.api_order.is_status_ready() || self.order.api_order.is_status_valid() 120 | } 121 | 122 | /// If the order [`is_validated`] progress it to a [`CsrOrder`]. 123 | /// 124 | /// This doesn't do any calls against the API. You must manually call [`refresh`]. 125 | /// 126 | /// [`is_validated`]: struct.NewOrder.html#method.is_validated 127 | /// [`CsrOrder`]: struct.CsrOrder.html 128 | pub fn confirm_validations(&self) -> Option> { 129 | if self.is_validated() { 130 | Some(CsrOrder { 131 | order: Order::new( 132 | &self.order.inner, 133 | self.order.api_order.clone(), 134 | self.order.url.clone(), 135 | ), 136 | }) 137 | } else { 138 | None 139 | } 140 | } 141 | 142 | /// Refresh the order state against the ACME API. 143 | /// 144 | /// The specification calls this a "POST-as-GET" against the order URL. 145 | pub fn refresh(&mut self) -> Result<()> { 146 | let order = refresh_order(&self.order.inner, self.order.url.clone(), "ready")?; 147 | self.order = order; 148 | Ok(()) 149 | } 150 | 151 | /// Provide the authorizations. The number of authorizations will be the same as 152 | /// the number of domains requests, i.e. at least one (the primary CN), but possibly 153 | /// more (for alt names). 154 | /// 155 | /// If the order includes new domain names that have not been authorized before, this 156 | /// list might contain a mix of already valid and not yet valid auths. 157 | pub fn authorizations(&self) -> Result>> { 158 | let mut result = vec![]; 159 | if let Some(authorizations) = &self.order.api_order.authorizations { 160 | for auth_url in authorizations { 161 | let res = self.order.inner.transport.call(auth_url, &ApiEmptyString)?; 162 | let api_auth: ApiAuth = read_json(res)?; 163 | result.push(Auth::new(&self.order.inner, api_auth, auth_url)); 164 | } 165 | } 166 | Ok(result) 167 | } 168 | 169 | /// Access the underlying JSON object for debugging. 170 | pub fn api_order(&self) -> &ApiOrder { 171 | &self.order.api_order 172 | } 173 | } 174 | 175 | /// An order that is ready for a [CSR] submission. 176 | /// 177 | /// To submit the CSR is called "finalizing" the order. 178 | /// 179 | /// To finalize, the user supplies a private key (from which a public key is derived). This 180 | /// library provides [functions to create private keys], but the user can opt for creating them 181 | /// in some other way. 182 | /// 183 | /// This library makes no attempt at validating which key algorithms are used. Unsupported 184 | /// algorithms will show as an error when finalizing the order. It is up to the ACME API 185 | /// provider to decide which key algorithms to support. 186 | /// 187 | /// Right now Let's Encrypt [supports]: 188 | /// 189 | /// * RSA keys from 2048 to 4096 bits in length 190 | /// * P-256 and P-384 ECDSA keys 191 | /// 192 | /// [CSR]: https://en.wikipedia.org/wiki/Certificate_signing_request 193 | /// [functions to create key pairs]: ../index.html#functions 194 | /// [supports]: https://letsencrypt.org/docs/integration-guide/#supported-key-algorithms 195 | pub struct CsrOrder { 196 | pub(crate) order: Order

, 197 | } 198 | 199 | impl CsrOrder

{ 200 | /// Finalize the order by providing a private key as PEM. 201 | /// 202 | /// Once the CSR has been submitted, the order goes into a `processing` status, 203 | /// where we must poll until the status changes. The `delay_millis` is the 204 | /// amount of time to wait between each poll attempt. 205 | /// 206 | /// This is a convenience wrapper that in turn calls the lower level [`finalize_pkey`]. 207 | /// 208 | /// [`finalize_pkey`]: struct.CsrOrder.html#method.finalize_pkey 209 | pub fn finalize(self, private_key_pem: &str, delay_millis: u64) -> Result> { 210 | let pkey_pri = PKey::private_key_from_pem(private_key_pem.as_bytes()) 211 | .map_err(|e| format!("Error reading private key PEM: {}", e))?; 212 | self.finalize_pkey(pkey_pri, delay_millis) 213 | } 214 | 215 | /// Lower level finalize call that works directly with the openssl crate structures. 216 | /// 217 | /// Creates the CSR for the domains in the order and submit it to the ACME API. 218 | /// 219 | /// Once the CSR has been submitted, the order goes into a `processing` status, 220 | /// where we must poll until the status changes. The `delay_millis` is the 221 | /// amount of time to wait between each poll attempt. 222 | pub fn finalize_pkey( 223 | self, 224 | private_key: PKey, 225 | delay_millis: u64, 226 | ) -> Result> { 227 | // 228 | // the domains that we have authorized 229 | let domains = self.order.api_order.domains(); 230 | 231 | // csr from private key and authorized domains. 232 | let csr = create_csr(&private_key, &domains)?; 233 | 234 | // this is not the same as PEM. 235 | let csr_der = csr.to_der().expect("to_der()"); 236 | let csr_enc = base64url(&csr_der); 237 | let finalize = ApiFinalize { csr: csr_enc }; 238 | 239 | let inner = self.order.inner; 240 | let order_url = self.order.url; 241 | let finalize_url = &self.order.api_order.finalize; 242 | 243 | // if the CSR is invalid, we will get a 4xx code back that 244 | // bombs out from this retry_call. 245 | inner.transport.call(finalize_url, &finalize)?; 246 | 247 | // wait for the status to not be processing. 248 | // valid -> cert is issued 249 | // invalid -> the whole thing is off 250 | let order = wait_for_order_status(&inner, &order_url, delay_millis)?; 251 | 252 | if !order.api_order.is_status_valid() { 253 | return Err(format!("Order is in status: {:?}", order.api_order.status).into()); 254 | } 255 | 256 | Ok(CertOrder { private_key, order }) 257 | } 258 | 259 | /// Access the underlying JSON object for debugging. 260 | pub fn api_order(&self) -> &ApiOrder { 261 | &self.order.api_order 262 | } 263 | } 264 | 265 | fn wait_for_order_status( 266 | inner: &Arc>, 267 | url: &str, 268 | delay_millis: u64, 269 | ) -> Result> { 270 | loop { 271 | let order = refresh_order(inner, url.to_string(), "valid")?; 272 | if !order.api_order.is_status_processing() { 273 | return Ok(order); 274 | } 275 | thread::sleep(Duration::from_millis(delay_millis)); 276 | } 277 | } 278 | 279 | /// Order for an issued certificate that is ready to download. 280 | pub struct CertOrder { 281 | private_key: PKey, 282 | order: Order

, 283 | } 284 | 285 | impl CertOrder

{ 286 | /// Request download of the issued certificate. 287 | /// 288 | /// When downloaded, the certificate and key will be saved in the 289 | /// persistence. They can later be retreived using [`Account::certificate`]. 290 | /// 291 | /// [`Account::certificate`]: ../struct.Account.html#method.certificate 292 | pub fn download_and_save_cert(self) -> Result { 293 | // 294 | let primary_name = self.order.api_order.domains()[0].to_string(); 295 | let url = self.order.api_order.certificate.expect("certificate url"); 296 | let inner = self.order.inner; 297 | let realm = &inner.realm[..]; 298 | 299 | let mut res = inner.transport.call(&url, &ApiEmptyString)?; 300 | 301 | // save key and cert into persistence 302 | let persist = &inner.persist; 303 | let pk_key = PersistKey::new(realm, PersistKind::PrivateKey, &primary_name); 304 | let pkey_pem_bytes = self.private_key.private_key_to_pem_pkcs8().expect("to_pem"); 305 | let pkey_pem = String::from_utf8_lossy(&pkey_pem_bytes); 306 | debug!("Save private key: {}", pk_key); 307 | persist.put(&pk_key, &pkey_pem_bytes)?; 308 | 309 | let cert = res.body_mut().read_to_string().map_err(|e| e.into_io())?; 310 | let pk_crt = PersistKey::new(realm, PersistKind::Certificate, &primary_name); 311 | debug!("Save certificate: {}", pk_crt); 312 | persist.put(&pk_crt, cert.as_bytes())?; 313 | 314 | Ok(Certificate::new(pkey_pem.to_string(), cert)) 315 | } 316 | 317 | /// Access the underlying JSON object for debugging. 318 | pub fn api_order(&self) -> &ApiOrder { 319 | &self.order.api_order 320 | } 321 | } 322 | 323 | #[cfg(test)] 324 | mod test { 325 | use super::*; 326 | use crate::persist::*; 327 | use crate::*; 328 | 329 | #[test] 330 | fn test_get_authorizations() -> Result<()> { 331 | let server = crate::test::with_directory_server(); 332 | let url = DirectoryUrl::Other(&server.dir_url); 333 | let persist = MemoryPersist::new(); 334 | let dir = Directory::from_url(persist, url)?; 335 | let acc = dir.account("foo@bar.com")?; 336 | let ord = acc.new_order("acmetest.example.com", &[])?; 337 | let _ = ord.authorizations()?; 338 | Ok(()) 339 | } 340 | 341 | #[test] 342 | fn test_finalize() -> Result<()> { 343 | let server = crate::test::with_directory_server(); 344 | let url = DirectoryUrl::Other(&server.dir_url); 345 | let persist = MemoryPersist::new(); 346 | let dir = Directory::from_url(persist, url)?; 347 | let acc = dir.account("foo@bar.com")?; 348 | let ord = acc.new_order("acmetest.example.com", &[])?; 349 | // shortcut auth 350 | let ord = CsrOrder { order: ord.order }; 351 | let pkey = cert::create_p256_key(); 352 | let _ord = ord.finalize_pkey(pkey, 1)?; 353 | Ok(()) 354 | } 355 | 356 | #[test] 357 | fn test_download_and_save_cert() -> Result<()> { 358 | let server = crate::test::with_directory_server(); 359 | let url = DirectoryUrl::Other(&server.dir_url); 360 | let persist = MemoryPersist::new(); 361 | let dir = Directory::from_url(persist, url)?; 362 | let acc = dir.account("foo@bar.com")?; 363 | let ord = acc.new_order("acmetest.example.com", &[])?; 364 | 365 | // shortcut auth 366 | let ord = CsrOrder { order: ord.order }; 367 | let pkey = cert::create_p256_key(); 368 | let ord = ord.finalize_pkey(pkey, 1)?; 369 | 370 | let cert = ord.download_and_save_cert()?; 371 | assert_eq!("CERT HERE", cert.certificate()); 372 | assert!(!cert.private_key().is_empty()); 373 | 374 | // check that the keys have been persisted 375 | let cert2 = acc.certificate("acmetest.example.com")?.unwrap(); 376 | assert_eq!(cert.private_key(), cert2.private_key()); 377 | assert_eq!(cert.certificate(), cert2.certificate()); 378 | assert_eq!(cert.valid_days_left(), 89); 379 | 380 | Ok(()) 381 | } 382 | } 383 | -------------------------------------------------------------------------------- /src/persist.rs: -------------------------------------------------------------------------------- 1 | //! Pluggable persistence. 2 | //! 3 | //! The persistence is a simple key-value store. The intention is to make it simple to implement 4 | //! other persistence mechanisms than the provided ones, such as against a databases. 5 | 6 | use std::collections::hash_map::{DefaultHasher, HashMap}; 7 | use std::fs; 8 | use std::hash::{Hash, Hasher}; 9 | use std::io::{Read, Write}; 10 | #[cfg(unix)] 11 | use std::os::unix::fs::OpenOptionsExt; 12 | use std::path::{Path, PathBuf}; 13 | use std::sync::{Arc, Mutex}; 14 | 15 | use crate::{Error, Result}; 16 | 17 | /// Kinds of [persistence keys](struct.PersistKey.html). 18 | #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] 19 | pub enum PersistKind { 20 | /// Persisted account private key. 21 | AccountPrivateKey, 22 | /// Persisted private key. 23 | PrivateKey, 24 | /// Persisted certificate. 25 | Certificate, 26 | } 27 | 28 | impl PersistKind { 29 | fn name(self) -> &'static str { 30 | match self { 31 | PersistKind::Certificate => "crt", 32 | PersistKind::PrivateKey => "key", 33 | PersistKind::AccountPrivateKey => "key", 34 | } 35 | } 36 | } 37 | 38 | /// Key for a value in the persistence. 39 | #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] 40 | pub struct PersistKey<'a> { 41 | pub realm: u64, 42 | pub kind: PersistKind, 43 | pub key: &'a str, 44 | } 45 | 46 | impl<'a> PersistKey<'a> { 47 | /// Create a new key under a "realm", kind and key. The realm is an opaque hash 48 | /// of the given realm string. 49 | /// 50 | /// The realm is in normally defined as the account contact email, however it depends 51 | /// on how the `Account` object is accessed, see [`account_with_realm`]. 52 | /// 53 | /// [`account_with_realm`]: ../struct.Directory.html#method.account_with_realm 54 | pub fn new(realm: &str, kind: PersistKind, key: &'a str) -> Self { 55 | let mut h = DefaultHasher::new(); 56 | realm.hash(&mut h); 57 | let realm = h.finish(); 58 | PersistKey { realm, kind, key } 59 | } 60 | } 61 | 62 | impl<'a> std::fmt::Display for PersistKey<'a> { 63 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 64 | write!( 65 | f, 66 | "{}_{}_{}", 67 | self.realm, 68 | self.kind.name(), 69 | self.key.replace('.', "_").replace('*', "STAR") 70 | ) 71 | } 72 | } 73 | 74 | /// Trait for a persistence implementation. 75 | /// 76 | /// Implementation must be clonable and thread safe (Send). This can easily be done by 77 | /// wrapping the implemetation an `Arc>`. 78 | pub trait Persist: Clone + Send { 79 | /// Store the given bytes under the given key. 80 | fn put(&self, key: &PersistKey, value: &[u8]) -> Result<()>; 81 | /// Read the bytes stored under the given key. 82 | /// 83 | /// `None` if the value doesn't exist. 84 | fn get(&self, key: &PersistKey) -> Result>>; 85 | } 86 | 87 | /// Memory implementation for dev/testing. 88 | /// 89 | /// The entries in memory are never saved to disk and are gone when the process dies. 90 | /// 91 | /// Since the API is [rate limited] it's not a good idea to use this in production code. 92 | /// 93 | /// [rate limited]: ../index.html#rate-limits 94 | #[derive(Clone, Default)] 95 | pub struct MemoryPersist { 96 | inner: Arc>>>, 97 | } 98 | 99 | impl MemoryPersist { 100 | /// Create a memory persistence for testing. 101 | pub fn new() -> Self { 102 | MemoryPersist { 103 | ..Default::default() 104 | } 105 | } 106 | } 107 | 108 | impl Persist for MemoryPersist { 109 | fn put(&self, key: &PersistKey, value: &[u8]) -> Result<()> { 110 | let mut lock = self.inner.lock().unwrap(); 111 | lock.insert(key.to_string(), value.to_owned()); 112 | Ok(()) 113 | } 114 | fn get(&self, key: &PersistKey) -> Result>> { 115 | let lock = self.inner.lock().unwrap(); 116 | Ok(lock.get(&key.to_string()).cloned()) 117 | } 118 | } 119 | 120 | /// Simple file persistence. 121 | /// 122 | /// Each key is saved under a unique filename. 123 | #[derive(Clone)] 124 | pub struct FilePersist { 125 | dir: PathBuf, 126 | } 127 | 128 | impl FilePersist { 129 | /// Create a file persistence in the directory pointed out by the `dir` given. 130 | /// 131 | /// The directory must be writable. 132 | pub fn new>(dir: P) -> Self { 133 | FilePersist { 134 | dir: dir.as_ref().to_path_buf(), 135 | } 136 | } 137 | } 138 | 139 | impl Persist for FilePersist { 140 | #[cfg(not(unix))] 141 | fn put(&self, key: &PersistKey, value: &[u8]) -> Result<()> { 142 | let f_name = file_name_of(&self.dir, &key); 143 | fs::write(f_name, value).map_err(Error::from) 144 | } 145 | 146 | #[cfg(unix)] 147 | fn put(&self, key: &PersistKey, value: &[u8]) -> Result<()> { 148 | let f_name = file_name_of(&self.dir, key); 149 | match key.kind { 150 | PersistKind::AccountPrivateKey | PersistKind::PrivateKey => fs::OpenOptions::new() 151 | .mode(0o600) 152 | .write(true) 153 | .truncate(true) 154 | .create(true) 155 | .open(f_name)? 156 | .write_all(value) 157 | .map_err(Error::from), 158 | PersistKind::Certificate => fs::write(f_name, value).map_err(Error::from), 159 | } 160 | } 161 | 162 | fn get(&self, key: &PersistKey) -> Result>> { 163 | let f_name = file_name_of(&self.dir, key); 164 | let ret = if let Ok(mut file) = fs::File::open(f_name) { 165 | let mut v = vec![]; 166 | file.read_to_end(&mut v)?; 167 | Some(v) 168 | } else { 169 | None 170 | }; 171 | Ok(ret) 172 | } 173 | } 174 | 175 | fn file_name_of(dir: &Path, key: &PersistKey) -> PathBuf { 176 | let mut f_name = dir.join(key.to_string()); 177 | f_name.set_extension(key.kind.name()); 178 | f_name 179 | } 180 | -------------------------------------------------------------------------------- /src/req.rs: -------------------------------------------------------------------------------- 1 | use ureq::{http, Body}; 2 | 3 | use crate::api::ApiProblem; 4 | 5 | pub(crate) type ReqResult = std::result::Result; 6 | 7 | const TIMEOUT_DURATION: std::time::Duration = std::time::Duration::from_secs(30); 8 | 9 | pub(crate) fn req_get(url: &str) -> Result, ureq::Error> { 10 | let req = ureq::get(url) 11 | .config() 12 | .timeout_global(Some(TIMEOUT_DURATION)) 13 | .http_status_as_error(false) 14 | .build(); 15 | trace!("{:?}", req); 16 | req.call() 17 | } 18 | 19 | pub(crate) fn req_head(url: &str) -> Result, ureq::Error> { 20 | let req = ureq::head(url) 21 | .config() 22 | .timeout_global(Some(TIMEOUT_DURATION)) 23 | .http_status_as_error(false) 24 | .build(); 25 | trace!("{:?}", req); 26 | req.call() 27 | } 28 | 29 | pub(crate) fn req_post(url: &str, body: &str) -> Result, ureq::Error> { 30 | let req = ureq::post(url) 31 | .header("content-type", "application/jose+json") 32 | .config() 33 | .timeout_global(Some(TIMEOUT_DURATION)) 34 | .http_status_as_error(false) 35 | .build(); 36 | trace!("{:?} {}", req, body); 37 | req.send(body) 38 | } 39 | 40 | pub(crate) fn req_handle_error( 41 | res: Result, ureq::Error>, 42 | ) -> ReqResult> { 43 | let res = match res { 44 | // ok responses pass through 45 | Ok(res) => res, 46 | Err(e) => { 47 | return Err(ApiProblem { 48 | _type: "httpReqError".into(), 49 | detail: Some(e.to_string()), 50 | subproblems: None, 51 | }) 52 | } 53 | }; 54 | 55 | if res.status().is_success() { 56 | return Ok(res); 57 | } 58 | 59 | let problem = if res.body().mime_type() == Some("application/problem+json") { 60 | // if we were sent a problem+json, deserialize it 61 | let body = req_safe_read_body(res); 62 | serde_json::from_str(&body).unwrap_or_else(|e| ApiProblem { 63 | _type: "problemJsonFail".into(), 64 | detail: Some(format!( 65 | "Failed to deserialize application/problem+json ({}) body: {}", 66 | e.to_string(), 67 | body 68 | )), 69 | subproblems: None, 70 | }) 71 | } else { 72 | // some other problem 73 | let status = format!( 74 | "{} {}", 75 | res.status(), 76 | res.status().canonical_reason().unwrap_or_default() 77 | ); 78 | let body = req_safe_read_body(res); 79 | let detail = format!("{} body: {}", status, body); 80 | ApiProblem { 81 | _type: "httpReqError".into(), 82 | detail: Some(detail), 83 | subproblems: None, 84 | } 85 | }; 86 | 87 | Err(problem) 88 | } 89 | 90 | pub(crate) fn req_expect_header(res: &http::Response, name: &str) -> ReqResult { 91 | res.headers() 92 | .get(name) 93 | .map(|v| v.to_str().unwrap_or_default()) 94 | .ok_or_else(|| ApiProblem { 95 | _type: format!("Missing header: {}", name), 96 | detail: None, 97 | subproblems: None, 98 | }) 99 | .map(|v| v.to_string()) 100 | } 101 | 102 | pub(crate) fn req_safe_read_body(mut res: http::Response) -> String { 103 | // letsencrypt sometimes closes the TLS abruptly causing io error 104 | // even though we did capture the body. 105 | let res_body = res.body_mut().read_to_string().unwrap_or_default(); 106 | res_body 107 | } 108 | -------------------------------------------------------------------------------- /src/test/mod.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::trivial_regex)] 2 | 3 | use futures::Future; 4 | use hyper::{service::service_fn_ok, Body, Method, Request, Response, Server}; 5 | use lazy_static::lazy_static; 6 | use std::net::TcpListener; 7 | use std::thread; 8 | 9 | lazy_static! { 10 | static ref RE_URL: regex::Regex = regex::Regex::new("").unwrap(); 11 | } 12 | 13 | pub struct TestServer { 14 | pub dir_url: String, 15 | shutdown: Option>, 16 | } 17 | 18 | impl Drop for TestServer { 19 | fn drop(&mut self) { 20 | self.shutdown.take().unwrap().send(()).ok(); 21 | } 22 | } 23 | 24 | fn get_directory(url: &str) -> Response { 25 | const BODY: &str = r#"{ 26 | "keyChange": "/acme/key-change", 27 | "newAccount": "/acme/new-acct", 28 | "newNonce": "/acme/new-nonce", 29 | "newOrder": "/acme/new-order", 30 | "revokeCert": "/acme/revoke-cert", 31 | "meta": { 32 | "caaIdentities": [ 33 | "testdir.org" 34 | ] 35 | } 36 | }"#; 37 | Response::new(Body::from(RE_URL.replace_all(BODY, url))) 38 | } 39 | 40 | fn head_new_nonce() -> Response { 41 | Response::builder() 42 | .status(204) 43 | .header( 44 | "Replay-Nonce", 45 | "8_uBBV3N2DBRJczhoiB46ugJKUkUHxGzVe6xIMpjHFM", 46 | ) 47 | .body(Body::empty()) 48 | .unwrap() 49 | } 50 | 51 | fn post_new_acct(url: &str) -> Response { 52 | const BODY: &str = r#"{ 53 | "id": 7728515, 54 | "key": { 55 | "use": "sig", 56 | "kty": "EC", 57 | "crv": "P-256", 58 | "alg": "ES256", 59 | "x": "ttpobTRK2bw7ttGBESRO7Nb23mbIRfnRZwunL1W6wRI", 60 | "y": "h2Z00J37_2qRKH0-flrHEsH0xbit915Tyvd2v_CAOSk" 61 | }, 62 | "contact": [ 63 | "mailto:foo@bar.com" 64 | ], 65 | "initialIp": "90.171.37.12", 66 | "createdAt": "2018-12-31T17:15:40.399104457Z", 67 | "status": "valid" 68 | }"#; 69 | let location: String = RE_URL.replace_all("/acme/acct/7728515", url).into(); 70 | Response::builder() 71 | .status(201) 72 | .header("Location", location) 73 | .body(Body::from(BODY)) 74 | .unwrap() 75 | } 76 | 77 | fn post_new_order(url: &str) -> Response { 78 | const BODY: &str = r#"{ 79 | "status": "pending", 80 | "expires": "2019-01-09T08:26:43.570360537Z", 81 | "identifiers": [ 82 | { 83 | "type": "dns", 84 | "value": "acmetest.example.com" 85 | } 86 | ], 87 | "authorizations": [ 88 | "/acme/authz/YTqpYUthlVfwBncUufE8IRWLMSRqcSs" 89 | ], 90 | "finalize": "/acme/finalize/7738992/18234324" 91 | }"#; 92 | let location: String = RE_URL 93 | .replace_all("/acme/order/YTqpYUthlVfwBncUufE8", url) 94 | .into(); 95 | Response::builder() 96 | .status(201) 97 | .header("Location", location) 98 | .body(Body::from(RE_URL.replace_all(BODY, url))) 99 | .unwrap() 100 | } 101 | 102 | fn post_get_order(url: &str) -> Response { 103 | const BODY: &str = r#"{ 104 | "status": "", 105 | "expires": "2019-01-09T08:26:43.570360537Z", 106 | "identifiers": [ 107 | { 108 | "type": "dns", 109 | "value": "acmetest.example.com" 110 | } 111 | ], 112 | "authorizations": [ 113 | "/acme/authz/YTqpYUthlVfwBncUufE8IRWLMSRqcSs" 114 | ], 115 | "finalize": "/acme/finalize/7738992/18234324", 116 | "certificate": "/acme/cert/fae41c070f967713109028" 117 | }"#; 118 | let b = RE_URL.replace_all(BODY, url).to_string(); 119 | Response::builder().status(200).body(Body::from(b)).unwrap() 120 | } 121 | 122 | fn post_authz(url: &str) -> Response { 123 | const BODY: &str = r#"{ 124 | "identifier": { 125 | "type": "dns", 126 | "value": "acmetest.algesten.se" 127 | }, 128 | "status": "pending", 129 | "expires": "2019-01-09T08:26:43Z", 130 | "challenges": [ 131 | { 132 | "type": "http-01", 133 | "status": "pending", 134 | "url": "/acme/challenge/YTqpYUthlVfwBncUufE8IRWLMSRqcSs/216789597", 135 | "token": "MUi-gqeOJdRkSb_YR2eaMxQBqf6al8dgt_dOttSWb0w" 136 | }, 137 | { 138 | "type": "tls-alpn-01", 139 | "status": "pending", 140 | "url": "/acme/challenge/YTqpYUthlVfwBncUufE8IRWLMSRqcSs/216789598", 141 | "token": "WCdRWkCy4THTD_j5IH4ISAzr59lFIg5wzYmKxuOJ1lU" 142 | }, 143 | { 144 | "type": "dns-01", 145 | "status": "pending", 146 | "url": "/acme/challenge/YTqpYUthlVfwBncUufE8IRWLMSRqcSs/216789599", 147 | "token": "RRo2ZcXAEqxKvMH8RGcATjSK1KknLEUmauwfQ5i3gG8" 148 | } 149 | ] 150 | }"#; 151 | Response::builder() 152 | .status(201) 153 | .body(Body::from(RE_URL.replace_all(BODY, url))) 154 | .unwrap() 155 | } 156 | 157 | fn post_finalize(_url: &str) -> Response { 158 | Response::builder().status(200).body(Body::empty()).unwrap() 159 | } 160 | 161 | fn post_certificate(_url: &str) -> Response { 162 | Response::builder() 163 | .status(200) 164 | .body("CERT HERE".into()) 165 | .unwrap() 166 | } 167 | 168 | fn route_request(req: Request, url: &str) -> Response { 169 | match (req.method(), req.uri().path()) { 170 | (&Method::GET, "/directory") => get_directory(url), 171 | (&Method::HEAD, "/acme/new-nonce") => head_new_nonce(), 172 | (&Method::POST, "/acme/new-acct") => post_new_acct(url), 173 | (&Method::POST, "/acme/new-order") => post_new_order(url), 174 | (&Method::POST, "/acme/order/YTqpYUthlVfwBncUufE8") => post_get_order(url), 175 | (&Method::POST, "/acme/authz/YTqpYUthlVfwBncUufE8IRWLMSRqcSs") => post_authz(url), 176 | (&Method::POST, "/acme/finalize/7738992/18234324") => post_finalize(url), 177 | (&Method::POST, "/acme/cert/fae41c070f967713109028") => post_certificate(url), 178 | (_, _) => Response::builder().status(404).body(Body::empty()).unwrap(), 179 | } 180 | } 181 | 182 | pub fn with_directory_server() -> TestServer { 183 | let tcp = TcpListener::bind("127.0.0.1:0").unwrap(); 184 | let port = tcp.local_addr().unwrap().port(); 185 | 186 | let url = format!("http://127.0.0.1:{}", port); 187 | let dir_url = format!("{}/directory", url); 188 | 189 | let make_service = move || { 190 | let url2 = url.clone(); 191 | service_fn_ok(move |req| route_request(req, &url2)) 192 | }; 193 | let server = Server::from_tcp(tcp).unwrap().serve(make_service); 194 | 195 | let (tx, rx) = futures::sync::oneshot::channel::<()>(); 196 | 197 | let graceful = server 198 | .with_graceful_shutdown(rx) 199 | .map_err(|err| eprintln!("server error: {}", err)); 200 | 201 | thread::spawn(move || { 202 | hyper::rt::run(graceful); 203 | }); 204 | 205 | TestServer { 206 | dir_url, 207 | shutdown: Some(tx), 208 | } 209 | } 210 | 211 | #[test] 212 | pub fn test_make_directory() { 213 | let server = with_directory_server(); 214 | let res = ureq::get(&server.dir_url).call(); 215 | assert!(res.is_ok()); 216 | } 217 | -------------------------------------------------------------------------------- /src/trans.rs: -------------------------------------------------------------------------------- 1 | use openssl::ecdsa::EcdsaSig; 2 | use openssl::sha::sha256; 3 | use serde::Serialize; 4 | use std::collections::VecDeque; 5 | use std::sync::{Arc, Mutex}; 6 | use ureq::{http, Body}; 7 | 8 | use crate::acc::AcmeKey; 9 | use crate::jwt::*; 10 | use crate::req::{req_expect_header, req_handle_error, req_head, req_post}; 11 | use crate::util::base64url; 12 | use crate::Result; 13 | 14 | /// JWS payload and nonce handling for requests to the API. 15 | /// 16 | /// Setup is: 17 | /// 18 | /// 1. `Transport::new()` 19 | /// 2. `call_jwk()` against newAccount url 20 | /// 3. `set_key_id` from the returned `Location` header. 21 | /// 4. `call()` for all calls after that. 22 | #[derive(Clone, Debug)] 23 | pub(crate) struct Transport { 24 | acme_key: AcmeKey, 25 | nonce_pool: Arc, 26 | } 27 | 28 | impl Transport { 29 | pub fn new(nonce_pool: &Arc, acme_key: AcmeKey) -> Self { 30 | Transport { 31 | acme_key, 32 | nonce_pool: nonce_pool.clone(), 33 | } 34 | } 35 | 36 | /// Update the key id once it is known (part of setting up the transport). 37 | pub fn set_key_id(&mut self, kid: String) { 38 | self.acme_key.set_key_id(kid); 39 | } 40 | 41 | /// The key used in the transport 42 | pub fn acme_key(&self) -> &AcmeKey { 43 | &self.acme_key 44 | } 45 | 46 | /// Make call using the full jwk. Only for the first newAccount request. 47 | pub fn call_jwk( 48 | &self, 49 | url: &str, 50 | body: &T, 51 | ) -> Result> { 52 | self.do_call(url, body, jws_with_jwk) 53 | } 54 | 55 | /// Make call using the key id 56 | pub fn call(&self, url: &str, body: &T) -> Result> { 57 | self.do_call(url, body, jws_with_kid) 58 | } 59 | 60 | fn do_call Result>( 61 | &self, 62 | url: &str, 63 | body: &T, 64 | make_body: F, 65 | ) -> Result> { 66 | // The ACME API may at any point invalidate all nonces. If we detect such an 67 | // error, we loop until the server accepts the nonce. 68 | loop { 69 | // Either get a new nonce, or reuse one from a previous request. 70 | let nonce = self.nonce_pool.get_nonce()?; 71 | 72 | // Sign the body. 73 | let body = make_body(url, nonce, &self.acme_key, body)?; 74 | 75 | debug!("Call endpoint {}", url); 76 | 77 | // Post it to the URL 78 | let response = req_post(url, &body); 79 | 80 | // Regardless of the request being a success or not, there might be 81 | // a nonce in the response. 82 | self.nonce_pool.extract_nonce(&response); 83 | 84 | // Turn errors into ApiProblem. 85 | let result = req_handle_error(response); 86 | 87 | if let Err(problem) = &result { 88 | if problem.is_bad_nonce() { 89 | // retry the request with a new nonce. 90 | debug!("Retrying on bad nonce"); 91 | continue; 92 | } 93 | // it seems we sometimes make bad JWTs. Why?! 94 | if problem.is_jwt_verification_error() { 95 | debug!("Retrying on: {}", problem); 96 | continue; 97 | } 98 | } 99 | 100 | return Ok(result?); 101 | } 102 | } 103 | } 104 | 105 | /// Shared pool of nonces. 106 | #[derive(Default, Debug)] 107 | pub(crate) struct NoncePool { 108 | nonce_url: String, 109 | pool: Mutex>, 110 | } 111 | 112 | impl NoncePool { 113 | pub fn new(nonce_url: &str) -> Self { 114 | NoncePool { 115 | nonce_url: nonce_url.into(), 116 | ..Default::default() 117 | } 118 | } 119 | 120 | fn extract_nonce(&self, res: &std::result::Result, ureq::Error>) { 121 | let res = match res { 122 | Ok(res) => res, 123 | _ => return, 124 | }; 125 | 126 | if let Some(nonce) = res.headers().get("replay-nonce") { 127 | trace!("Extract nonce"); 128 | let mut pool = self.pool.lock().unwrap(); 129 | let nonce = match nonce.to_str() { 130 | Ok(v) => v, 131 | _ => return, 132 | }; 133 | pool.push_back(nonce.to_string()); 134 | if pool.len() > 10 { 135 | pool.pop_front(); 136 | } 137 | } 138 | } 139 | 140 | fn get_nonce(&self) -> Result { 141 | { 142 | let mut pool = self.pool.lock().unwrap(); 143 | if let Some(nonce) = pool.pop_front() { 144 | trace!("Use previous nonce"); 145 | return Ok(nonce); 146 | } 147 | } 148 | debug!("Request new nonce"); 149 | let res = req_head(&self.nonce_url); 150 | 151 | let res = match res { 152 | Ok(res) => res, 153 | Err(e) => { 154 | return Err(crate::Error::ApiProblem(crate::api::ApiProblem { 155 | _type: "httpReqError".into(), 156 | detail: Some(e.to_string()), 157 | subproblems: None, 158 | })) 159 | } 160 | }; 161 | 162 | Ok(req_expect_header(&res, "replay-nonce")?) 163 | } 164 | } 165 | 166 | fn jws_with_kid( 167 | url: &str, 168 | nonce: String, 169 | key: &AcmeKey, 170 | payload: &T, 171 | ) -> Result { 172 | let protected = JwsProtected::new_kid(key.key_id(), url, nonce); 173 | jws_with(protected, key, payload) 174 | } 175 | 176 | fn jws_with_jwk( 177 | url: &str, 178 | nonce: String, 179 | key: &AcmeKey, 180 | payload: &T, 181 | ) -> Result { 182 | let jwk: Jwk = key.into(); 183 | let protected = JwsProtected::new_jwk(jwk, url, nonce); 184 | jws_with(protected, key, payload) 185 | } 186 | 187 | fn jws_with( 188 | protected: JwsProtected, 189 | key: &AcmeKey, 190 | payload: &T, 191 | ) -> Result { 192 | let protected = { 193 | let pro_json = serde_json::to_string(&protected)?; 194 | base64url(pro_json.as_bytes()) 195 | }; 196 | let payload = { 197 | let pay_json = serde_json::to_string(payload)?; 198 | if pay_json == "\"\"" { 199 | // This is a special case produced by ApiEmptyString and should 200 | // not be further base64url encoded. 201 | "".to_string() 202 | } else { 203 | base64url(pay_json.as_bytes()) 204 | } 205 | }; 206 | 207 | let to_sign = format!("{}.{}", protected, payload); 208 | let digest = sha256(to_sign.as_bytes()); 209 | let sig = EcdsaSig::sign(&digest, key.private_key()).expect("EcdsaSig::sign"); 210 | let r = sig.r().to_vec(); 211 | let s = sig.s().to_vec(); 212 | 213 | let mut v = Vec::with_capacity(r.len() + s.len()); 214 | v.extend_from_slice(&r); 215 | v.extend_from_slice(&s); 216 | let signature = base64url(&v); 217 | 218 | let jws = Jws::new(protected, payload, signature); 219 | 220 | Ok(serde_json::to_string(&jws)?) 221 | } 222 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use base64::Engine; 2 | use serde::de::DeserializeOwned; 3 | use ureq::{http, Body}; 4 | 5 | use crate::req::req_safe_read_body; 6 | use crate::Result; 7 | 8 | pub(crate) fn base64url>(input: &T) -> String { 9 | base64::prelude::BASE64_URL_SAFE_NO_PAD.encode(input) 10 | } 11 | 12 | pub(crate) fn read_json(res: http::Response) -> Result { 13 | let res_body = req_safe_read_body(res); 14 | debug!("{}", res_body); 15 | Ok(serde_json::from_str(&res_body)?) 16 | } 17 | --------------------------------------------------------------------------------