+++ /dev/null
-/target
-Cargo.lock
+++ /dev/null
-[package]
-name = "proxmox-acme-rs"
-version = "0.4.0"
-authors = ["Wolfgang Bumiller <w.bumiller@proxmox.com>"]
-edition = "2021"
-license = "AGPL-3"
-description = "ACME client library"
-exclude = [
- "build",
- "debian",
-]
-
-[dependencies]
-base64 = "0.13.0"
-serde = { version = "1.0", features = ["derive"] }
-serde_json = "1.0"
-openssl = "0.10.29"
-
-# For the client
-native-tls = { version = "0.2", optional = true }
-
-[dependencies.ureq]
-optional = true
-version = "2.4"
-default-features = false
-features = [ "native-tls", "gzip" ]
-
-[features]
-default = []
-client = ["ureq", "native-tls"]
-
-[dev-dependencies]
-anyhow = "1.0"
+++ /dev/null
-.PHONY: all
-all: check
-
-.PHONY: check
-check:
- cargo test --all-features
-
-.PHONY: dinstall
-dinstall: deb
- sudo -k dpkg -i build/librust-*.deb
-
-.PHONY: build
-build:
- rm -rf build
- rm -f debian/control
- mkdir build
- debcargo package \
- --config "$(PWD)/debian/debcargo.toml" \
- --changelog-ready \
- --no-overlay-write-back \
- --directory "$(PWD)/build/proxmox-acme-rs" \
- "proxmox-acme-rs" \
- "$$(dpkg-parsechangelog -l "debian/changelog" -SVersion | sed -e 's/-.*//')"
- echo system >build/rust-toolchain
- rm -f build/proxmox-acme-rs/Cargo.lock
- find build/proxmox-acme-rs/debian -name '*.hint' -delete
- cp build/proxmox-acme-rs/debian/control debian/control
-
-.PHONY: deb
-deb: build
- (cd build/proxmox-acme-rs && CARGO=/usr/bin/cargo RUSTC=/usr/bin/rustc dpkg-buildpackage -b -uc -us)
- lintian build/*.deb
-
-.PHONY: clean
-clean:
- rm -rf build *.deb *.buildinfo *.changes *.orig.tar.gz
- cargo clean
-
-upload: deb
- cd build; \
- dcmd --deb rust-proxmox-acme-rs_*.changes \
- | grep -v '.changes$$' \
- | tar -cf "rust-proxmox-acme-rs-debs.tar" -T-; \
- cat "rust-proxmox-acme-rs-debs.tar" | ssh -X repoman@repo.proxmox.com upload --product devel --dist bullseye; \
- rm -f rust-proxmox-acme-rs-debs.tar
+++ /dev/null
-rust-proxmox-acme-rs (0.4.0) pve; urgency=medium
-
- * switch from curl to ureq with native-tls
-
- * bump edition to 2021
-
- -- Proxmox Support Team <support@proxmox.com> Tue, 01 Feb 2022 10:19:29 +0100
-
-rust-proxmox-acme-rs (0.3.2) pve; urgency=medium
-
- * rebuild with base64 0.13
-
- -- Proxmox Support Team <support@proxmox.com> Thu, 18 Nov 2021 12:49:25 +0100
-
-rust-proxmox-acme-rs (0.3.1) pve; urgency=medium
-
- * add proxy support
-
- -- Proxmox Support Team <support@proxmox.com> Thu, 18 Nov 2021 09:46:34 +0100
-
-rust-proxmox-acme-rs (0.3.0) pve; urgency=medium
-
- * directory: make metadata optional
-
- -- Proxmox Support Team <support@proxmox.com> Thu, 21 Oct 2021 13:10:27 +0200
-
-rust-proxmox-acme-rs (0.2.2-1) pve; urgency=medium
-
- * improve crate documentation
-
- * mark `Error` as 'must_use'
-
- * make status types `Copy`
-
- * add Client::directory_url() to get the URL without querying the whole
- directory
-
- -- Proxmox Support Team <support@proxmox.com> Fri, 07 May 2021 13:53:08 +0200
-
-rust-proxmox-acme-rs (0.2.1-1) pve; urgency=medium
-
- * make revocation workflow accessible without client
-
- -- Proxmox Support Team <support@proxmox.com> Wed, 14 Apr 2021 14:56:49 +0200
-
-rust-proxmox-acme-rs (0.2.0-1) pve; urgency=medium
-
- * add 'status' and 'url' as fixed members to `Challenge`
-
- * expose some workflow helpers in a more consistentw ay
-
- * add `util::Csr` for CSR generation
-
- -- Proxmox Support Team <support@proxmox.com> Mon, 12 Apr 2021 13:06:19 +0200
-
-rust-proxmox-acme-rs (0.1.4-1) pve; urgency=medium
-
- * collect extra account fields (such as 'created' from let's encrypt)
- in the AccountData struct
-
- -- Proxmox Support Team <support@proxmox.com> Wed, 17 Mar 2021 15:28:09 +0100
-
-rust-proxmox-acme-rs (0.1.3-1) pve; urgency=medium
-
- * fix padding in ecdsa signatures
-
- -- Proxmox Support Team <support@proxmox.com> Wed, 17 Mar 2021 13:34:10 +0100
-
-rust-proxmox-acme-rs (0.1.2-1) pve; urgency=medium
-
- * include Content-length header in requests
-
- -- Proxmox Support Team <support@proxmox.com> Fri, 12 Mar 2021 15:43:01 +0100
-
-rust-proxmox-acme-rs (0.1.1-1) pve; urgency=medium
-
- * make AccountData fields public
-
- -- Proxmox Support Team <support@proxmox.com> Tue, 09 Mar 2021 13:22:55 +0100
-
-rust-proxmox-acme-rs (0.1.0-1) pve; urgency=medium
-
- * initial release
-
- -- Proxmox Support Team <support@proxmox.com> Tue, 09 Mar 2021 13:01:56 +0100
+++ /dev/null
-Source: rust-proxmox-acme-rs
-Section: rust
-Priority: optional
-Build-Depends: debhelper (>= 12),
- dh-cargo (>= 25),
- cargo:native <!nocheck>,
- rustc:native <!nocheck>,
- libstd-rust-dev <!nocheck>,
- librust-base64-0.13+default-dev <!nocheck>,
- librust-openssl-0.10+default-dev (>= 0.10.29-~~) <!nocheck>,
- librust-serde-1+default-dev <!nocheck>,
- librust-serde-1+derive-dev <!nocheck>,
- librust-serde-json-1+default-dev <!nocheck>
-Maintainer: Proxmox Support Team <support@proxmox.com>
-Standards-Version: 4.6.1
-Vcs-Git:
-Vcs-Browser:
-X-Cargo-Crate: proxmox-acme-rs
-Rules-Requires-Root: no
-
-Package: librust-proxmox-acme-rs-dev
-Architecture: any
-Multi-Arch: same
-Depends:
- ${misc:Depends},
- librust-base64-0.13+default-dev,
- librust-openssl-0.10+default-dev (>= 0.10.29-~~),
- librust-serde-1+default-dev,
- librust-serde-1+derive-dev,
- librust-serde-json-1+default-dev
-Suggests:
- librust-proxmox-acme-rs+client-dev (= ${binary:Version}),
- librust-proxmox-acme-rs+native-tls-dev (= ${binary:Version}),
- librust-proxmox-acme-rs+ureq-dev (= ${binary:Version})
-Provides:
- librust-proxmox-acme-rs+default-dev (= ${binary:Version}),
- librust-proxmox-acme-rs-0-dev (= ${binary:Version}),
- librust-proxmox-acme-rs-0+default-dev (= ${binary:Version}),
- librust-proxmox-acme-rs-0.4-dev (= ${binary:Version}),
- librust-proxmox-acme-rs-0.4+default-dev (= ${binary:Version}),
- librust-proxmox-acme-rs-0.4.0-dev (= ${binary:Version}),
- librust-proxmox-acme-rs-0.4.0+default-dev (= ${binary:Version})
-Description: ACME client library - Rust source code
- This package contains the source for the Rust proxmox-acme-rs crate, packaged
- by debcargo for use with cargo and dh-cargo.
-
-Package: librust-proxmox-acme-rs+client-dev
-Architecture: any
-Multi-Arch: same
-Depends:
- ${misc:Depends},
- librust-proxmox-acme-rs-dev (= ${binary:Version}),
- librust-proxmox-acme-rs+ureq-dev (= ${binary:Version}),
- librust-proxmox-acme-rs+native-tls-dev (= ${binary:Version})
-Provides:
- librust-proxmox-acme-rs-0+client-dev (= ${binary:Version}),
- librust-proxmox-acme-rs-0.4+client-dev (= ${binary:Version}),
- librust-proxmox-acme-rs-0.4.0+client-dev (= ${binary:Version})
-Description: ACME client library - feature "client"
- This metapackage enables feature "client" for the Rust proxmox-acme-rs crate,
- by pulling in any additional dependencies needed by that feature.
-
-Package: librust-proxmox-acme-rs+native-tls-dev
-Architecture: any
-Multi-Arch: same
-Depends:
- ${misc:Depends},
- librust-proxmox-acme-rs-dev (= ${binary:Version}),
- librust-native-tls-0.2+default-dev
-Provides:
- librust-proxmox-acme-rs-0+native-tls-dev (= ${binary:Version}),
- librust-proxmox-acme-rs-0.4+native-tls-dev (= ${binary:Version}),
- librust-proxmox-acme-rs-0.4.0+native-tls-dev (= ${binary:Version})
-Description: ACME client library - feature "native-tls"
- This metapackage enables feature "native-tls" for the Rust proxmox-acme-rs
- crate, by pulling in any additional dependencies needed by that feature.
-
-Package: librust-proxmox-acme-rs+ureq-dev
-Architecture: any
-Multi-Arch: same
-Depends:
- ${misc:Depends},
- librust-proxmox-acme-rs-dev (= ${binary:Version}),
- librust-ureq-2+gzip-dev (>= 2.4-~~),
- librust-ureq-2+native-tls-dev (>= 2.4-~~)
-Provides:
- librust-proxmox-acme-rs-0+ureq-dev (= ${binary:Version}),
- librust-proxmox-acme-rs-0.4+ureq-dev (= ${binary:Version}),
- librust-proxmox-acme-rs-0.4.0+ureq-dev (= ${binary:Version})
-Description: ACME client library - feature "ureq"
- This metapackage enables feature "ureq" for the Rust proxmox-acme-rs crate, by
- pulling in any additional dependencies needed by that feature.
+++ /dev/null
-Copyright (C) 2020-2021 Proxmox Server Solutions GmbH
-
-This software is written by Proxmox Server Solutions GmbH <support@proxmox.com>
-
-This program is free software: you can redistribute it and/or modify
-it under the terms of the GNU Affero General Public License as published by
-the Free Software Foundation, either version 3 of the License, or
-(at your option) any later version.
-
-This program is distributed in the hope that it will be useful,
-but WITHOUT ANY WARRANTY; without even the implied warranty of
-MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-GNU Affero General Public License for more details.
-
-You should have received a copy of the GNU Affero General Public License
-along with this program. If not, see <http://www.gnu.org/licenses/>.
+++ /dev/null
-overlay = "."
-crate_src_path = ".."
-maintainer = "Proxmox Support Team <support@proxmox.com>"
-
-[source]
-# TODO: update once public
-vcs_git = ""
-vcs_browser = ""
+++ /dev/null
-3.0 (native)
--- /dev/null
+[package]
+name = "proxmox-acme-rs"
+version = "0.4.0"
+authors = ["Wolfgang Bumiller <w.bumiller@proxmox.com>"]
+edition = "2021"
+license = "AGPL-3"
+description = "ACME client library"
+exclude = [
+ "build",
+ "debian",
+]
+
+[dependencies]
+base64 = "0.13.0"
+serde = { version = "1.0", features = ["derive"] }
+serde_json = "1.0"
+openssl = "0.10.29"
+
+# For the client
+native-tls = { version = "0.2", optional = true }
+
+[dependencies.ureq]
+optional = true
+version = "2.4"
+default-features = false
+features = [ "native-tls", "gzip" ]
+
+[features]
+default = []
+client = ["ureq", "native-tls"]
+
+[dev-dependencies]
+anyhow = "1.0"
--- /dev/null
+rust-proxmox-acme-rs (0.4.0) pve; urgency=medium
+
+ * switch from curl to ureq with native-tls
+
+ * bump edition to 2021
+
+ -- Proxmox Support Team <support@proxmox.com> Tue, 01 Feb 2022 10:19:29 +0100
+
+rust-proxmox-acme-rs (0.3.2) pve; urgency=medium
+
+ * rebuild with base64 0.13
+
+ -- Proxmox Support Team <support@proxmox.com> Thu, 18 Nov 2021 12:49:25 +0100
+
+rust-proxmox-acme-rs (0.3.1) pve; urgency=medium
+
+ * add proxy support
+
+ -- Proxmox Support Team <support@proxmox.com> Thu, 18 Nov 2021 09:46:34 +0100
+
+rust-proxmox-acme-rs (0.3.0) pve; urgency=medium
+
+ * directory: make metadata optional
+
+ -- Proxmox Support Team <support@proxmox.com> Thu, 21 Oct 2021 13:10:27 +0200
+
+rust-proxmox-acme-rs (0.2.2-1) pve; urgency=medium
+
+ * improve crate documentation
+
+ * mark `Error` as 'must_use'
+
+ * make status types `Copy`
+
+ * add Client::directory_url() to get the URL without querying the whole
+ directory
+
+ -- Proxmox Support Team <support@proxmox.com> Fri, 07 May 2021 13:53:08 +0200
+
+rust-proxmox-acme-rs (0.2.1-1) pve; urgency=medium
+
+ * make revocation workflow accessible without client
+
+ -- Proxmox Support Team <support@proxmox.com> Wed, 14 Apr 2021 14:56:49 +0200
+
+rust-proxmox-acme-rs (0.2.0-1) pve; urgency=medium
+
+ * add 'status' and 'url' as fixed members to `Challenge`
+
+ * expose some workflow helpers in a more consistentw ay
+
+ * add `util::Csr` for CSR generation
+
+ -- Proxmox Support Team <support@proxmox.com> Mon, 12 Apr 2021 13:06:19 +0200
+
+rust-proxmox-acme-rs (0.1.4-1) pve; urgency=medium
+
+ * collect extra account fields (such as 'created' from let's encrypt)
+ in the AccountData struct
+
+ -- Proxmox Support Team <support@proxmox.com> Wed, 17 Mar 2021 15:28:09 +0100
+
+rust-proxmox-acme-rs (0.1.3-1) pve; urgency=medium
+
+ * fix padding in ecdsa signatures
+
+ -- Proxmox Support Team <support@proxmox.com> Wed, 17 Mar 2021 13:34:10 +0100
+
+rust-proxmox-acme-rs (0.1.2-1) pve; urgency=medium
+
+ * include Content-length header in requests
+
+ -- Proxmox Support Team <support@proxmox.com> Fri, 12 Mar 2021 15:43:01 +0100
+
+rust-proxmox-acme-rs (0.1.1-1) pve; urgency=medium
+
+ * make AccountData fields public
+
+ -- Proxmox Support Team <support@proxmox.com> Tue, 09 Mar 2021 13:22:55 +0100
+
+rust-proxmox-acme-rs (0.1.0-1) pve; urgency=medium
+
+ * initial release
+
+ -- Proxmox Support Team <support@proxmox.com> Tue, 09 Mar 2021 13:01:56 +0100
--- /dev/null
+Source: rust-proxmox-acme-rs
+Section: rust
+Priority: optional
+Build-Depends: debhelper (>= 12),
+ dh-cargo (>= 25),
+ cargo:native <!nocheck>,
+ rustc:native <!nocheck>,
+ libstd-rust-dev <!nocheck>,
+ librust-base64-0.13+default-dev <!nocheck>,
+ librust-openssl-0.10+default-dev (>= 0.10.29-~~) <!nocheck>,
+ librust-serde-1+default-dev <!nocheck>,
+ librust-serde-1+derive-dev <!nocheck>,
+ librust-serde-json-1+default-dev <!nocheck>
+Maintainer: Proxmox Support Team <support@proxmox.com>
+Standards-Version: 4.6.1
+Vcs-Git:
+Vcs-Browser:
+X-Cargo-Crate: proxmox-acme-rs
+Rules-Requires-Root: no
+
+Package: librust-proxmox-acme-rs-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-base64-0.13+default-dev,
+ librust-openssl-0.10+default-dev (>= 0.10.29-~~),
+ librust-serde-1+default-dev,
+ librust-serde-1+derive-dev,
+ librust-serde-json-1+default-dev
+Suggests:
+ librust-proxmox-acme-rs+client-dev (= ${binary:Version}),
+ librust-proxmox-acme-rs+native-tls-dev (= ${binary:Version}),
+ librust-proxmox-acme-rs+ureq-dev (= ${binary:Version})
+Provides:
+ librust-proxmox-acme-rs+default-dev (= ${binary:Version}),
+ librust-proxmox-acme-rs-0-dev (= ${binary:Version}),
+ librust-proxmox-acme-rs-0+default-dev (= ${binary:Version}),
+ librust-proxmox-acme-rs-0.4-dev (= ${binary:Version}),
+ librust-proxmox-acme-rs-0.4+default-dev (= ${binary:Version}),
+ librust-proxmox-acme-rs-0.4.0-dev (= ${binary:Version}),
+ librust-proxmox-acme-rs-0.4.0+default-dev (= ${binary:Version})
+Description: ACME client library - Rust source code
+ This package contains the source for the Rust proxmox-acme-rs crate, packaged
+ by debcargo for use with cargo and dh-cargo.
+
+Package: librust-proxmox-acme-rs+client-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-proxmox-acme-rs-dev (= ${binary:Version}),
+ librust-proxmox-acme-rs+ureq-dev (= ${binary:Version}),
+ librust-proxmox-acme-rs+native-tls-dev (= ${binary:Version})
+Provides:
+ librust-proxmox-acme-rs-0+client-dev (= ${binary:Version}),
+ librust-proxmox-acme-rs-0.4+client-dev (= ${binary:Version}),
+ librust-proxmox-acme-rs-0.4.0+client-dev (= ${binary:Version})
+Description: ACME client library - feature "client"
+ This metapackage enables feature "client" for the Rust proxmox-acme-rs crate,
+ by pulling in any additional dependencies needed by that feature.
+
+Package: librust-proxmox-acme-rs+native-tls-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-proxmox-acme-rs-dev (= ${binary:Version}),
+ librust-native-tls-0.2+default-dev
+Provides:
+ librust-proxmox-acme-rs-0+native-tls-dev (= ${binary:Version}),
+ librust-proxmox-acme-rs-0.4+native-tls-dev (= ${binary:Version}),
+ librust-proxmox-acme-rs-0.4.0+native-tls-dev (= ${binary:Version})
+Description: ACME client library - feature "native-tls"
+ This metapackage enables feature "native-tls" for the Rust proxmox-acme-rs
+ crate, by pulling in any additional dependencies needed by that feature.
+
+Package: librust-proxmox-acme-rs+ureq-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-proxmox-acme-rs-dev (= ${binary:Version}),
+ librust-ureq-2+gzip-dev (>= 2.4-~~),
+ librust-ureq-2+native-tls-dev (>= 2.4-~~)
+Provides:
+ librust-proxmox-acme-rs-0+ureq-dev (= ${binary:Version}),
+ librust-proxmox-acme-rs-0.4+ureq-dev (= ${binary:Version}),
+ librust-proxmox-acme-rs-0.4.0+ureq-dev (= ${binary:Version})
+Description: ACME client library - feature "ureq"
+ This metapackage enables feature "ureq" for the Rust proxmox-acme-rs crate, by
+ pulling in any additional dependencies needed by that feature.
--- /dev/null
+Copyright (C) 2020-2021 Proxmox Server Solutions GmbH
+
+This software is written by Proxmox Server Solutions GmbH <support@proxmox.com>
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
--- /dev/null
+overlay = "."
+crate_src_path = ".."
+maintainer = "Proxmox Support Team <support@proxmox.com>"
+
+[source]
+# TODO: update once public
+vcs_git = ""
+vcs_browser = ""
--- /dev/null
+3.0 (native)
--- /dev/null
+edition = "2018"
--- /dev/null
+//! ACME Account management and creation. The [`Account`] type also contains most of the ACME API
+//! entry point helpers.
+
+use std::collections::HashMap;
+use std::convert::TryFrom;
+
+use openssl::pkey::{PKey, Private};
+use serde::{Deserialize, Serialize};
+use serde_json::Value;
+
+use crate::authorization::{Authorization, GetAuthorization};
+use crate::b64u;
+use crate::directory::Directory;
+use crate::eab::ExternalAccountBinding;
+use crate::jws::Jws;
+use crate::key::{Jwk, PublicKey};
+use crate::order::{NewOrder, Order, OrderData};
+use crate::request::Request;
+use crate::Error;
+
+/// An ACME Account.
+///
+/// This contains the location URL, the account data and the private key for an account.
+/// This can directly be serialized via serde to persist the account.
+///
+/// In order to register a new account with an ACME provider, see the [`Account::creator`] method.
+#[derive(Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Account {
+ /// Account location URL.
+ pub location: String,
+
+ /// Acme account data.
+ pub data: AccountData,
+
+ /// base64url encoded PEM formatted private key.
+ pub private_key: String,
+}
+
+impl Account {
+ /// Rebuild an account from its components.
+ pub fn from_parts(location: String, private_key: String, data: AccountData) -> Self {
+ Self {
+ location,
+ data,
+ private_key,
+ }
+ }
+
+ /// Builds an [`AccountCreator`]. This handles creation of the private key and account data as
+ /// well as handling the response sent by the server for the registration request.
+ pub fn creator() -> AccountCreator {
+ AccountCreator::default()
+ }
+
+ /// Place a new order. This will build a [`NewOrder`] representing an in flight order creation
+ /// request.
+ ///
+ /// The returned `NewOrder`'s `request` option is *guaranteed* to be `Some(Request)`.
+ pub fn new_order(
+ &self,
+ order: &OrderData,
+ directory: &Directory,
+ nonce: &str,
+ ) -> Result<NewOrder, Error> {
+ let key = PKey::private_key_from_pem(self.private_key.as_bytes())?;
+
+ if order.identifiers.is_empty() {
+ return Err(Error::EmptyOrder);
+ }
+
+ let url = directory.new_order_url();
+ let body = serde_json::to_string(&Jws::new(
+ &key,
+ Some(self.location.clone()),
+ url.to_owned(),
+ nonce.to_owned(),
+ order,
+ )?)?;
+
+ let request = Request {
+ url: url.to_owned(),
+ method: "POST",
+ content_type: crate::request::JSON_CONTENT_TYPE,
+ body,
+ expected: crate::request::CREATED,
+ };
+
+ Ok(NewOrder::new(request))
+ }
+
+ /// Prepare a "POST-as-GET" request to fetch data. Low level helper.
+ pub fn get_request(&self, url: &str, nonce: &str) -> Result<Request, Error> {
+ let key = PKey::private_key_from_pem(self.private_key.as_bytes())?;
+ let body = serde_json::to_string(&Jws::new_full(
+ &key,
+ Some(self.location.clone()),
+ url.to_owned(),
+ nonce.to_owned(),
+ String::new(),
+ )?)?;
+
+ Ok(Request {
+ url: url.to_owned(),
+ method: "POST",
+ content_type: crate::request::JSON_CONTENT_TYPE,
+ body,
+ expected: 200,
+ })
+ }
+
+ /// Prepare a JSON POST request. Low level helper.
+ pub fn post_request<T: Serialize>(
+ &self,
+ url: &str,
+ nonce: &str,
+ data: &T,
+ ) -> Result<Request, Error> {
+ let key = PKey::private_key_from_pem(self.private_key.as_bytes())?;
+ let body = serde_json::to_string(&Jws::new(
+ &key,
+ Some(self.location.clone()),
+ url.to_owned(),
+ nonce.to_owned(),
+ data,
+ )?)?;
+
+ Ok(Request {
+ url: url.to_owned(),
+ method: "POST",
+ content_type: crate::request::JSON_CONTENT_TYPE,
+ body,
+ expected: 200,
+ })
+ }
+
+ /// Prepare a JSON POST request.
+ fn post_request_raw_payload(
+ &self,
+ url: &str,
+ nonce: &str,
+ payload: String,
+ ) -> Result<Request, Error> {
+ let key = PKey::private_key_from_pem(self.private_key.as_bytes())?;
+ let body = serde_json::to_string(&Jws::new_full(
+ &key,
+ Some(self.location.clone()),
+ url.to_owned(),
+ nonce.to_owned(),
+ payload,
+ )?)?;
+
+ Ok(Request {
+ url: url.to_owned(),
+ method: "POST",
+ content_type: crate::request::JSON_CONTENT_TYPE,
+ body,
+ expected: 200,
+ })
+ }
+
+ /// Get the "key authorization" for a token.
+ pub fn key_authorization(&self, token: &str) -> Result<String, Error> {
+ let key = PKey::private_key_from_pem(self.private_key.as_bytes())?;
+ let thumbprint = PublicKey::try_from(&*key)?.thumbprint()?;
+ Ok(format!("{}.{}", token, thumbprint))
+ }
+
+ /// Get the TXT field value for a dns-01 token. This is the base64url encoded sha256 digest of
+ /// the key authorization value.
+ pub fn dns_01_txt_value(&self, token: &str) -> Result<String, Error> {
+ let key_authorization = self.key_authorization(token)?;
+ let digest = openssl::sha::sha256(key_authorization.as_bytes());
+ Ok(b64u::encode(&digest))
+ }
+
+ /// Prepare a request to update account data.
+ ///
+ /// This is a rather low level interface. You should know what you're doing.
+ pub fn update_account_request<T: Serialize>(
+ &self,
+ nonce: &str,
+ data: &T,
+ ) -> Result<Request, Error> {
+ self.post_request(&self.location, nonce, data)
+ }
+
+ /// Prepare a request to deactivate this account.
+ pub fn deactivate_account_request<T: Serialize>(&self, nonce: &str) -> Result<Request, Error> {
+ self.post_request_raw_payload(
+ &self.location,
+ nonce,
+ r#"{"status":"deactivated"}"#.to_string(),
+ )
+ }
+
+ /// Prepare a request to query an Authorization for an Order.
+ ///
+ /// Returns `Ok(None)` if `auth_index` is out of out of range. You can query the number of
+ /// authorizations from via [`Order::authorization_len`] or by manually inspecting its
+ /// `.data.authorization` vector.
+ pub fn get_authorization(
+ &self,
+ order: &Order,
+ auth_index: usize,
+ nonce: &str,
+ ) -> Result<Option<GetAuthorization>, Error> {
+ match order.authorization(auth_index) {
+ None => Ok(None),
+ Some(url) => Ok(Some(GetAuthorization::new(self.get_request(url, nonce)?))),
+ }
+ }
+
+ /// Prepare a request to validate a Challenge from an Authorization.
+ ///
+ /// Returns `Ok(None)` if `challenge_index` is out of out of range. The challenge count is
+ /// available by inspecting the [`Authorization::challenges`] vector.
+ ///
+ /// This returns a raw `Request` since validation takes some time and the `Authorization`
+ /// object has to be re-queried and its `status` inspected.
+ pub fn validate_challenge(
+ &self,
+ authorization: &Authorization,
+ challenge_index: usize,
+ nonce: &str,
+ ) -> Result<Option<Request>, Error> {
+ match authorization.challenges.get(challenge_index) {
+ None => Ok(None),
+ Some(challenge) => self
+ .post_request_raw_payload(&challenge.url, nonce, "{}".to_string())
+ .map(Some),
+ }
+ }
+
+ /// Prepare a request to revoke a certificate.
+ ///
+ /// The certificate can be either PEM or DER formatted.
+ ///
+ /// Note that this uses the account's key for authorization.
+ ///
+ /// Revocation using a certificate's private key is not yet implemented.
+ pub fn revoke_certificate(
+ &self,
+ certificate: &[u8],
+ reason: Option<u32>,
+ ) -> Result<CertificateRevocation, Error> {
+ let cert = if certificate.starts_with(b"-----BEGIN CERTIFICATE-----") {
+ b64u::encode(&openssl::x509::X509::from_pem(certificate)?.to_der()?)
+ } else {
+ b64u::encode(certificate)
+ };
+
+ let data = match reason {
+ Some(reason) => serde_json::json!({ "certificate": cert, "reason": reason }),
+ None => serde_json::json!({ "certificate": cert }),
+ };
+
+ Ok(CertificateRevocation {
+ account: self,
+ data,
+ })
+ }
+}
+
+/// Certificate revocation involves converting the certificate to base64url encoded DER and then
+/// embedding it in a json structure. Since we also need a nonce and possibly retry the request if
+/// a `BadNonce` error happens, this caches the converted data for efficiency.
+pub struct CertificateRevocation<'a> {
+ account: &'a Account,
+ data: Value,
+}
+
+impl CertificateRevocation<'_> {
+ /// Create the revocation request using the specified nonce for the given directory.
+ pub fn request(&self, directory: &Directory, nonce: &str) -> Result<Request, Error> {
+ self.account
+ .post_request(&directory.data.revoke_cert, nonce, &self.data)
+ }
+}
+
+/// Status of an ACME account.
+#[derive(Clone, Copy, Eq, PartialEq, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub enum AccountStatus {
+ /// This is not part of the ACME API, but a temporary marker for us until the ACME provider
+ /// tells us the account's real status.
+ #[serde(rename = "<invalid>")]
+ New,
+
+ /// Means the account is valid and can be used.
+ Valid,
+
+ /// The account has been deactivated by its user and cannot be used anymore.
+ Deactivated,
+
+ /// The account has been revoked by the server and cannot be used anymore.
+ Revoked,
+}
+
+impl AccountStatus {
+ #[inline]
+ fn new() -> Self {
+ AccountStatus::New
+ }
+
+ #[inline]
+ fn is_new(&self) -> bool {
+ *self == AccountStatus::New
+ }
+}
+
+/// ACME Account data. This is the part of the account returned from and possibly sent to the ACME
+/// provider. Some fields may be uptdated by the user via a request to the account location, others
+/// may not be changed.
+#[derive(Clone, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct AccountData {
+ /// The current account status.
+ #[serde(
+ skip_serializing_if = "AccountStatus::is_new",
+ default = "AccountStatus::new"
+ )]
+ pub status: AccountStatus,
+
+ /// URLs to currently pending orders.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub orders: Option<String>,
+
+ /// The acccount's contact info.
+ ///
+ /// This usually contains a `"mailto:<email address>"` entry but may also contain some other
+ /// data if the server accepts it.
+ #[serde(skip_serializing_if = "Vec::is_empty", default)]
+ pub contact: Vec<String>,
+
+ /// Indicated whether the user agreed to the ACME provider's terms of service.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub terms_of_service_agreed: Option<bool>,
+
+ /// External account information.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub external_account_binding: Option<ExternalAccountBinding>,
+
+ /// This is only used by the client when querying an account.
+ #[serde(default = "default_true", skip_serializing_if = "is_false")]
+ pub only_return_existing: bool,
+
+ /// Stores unknown fields if there are any.
+ #[serde(flatten, default, skip_serializing_if = "HashMap::is_empty")]
+ pub extra: HashMap<String, Value>,
+}
+
+#[inline]
+fn default_true() -> bool {
+ true
+}
+
+#[inline]
+fn is_false(b: &bool) -> bool {
+ !*b
+}
+
+/// Helper to create an account.
+///
+/// This is used to generate a private key and set the contact info for the account. Afterwards the
+/// creation request can be created via the [`request`](AccountCreator::request()) method, giving
+/// it a nonce and a directory. This can be repeated, if necessary, like when the nonce fails.
+///
+/// When the server sends a succesful response, it should be passed to the
+/// [`response`](AccountCreator::response()) method to finish the creation of an [`Account`] which
+/// can then be persisted.
+#[derive(Default)]
+#[must_use = "when creating an account you must pass the response to AccountCreator::response()!"]
+pub struct AccountCreator {
+ contact: Vec<String>,
+ terms_of_service_agreed: bool,
+ key: Option<PKey<Private>>,
+ eab_credentials: Option<(String, PKey<Private>)>,
+}
+
+impl AccountCreator {
+ /// Replace the contact infor with the provided ACME compatible data.
+ pub fn set_contacts(mut self, contact: Vec<String>) -> Self {
+ self.contact = contact;
+ self
+ }
+
+ /// Append a contact string.
+ pub fn contact(mut self, contact: String) -> Self {
+ self.contact.push(contact);
+ self
+ }
+
+ /// Append an email address to the contact list.
+ pub fn email(self, email: String) -> Self {
+ self.contact(format!("mailto:{}", email))
+ }
+
+ /// Change whether the account agrees to the terms of service. Use the directory's or client's
+ /// `terms_of_service_url()` method to present the user with the Terms of Service.
+ pub fn agree_to_tos(mut self, agree: bool) -> Self {
+ self.terms_of_service_agreed = agree;
+ self
+ }
+
+ /// Set the EAB credentials for the account registration
+ pub fn set_eab_credentials(mut self, kid: String, hmac_key: String) -> Result<Self, Error> {
+ let hmac_key = PKey::hmac(&base64::decode(hmac_key)?)?;
+ self.eab_credentials = Some((kid, hmac_key));
+ Ok(self)
+ }
+
+ /// Generate a new RSA key of the specified key size.
+ pub fn generate_rsa_key(self, bits: u32) -> Result<Self, Error> {
+ let key = openssl::rsa::Rsa::generate(bits)?;
+ Ok(self.with_key(PKey::from_rsa(key)?))
+ }
+
+ /// Generate a new P-256 EC key.
+ pub fn generate_ec_key(self) -> Result<Self, Error> {
+ let key = openssl::ec::EcKey::generate(
+ openssl::ec::EcGroup::from_curve_name(openssl::nid::Nid::X9_62_PRIME256V1)?.as_ref(),
+ )?;
+ Ok(self.with_key(PKey::from_ec_key(key)?))
+ }
+
+ /// Use an existing key. Note that only RSA and EC keys using the `P-256` curve are currently
+ /// supported, however, this will not be checked at this point.
+ pub fn with_key(mut self, key: PKey<Private>) -> Self {
+ self.key = Some(key);
+ self
+ }
+
+ /// Prepare a HTTP request to create this account.
+ ///
+ /// Changes to the user data made after this will have no effect on the account generated with
+ /// the resulting request.
+ /// Changing the private key between using the request and passing the response to
+ /// [`response`](AccountCreator::response()) will render the account unusable!
+ pub fn request(&self, directory: &Directory, nonce: &str) -> Result<Request, Error> {
+ let key = self.key.as_deref().ok_or(Error::MissingKey)?;
+ let url = directory.new_account_url();
+
+ let external_account_binding = self
+ .eab_credentials
+ .as_ref()
+ .map(|cred| {
+ ExternalAccountBinding::new(&cred.0, &cred.1, Jwk::try_from(key)?, url.to_string())
+ })
+ .transpose()?;
+
+ let data = AccountData {
+ orders: None,
+ status: AccountStatus::New,
+ contact: self.contact.clone(),
+ terms_of_service_agreed: if self.terms_of_service_agreed {
+ Some(true)
+ } else {
+ None
+ },
+ external_account_binding,
+ only_return_existing: false,
+ extra: HashMap::new(),
+ };
+
+ let body = serde_json::to_string(&Jws::new(
+ key,
+ None,
+ url.to_owned(),
+ nonce.to_owned(),
+ &data,
+ )?)?;
+
+ Ok(Request {
+ url: url.to_owned(),
+ method: "POST",
+ content_type: crate::request::JSON_CONTENT_TYPE,
+ body,
+ expected: crate::request::CREATED,
+ })
+ }
+
+ /// After issuing the request from [`request()`](AccountCreator::request()), the response's
+ /// `Location` header and body must be passed to this for verification and to create an account
+ /// which is to be persisted!
+ pub fn response(self, location_header: String, response_body: &[u8]) -> Result<Account, Error> {
+ let private_key = self
+ .key
+ .ok_or(Error::MissingKey)?
+ .private_key_to_pem_pkcs8()?;
+ let private_key = String::from_utf8(private_key).map_err(|_| {
+ Error::Custom("PEM key contained illegal non-utf-8 characters".to_string())
+ })?;
+
+ Ok(Account {
+ location: location_header,
+ data: serde_json::from_slice(response_body)
+ .map_err(|err| Error::BadAccountData(err.to_string()))?,
+ private_key,
+ })
+ }
+}
--- /dev/null
+//! Authorization and Challenge data.
+
+use std::collections::HashMap;
+
+use serde::{Deserialize, Serialize};
+use serde_json::Value;
+
+use crate::order::Identifier;
+use crate::request::Request;
+use crate::Error;
+
+/// Status of an [`Authorization`].
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
+#[serde(rename_all = "lowercase")]
+pub enum Status {
+ /// The authorization was deactivated by the client.
+ Deactivated,
+
+ /// The authorization expired.
+ Expired,
+
+ /// The authorization failed and is now invalid.
+ Invalid,
+
+ /// Validation is pending.
+ Pending,
+
+ /// The authorization was revoked by the server.
+ Revoked,
+
+ /// The identifier is authorized.
+ Valid,
+}
+
+impl Status {
+ /// Convenience method to check if the status is 'pending'.
+ #[inline]
+ pub fn is_pending(self) -> bool {
+ self == Status::Pending
+ }
+
+ /// Convenience method to check if the status is 'valid'.
+ #[inline]
+ pub fn is_valid(self) -> bool {
+ self == Status::Valid
+ }
+}
+
+/// Represents an authorization state for an order. The user is expected to pick a challenge,
+/// execute it, and the request validation for it.
+#[derive(Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Authorization {
+ /// The identifier (usually domain name) this authorization is for.
+ pub identifier: Identifier,
+
+ /// The current status of this authorization entry.
+ pub status: Status,
+
+ /// Expiration date for the authorization.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub expires: Option<String>,
+
+ /// List of challenges which can be used to complete this authorization.
+ pub challenges: Vec<Challenge>,
+
+ /// The authorization is for a wildcard domain.
+ #[serde(default, skip_serializing_if = "is_false")]
+ pub wildcard: bool,
+}
+
+/// The state of a challenge.
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
+#[serde(rename_all = "lowercase")]
+pub enum ChallengeStatus {
+ /// The challenge is pending and has not been validated yet.
+ Pending,
+
+ /// The valiation is in progress.
+ Processing,
+
+ /// The challenge was successfully validated.
+ Valid,
+
+ /// Validation of this challenge failed.
+ Invalid,
+}
+
+impl ChallengeStatus {
+ /// Convenience method to check if the status is 'pending'.
+ #[inline]
+ pub fn is_pending(self) -> bool {
+ self == ChallengeStatus::Pending
+ }
+
+ /// Convenience method to check if the status is 'valid'.
+ #[inline]
+ pub fn is_valid(self) -> bool {
+ self == ChallengeStatus::Valid
+ }
+}
+
+/// A challenge object contains information on how to complete an authorization for an order.
+#[derive(Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Challenge {
+ /// The challenge type (such as `"dns-01"`).
+ #[serde(rename = "type")]
+ pub ty: String,
+
+ /// The current challenge status.
+ pub status: ChallengeStatus,
+
+ /// The URL used to post to in order to begin the validation for this challenge.
+ pub url: String,
+
+ /// Contains the remaining fields of the Challenge object, such as the `token`.
+ #[serde(flatten)]
+ pub data: HashMap<String, Value>,
+}
+
+impl Challenge {
+ /// Most challenges have a `token` used for key authorizations. This is a convenience helper to
+ /// access it.
+ pub fn token(&self) -> Option<&str> {
+ self.data.get("token").and_then(Value::as_str)
+ }
+}
+
+/// Serde helper
+#[inline]
+fn is_false(b: &bool) -> bool {
+ !*b
+}
+
+/// Represents an in-flight query for an authorization.
+///
+/// This is created via [`Account::get_authorization`](crate::Account::get_authorization()).
+pub struct GetAuthorization {
+ //order: OrderData,
+ /// The request to send to the ACME provider. This is wrapped in an option in order to allow
+ /// moving it out instead of copying the contents.
+ ///
+ /// When generated via [`Account::get_authorization`](crate::Account::get_authorization()),
+ /// this is guaranteed to be `Some`.
+ ///
+ /// The response should be passed to the the [`response`](GetAuthorization::response()) method.
+ pub request: Option<Request>,
+}
+
+impl GetAuthorization {
+ pub(crate) fn new(request: Request) -> Self {
+ Self {
+ request: Some(request),
+ }
+ }
+
+ /// Deal with the response we got from the server.
+ pub fn response(self, response_body: &[u8]) -> Result<Authorization, Error> {
+ Ok(serde_json::from_slice(response_body)?)
+ }
+}
--- /dev/null
+fn config() -> base64::Config {
+ base64::Config::new(base64::CharacterSet::UrlSafe, false)
+}
+
+/// Encode bytes as base64url into a `String`.
+pub fn encode(data: &[u8]) -> String {
+ base64::encode_config(data, config())
+}
+
+// curiously currently unused as we don't deserialize any of that
+// /// Decode bytes from a base64url string.
+// pub fn decode(data: &str) -> Result<Vec<u8>, base64::DecodeError> {
+// base64::decode_config(data, config())
+// }
+
+/// Our serde module for encoding bytes as base64url encoded strings.
+pub mod bytes {
+ use serde::{Serialize, Serializer};
+ //use serde::{Deserialize, Deserializer};
+
+ pub fn serialize<S>(data: &[u8], serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: Serializer,
+ {
+ super::encode(data).serialize(serializer)
+ }
+
+ // curiously currently unused as we don't deserialize any of that
+ // pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
+ // where
+ // D: Deserializer<'de>,
+ // {
+ // use serde::de::Error;
+
+ // Ok(super::decode(&String::deserialize(deserializer)?)
+ // .map_err(|e| D::Error::custom(e.to_string()))?)
+ // }
+}
--- /dev/null
+//! A blocking higher-level ACME client implementation using 'curl'.
+
+use std::io::Read;
+use std::sync::Arc;
+
+use serde::{Deserialize, Serialize};
+
+use crate::b64u;
+use crate::error;
+use crate::order::OrderData;
+use crate::request::ErrorResponse;
+use crate::{Account, Authorization, Challenge, Directory, Error, Order, Request};
+
+macro_rules! format_err {
+ ($($fmt:tt)*) => { Error::Client(format!($($fmt)*)) };
+}
+
+macro_rules! bail {
+ ($($fmt:tt)*) => {{ return Err(format_err!($($fmt)*)); }}
+}
+
+/// Low level HTTP response structure.
+pub struct HttpResponse {
+ /// The raw HTTP response body as a byte vector.
+ pub body: Vec<u8>,
+
+ /// The http status code.
+ pub status: u16,
+
+ /// The headers relevant to the ACME protocol.
+ pub headers: Headers,
+}
+
+impl HttpResponse {
+ /// Check the HTTP status code for a success code (200..299).
+ pub fn is_success(&self) -> bool {
+ self.status >= 200 && self.status < 300
+ }
+
+ /// Convenience shortcut to perform json deserialization of the returned body.
+ pub fn json<T: for<'a> Deserialize<'a>>(&self) -> Result<T, Error> {
+ Ok(serde_json::from_slice(&self.body)?)
+ }
+
+ /// Access the raw body as bytes.
+ pub fn bytes(&self) -> &[u8] {
+ &self.body
+ }
+
+ /// Get the returned location header. Borrowing shortcut to `self.headers.location`.
+ pub fn location(&self) -> Option<&str> {
+ self.headers.location.as_deref()
+ }
+
+ /// Convenience helper to assert that a location header was part of the response.
+ pub fn location_required(&mut self) -> Result<String, Error> {
+ self.headers
+ .location
+ .take()
+ .ok_or_else(|| format_err!("missing Location header"))
+ }
+}
+
+/// Contains headers from the HTTP response which are relevant parts of the Acme API.
+///
+/// Note that access to the `nonce` header is internal to this crate only, since a nonce will
+/// always be moved out of the response into the `Client` whenever a new nonce is received.
+#[derive(Default)]
+pub struct Headers {
+ /// The 'Location' header usually encodes the URL where an account or order can be queried from
+ /// after they were created.
+ pub location: Option<String>,
+ nonce: Option<String>,
+}
+
+struct Inner {
+ agent: Option<ureq::Agent>,
+ nonce: Option<String>,
+ proxy: Option<String>,
+}
+
+impl Inner {
+ fn agent(&mut self) -> Result<&mut ureq::Agent, Error> {
+ if self.agent.is_none() {
+ let connector = Arc::new(
+ native_tls::TlsConnector::new()
+ .map_err(|err| format_err!("failed to create tls connector: {}", err))?,
+ );
+
+ let mut builder = ureq::AgentBuilder::new().tls_connector(connector);
+
+ if let Some(proxy) = self.proxy.as_deref() {
+ builder = builder.proxy(
+ ureq::Proxy::new(proxy)
+ .map_err(|err| format_err!("failed to set proxy: {}", err))?,
+ );
+ }
+
+ self.agent = Some(builder.build());
+ }
+
+ Ok(self.agent.as_mut().unwrap())
+ }
+
+ fn new() -> Self {
+ Self {
+ agent: None,
+ nonce: None,
+ proxy: None,
+ }
+ }
+
+ fn execute(
+ &mut self,
+ method: &[u8],
+ url: &str,
+ request_body: Option<(&str, &[u8])>, // content-type and body
+ ) -> Result<HttpResponse, Error> {
+ let agent = self.agent()?;
+ let req = match method {
+ b"POST" => agent.post(url),
+ b"GET" => agent.get(url),
+ b"HEAD" => agent.head(url),
+ other => bail!("invalid http method: {:?}", other),
+ };
+
+ let response = if let Some((content_type, body)) = request_body {
+ req.set("Content-Type", content_type)
+ .set("Content-Length", &body.len().to_string())
+ .send_bytes(body)
+ } else {
+ req.call()
+ }
+ .map_err(|err| format_err!("http request failed: {}", err))?;
+
+ let mut headers = Headers::default();
+ if let Some(value) = response.header(crate::LOCATION) {
+ headers.location = Some(value.to_owned());
+ }
+
+ if let Some(value) = response.header(crate::REPLAY_NONCE) {
+ headers.nonce = Some(value.to_owned());
+ }
+
+ let status = response.status();
+
+ let mut body = Vec::new();
+ response
+ .into_reader()
+ .take(16 * 1024 * 1024) // arbitrary limit
+ .read_to_end(&mut body)
+ .map_err(|err| format_err!("failed to read response body: {}", err))?;
+
+ Ok(HttpResponse {
+ status,
+ headers,
+ body,
+ })
+ }
+
+ pub fn set_proxy(&mut self, proxy: String) {
+ self.proxy = Some(proxy);
+ self.agent = None;
+ }
+
+ /// Low-level API to run an API request. This automatically updates the current nonce!
+ fn run_request(&mut self, request: Request) -> Result<HttpResponse, Error> {
+ let body = if request.body.is_empty() {
+ None
+ } else {
+ Some((request.content_type, request.body.as_bytes()))
+ };
+
+ let mut response = self
+ .execute(request.method.as_bytes(), &request.url, body)
+ .map_err({
+ // borrow fixup:
+ let method = &request.method;
+ let url = &request.url;
+ move |err| format_err!("failed to execute {} request to {}: {}", method, url, err)
+ })?;
+
+ let got_nonce = self.update_nonce(&mut response)?;
+
+ if response.is_success() {
+ if response.status != request.expected {
+ return Err(Error::InvalidApi(format!(
+ "API server responded with unexpected status code: {:?}",
+ response.status
+ )));
+ }
+ return Ok(response);
+ }
+
+ let error: ErrorResponse = response.json().map_err(|err| {
+ format_err!("error status with improper error ACME response: {}", err)
+ })?;
+
+ if error.ty == error::BAD_NONCE {
+ if !got_nonce {
+ return Err(Error::InvalidApi(
+ "badNonce without a new Replay-Nonce header".to_string(),
+ ));
+ }
+ return Err(Error::BadNonce);
+ }
+
+ Err(Error::Api(error))
+ }
+
+ /// If the response contained a nonce, update our nonce and return `true`, otherwise return
+ /// `false`.
+ fn update_nonce(&mut self, response: &mut HttpResponse) -> Result<bool, Error> {
+ match response.headers.nonce.take() {
+ Some(nonce) => {
+ self.nonce = Some(nonce);
+ Ok(true)
+ }
+ None => Ok(false),
+ }
+ }
+
+ /// Update the nonce, if there isn't one it is an error.
+ fn must_update_nonce(&mut self, response: &mut HttpResponse) -> Result<(), Error> {
+ if !self.update_nonce(response)? {
+ bail!("newNonce URL did not return a nonce");
+ }
+ Ok(())
+ }
+
+ /// Update the Nonce.
+ fn new_nonce(&mut self, new_nonce_url: &str) -> Result<(), Error> {
+ let mut response = self.execute(b"HEAD", new_nonce_url, None).map_err(|err| {
+ Error::InvalidApi(format!("failed to get HEAD of newNonce URL: {}", err))
+ })?;
+
+ if !response.is_success() {
+ bail!("HEAD on newNonce URL returned error");
+ }
+
+ self.must_update_nonce(&mut response)?;
+
+ Ok(())
+ }
+
+ /// Make sure a nonce is available without forcing renewal.
+ fn nonce(&mut self, new_nonce_url: &str) -> Result<&str, Error> {
+ if self.nonce.is_none() {
+ self.new_nonce(new_nonce_url)?;
+ }
+ self.nonce
+ .as_deref()
+ .ok_or_else(|| format_err!("failed to get nonce"))
+ }
+}
+
+/// A blocking Acme client using curl's `Easy` interface.
+pub struct Client {
+ inner: Inner,
+ directory: Option<Directory>,
+ account: Option<Account>,
+ directory_url: String,
+}
+
+impl Client {
+ /// Create a new Client. This has no account associated with it yet, so the next step is to
+ /// either attach an existing `Account` or create a new one.
+ pub fn new(directory_url: String) -> Self {
+ Self {
+ inner: Inner::new(),
+ directory: None,
+ account: None,
+ directory_url,
+ }
+ }
+
+ /// Get the directory URL without querying the `Directory` structure.
+ ///
+ /// The difference to [`directory`](Client::directory()) is that this does not
+ /// attempt to fetch the directory data from the ACME server.
+ pub fn directory_url(&self) -> &str {
+ &self.directory_url
+ }
+
+ /// Set the account this client should use.
+ pub fn set_account(&mut self, account: Account) {
+ self.account = Some(account);
+ }
+
+ /// Get the Directory information.
+ pub fn directory(&mut self) -> Result<&Directory, Error> {
+ Self::get_directory(&mut self.inner, &mut self.directory, &self.directory_url)
+ }
+
+ /// Get the Directory information.
+ fn get_directory<'a>(
+ inner: &'_ mut Inner,
+ directory: &'a mut Option<Directory>,
+ directory_url: &str,
+ ) -> Result<&'a Directory, Error> {
+ if let Some(d) = directory {
+ return Ok(d);
+ }
+
+ let response = inner
+ .execute(b"GET", directory_url, None)
+ .map_err(|err| Error::InvalidApi(format!("failed to get directory info: {}", err)))?;
+
+ if !response.is_success() {
+ bail!(
+ "GET on the directory URL returned error status ({})",
+ response.status
+ );
+ }
+
+ *directory = Some(Directory::from_parts(
+ directory_url.to_string(),
+ response.json()?,
+ ));
+ Ok(directory.as_ref().unwrap())
+ }
+
+ /// Get the current account, if there is one.
+ pub fn account(&self) -> Option<&Account> {
+ self.account.as_ref()
+ }
+
+ /// Convenience method to get the ToS URL from the contained `Directory`.
+ ///
+ /// This requires mutable self as the directory information may be lazily loaded, which can
+ /// fail.
+ pub fn terms_of_service_url(&mut self) -> Result<Option<&str>, Error> {
+ Ok(self.directory()?.terms_of_service_url())
+ }
+
+ /// Get a fresh nonce (this should normally not be required as nonces are updated
+ /// automatically, even when a `badNonce` error occurs, which according to the ACME API
+ /// specification should include a new valid nonce in its headers anyway).
+ pub fn new_nonce(&mut self) -> Result<(), Error> {
+ let was_none = self.inner.nonce.is_none();
+ let directory =
+ Self::get_directory(&mut self.inner, &mut self.directory, &self.directory_url)?;
+ if was_none && self.inner.nonce.is_some() {
+ // this was the first call and we already got a nonce from querying the directory
+ return Ok(());
+ }
+
+ // otherwise actually call up to get a new nonce
+ self.inner.new_nonce(directory.new_nonce_url())
+ }
+
+ /// borrow helper
+ fn nonce<'a>(inner: &'a mut Inner, directory: &'_ Directory) -> Result<&'a str, Error> {
+ inner.nonce(directory.new_nonce_url())
+ }
+
+ /// Convenience method to create a new account with a list of ACME compatible contact strings
+ /// (eg. `mailto:someone@example.com`).
+ ///
+ /// Please remember to persist the returned `Account` structure somewhere to not lose access to
+ /// the account!
+ ///
+ /// If an RSA key size is provided, an RSA key will be generated. Otherwise an EC key using the
+ /// P-256 curve will be generated.
+ pub fn new_account(
+ &mut self,
+ contact: Vec<String>,
+ tos_agreed: bool,
+ rsa_bits: Option<u32>,
+ eab_creds: Option<(String, String)>,
+ ) -> Result<&Account, Error> {
+ let mut account = Account::creator()
+ .set_contacts(contact)
+ .agree_to_tos(tos_agreed);
+ if let Some((eab_kid, eab_hmac_key)) = eab_creds {
+ account = account.set_eab_credentials(eab_kid, eab_hmac_key)?;
+ }
+ let account = if let Some(bits) = rsa_bits {
+ account.generate_rsa_key(bits)?
+ } else {
+ account.generate_ec_key()?
+ };
+
+ self.register_account(account)
+ }
+
+ /// Register an ACME account.
+ ///
+ /// This uses an [`AccountCreator`](crate::account::AccountCreator) since it may need to build
+ /// the request multiple times in case the we get a `BadNonce` error.
+ pub fn register_account(
+ &mut self,
+ account: crate::account::AccountCreator,
+ ) -> Result<&Account, Error> {
+ let mut retry = retry();
+ let mut response = loop {
+ retry.tick()?;
+
+ let directory =
+ Self::get_directory(&mut self.inner, &mut self.directory, &self.directory_url)?;
+ let nonce = Self::nonce(&mut self.inner, directory)?;
+ let request = account.request(directory, nonce)?;
+ match self.run_request(request) {
+ Ok(response) => break response,
+ Err(err) if err.is_bad_nonce() => continue,
+ Err(err) => return Err(err),
+ }
+ };
+
+ let account = account.response(response.location_required()?, response.bytes().as_ref())?;
+
+ self.account = Some(account);
+ Ok(self.account.as_ref().unwrap())
+ }
+
+ fn need_account(account: &Option<Account>) -> Result<&Account, Error> {
+ account
+ .as_ref()
+ .ok_or_else(|| format_err!("cannot use client without an account"))
+ }
+
+ /// Update account data.
+ ///
+ /// Low-level version: we allow arbitrary data to be passed to the remote here, it's up to the
+ /// user to know what to do for now.
+ pub fn update_account<T: Serialize>(&mut self, data: &T) -> Result<&Account, Error> {
+ let account = Self::need_account(&self.account)?;
+
+ let mut retry = retry();
+ let response = loop {
+ retry.tick()?;
+ let directory =
+ Self::get_directory(&mut self.inner, &mut self.directory, &self.directory_url)?;
+ let nonce = Self::nonce(&mut self.inner, directory)?;
+ let request = account.post_request(&account.location, nonce, data)?;
+ let response = match self.inner.run_request(request) {
+ Ok(response) => response,
+ Err(err) if err.is_bad_nonce() => continue,
+ Err(err) => return Err(err),
+ };
+
+ break response;
+ };
+
+ // unwrap: we asserted we have an account at the top of the method!
+ let account = self.account.as_mut().unwrap();
+ account.data = response.json()?;
+ Ok(account)
+ }
+
+ /// Method to create a new order for a set of domains.
+ ///
+ /// Please remember to persist the order somewhere (ideally along with the account data) in
+ /// order to finish & query it later on.
+ pub fn new_order(&mut self, domains: Vec<String>) -> Result<Order, Error> {
+ let account = Self::need_account(&self.account)?;
+
+ let order = domains
+ .into_iter()
+ .fold(OrderData::new(), |order, domain| order.domain(domain));
+
+ let mut retry = retry();
+ loop {
+ retry.tick()?;
+
+ let directory =
+ Self::get_directory(&mut self.inner, &mut self.directory, &self.directory_url)?;
+ let nonce = Self::nonce(&mut self.inner, directory)?;
+ let mut new_order = account.new_order(&order, directory, nonce)?;
+ let mut response = match self.inner.run_request(new_order.request.take().unwrap()) {
+ Ok(response) => response,
+ Err(err) if err.is_bad_nonce() => continue,
+ Err(err) => return Err(err),
+ };
+
+ return new_order.response(response.location_required()?, response.bytes().as_ref());
+ }
+ }
+
+ /// Assuming the provided URL is an 'Authorization' URL, get and deserialize it.
+ pub fn get_authorization(&mut self, url: &str) -> Result<Authorization, Error> {
+ self.post_as_get(url)?.json()
+ }
+
+ /// Assuming the provided URL is an 'Order' URL, get and deserialize it.
+ pub fn get_order(&mut self, url: &str) -> Result<OrderData, Error> {
+ self.post_as_get(url)?.json()
+ }
+
+ /// Low level "POST-as-GET" request.
+ pub fn post_as_get(&mut self, url: &str) -> Result<HttpResponse, Error> {
+ let account = Self::need_account(&self.account)?;
+
+ let mut retry = retry();
+ loop {
+ retry.tick()?;
+
+ let directory =
+ Self::get_directory(&mut self.inner, &mut self.directory, &self.directory_url)?;
+ let nonce = Self::nonce(&mut self.inner, directory)?;
+ let request = account.get_request(url, nonce)?;
+ match self.inner.run_request(request) {
+ Ok(response) => return Ok(response),
+ Err(err) if err.is_bad_nonce() => continue,
+ Err(err) => return Err(err),
+ }
+ }
+ }
+
+ /// Low level POST request.
+ pub fn post<T: Serialize>(&mut self, url: &str, data: &T) -> Result<HttpResponse, Error> {
+ let account = Self::need_account(&self.account)?;
+
+ let mut retry = retry();
+ loop {
+ retry.tick()?;
+
+ let directory =
+ Self::get_directory(&mut self.inner, &mut self.directory, &self.directory_url)?;
+ let nonce = Self::nonce(&mut self.inner, directory)?;
+ let request = account.post_request(url, nonce, data)?;
+ match self.inner.run_request(request) {
+ Ok(response) => return Ok(response),
+ Err(err) if err.is_bad_nonce() => continue,
+ Err(err) => return Err(err),
+ }
+ }
+ }
+
+ /// Request challenge validation. Afterwards, the challenge should be polled.
+ pub fn request_challenge_validation(&mut self, url: &str) -> Result<Challenge, Error> {
+ self.post(url, &serde_json::json!({}))?.json()
+ }
+
+ /// Shortcut to `account().ok_or_else(...).key_authorization()`.
+ pub fn key_authorization(&self, token: &str) -> Result<String, Error> {
+ Self::need_account(&self.account)?.key_authorization(token)
+ }
+
+ /// Shortcut to `account().ok_or_else(...).dns_01_txt_value()`.
+ /// the key authorization value.
+ pub fn dns_01_txt_value(&self, token: &str) -> Result<String, Error> {
+ Self::need_account(&self.account)?.dns_01_txt_value(token)
+ }
+
+ /// Low-level API to run an n API request. This automatically updates the current nonce!
+ pub fn run_request(&mut self, request: Request) -> Result<HttpResponse, Error> {
+ self.inner.run_request(request)
+ }
+
+ /// Finalize an Order via its `finalize` URL property and the DER encoded CSR.
+ pub fn finalize(&mut self, url: &str, csr: &[u8]) -> Result<(), Error> {
+ let csr = b64u::encode(csr);
+ let data = serde_json::json!({ "csr": csr });
+ self.post(url, &data)?;
+ Ok(())
+ }
+
+ /// Download a certificate via its 'certificate' URL property.
+ ///
+ /// The certificate will be a PEM certificate chain.
+ pub fn get_certificate(&mut self, url: &str) -> Result<Vec<u8>, Error> {
+ Ok(self.post_as_get(url)?.body)
+ }
+
+ /// Revoke an existing certificate (PEM or DER formatted).
+ pub fn revoke_certificate(
+ &mut self,
+ certificate: &[u8],
+ reason: Option<u32>,
+ ) -> Result<(), Error> {
+ // TODO: This can also work without an account.
+ let account = Self::need_account(&self.account)?;
+
+ let revocation = account.revoke_certificate(certificate, reason)?;
+
+ let mut retry = retry();
+ loop {
+ retry.tick()?;
+
+ let directory =
+ Self::get_directory(&mut self.inner, &mut self.directory, &self.directory_url)?;
+ let nonce = Self::nonce(&mut self.inner, directory)?;
+ let request = revocation.request(directory, nonce)?;
+ match self.inner.run_request(request) {
+ Ok(_response) => return Ok(()),
+ Err(err) if err.is_bad_nonce() => continue,
+ Err(err) => return Err(err),
+ }
+ }
+ }
+
+ /// Set a proxy
+ pub fn set_proxy(&mut self, proxy: String) {
+ self.inner.set_proxy(proxy)
+ }
+}
+
+/// bad nonce retry count helper
+struct Retry(usize);
+
+const fn retry() -> Retry {
+ Retry(0)
+}
+
+impl Retry {
+ fn tick(&mut self) -> Result<(), Error> {
+ if self.0 >= 3 {
+ bail!("kept getting a badNonce error!");
+ }
+ self.0 += 1;
+ Ok(())
+ }
+}
--- /dev/null
+//! ACME Directory information.
+
+use serde::{Deserialize, Serialize};
+
+/// An ACME Directory. This contains the base URL and the directory data as received via a `GET`
+/// request to the URL.
+pub struct Directory {
+ /// The main entry point URL to the ACME directory.
+ pub url: String,
+
+ /// The json structure received via a `GET` request to the directory URL. This contains the
+ /// URLs for various API entry points.
+ pub data: DirectoryData,
+}
+
+/// The ACME Directory object structure.
+///
+/// The data in here is typically not relevant to the user of this crate.
+#[derive(Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct DirectoryData {
+ /// The entry point to create a new account.
+ pub new_account: String,
+
+ /// The entry point to retrieve a new nonce, should be used with a `HEAD` request.
+ pub new_nonce: String,
+
+ /// URL to post new orders to.
+ pub new_order: String,
+
+ /// URL to use for certificate revocation.
+ pub revoke_cert: String,
+
+ /// Account key rollover URL.
+ pub key_change: String,
+
+ /// Metadata object, for additional information which aren't directly part of the API
+ /// itself, such as the terms of service.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub meta: Option<Meta>,
+}
+
+/// The directory's "meta" object.
+#[derive(Clone, Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Meta {
+ /// The terms of service. This is typically in the form of an URL.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub terms_of_service: Option<String>,
+
+ /// Flag indicating if EAB is required, None is equivalent to false
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub external_account_required: Option<bool>,
+
+ /// Website with information about the ACME Server
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub website: Option<String>,
+
+ /// List of hostnames used by the CA, intended for the use with caa dns records
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
+ pub caa_identities: Vec<String>,
+}
+
+impl Directory {
+ /// Create a `Directory` given the parsed `DirectoryData` of a `GET` request to the directory
+ /// URL.
+ pub fn from_parts(url: String, data: DirectoryData) -> Self {
+ Self { url, data }
+ }
+
+ /// Get the ToS URL.
+ pub fn terms_of_service_url(&self) -> Option<&str> {
+ match &self.data.meta {
+ Some(meta) => meta.terms_of_service.as_deref(),
+ None => None,
+ }
+ }
+
+ /// Get if external account binding is required
+ pub fn external_account_binding_required(&self) -> bool {
+ matches!(
+ &self.data.meta,
+ Some(Meta {
+ external_account_required: Some(true),
+ ..
+ })
+ )
+ }
+
+ /// Get the "newNonce" URL. Use `HEAD` requests on this to get a new nonce.
+ pub fn new_nonce_url(&self) -> &str {
+ &self.data.new_nonce
+ }
+
+ pub(crate) fn new_account_url(&self) -> &str {
+ &self.data.new_account
+ }
+
+ pub(crate) fn new_order_url(&self) -> &str {
+ &self.data.new_order
+ }
+
+ /// Access to the in the Acme spec defined metadata structure.
+ pub fn meta(&self) -> Option<&Meta> {
+ self.data.meta.as_ref()
+ }
+}
--- /dev/null
+use openssl::hash::MessageDigest;
+use openssl::pkey::{HasPrivate, PKeyRef};
+use openssl::sign::Signer;
+use serde::{Deserialize, Serialize};
+
+use crate::key::Jwk;
+use crate::{b64u, Error};
+
+#[derive(Debug, Serialize)]
+#[serde(rename_all = "camelCase")]
+struct Protected {
+ alg: &'static str,
+ url: String,
+ kid: String,
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone)]
+#[serde(rename_all = "camelCase")]
+pub struct ExternalAccountBinding {
+ protected: String,
+ payload: String,
+ signature: String,
+}
+
+impl ExternalAccountBinding {
+ pub fn new<P>(
+ eab_kid: &str,
+ eab_hmac_key: &PKeyRef<P>,
+ jwk: Jwk,
+ url: String,
+ ) -> Result<Self, Error>
+ where
+ P: HasPrivate,
+ {
+ let protected = Protected {
+ alg: "HS256",
+ kid: eab_kid.to_string(),
+ url,
+ };
+ let payload = b64u::encode(serde_json::to_string(&jwk)?.as_bytes());
+ let protected_data = b64u::encode(serde_json::to_string(&protected)?.as_bytes());
+ let signature = {
+ let protected = protected_data.as_bytes();
+ let payload = payload.as_bytes();
+ Self::sign_hmac(eab_hmac_key, protected, payload)?
+ };
+
+ let signature = b64u::encode(&signature);
+ Ok(ExternalAccountBinding {
+ protected: protected_data,
+ payload,
+ signature,
+ })
+ }
+
+ fn sign_hmac<P>(key: &PKeyRef<P>, protected: &[u8], payload: &[u8]) -> Result<Vec<u8>, Error>
+ where
+ P: HasPrivate,
+ {
+ let mut signer = Signer::new(MessageDigest::sha256(), key)?;
+ signer.update(protected)?;
+ signer.update(b".")?;
+ signer.update(payload)?;
+ Ok(signer.sign_to_vec()?)
+ }
+}
--- /dev/null
+//! The `Error` type and some ACME error constants for reference.
+
+use std::fmt;
+
+use openssl::error::ErrorStack as SslErrorStack;
+
+/// The ACME error string for a "bad nonce" error.
+pub const BAD_NONCE: &str = "urn:ietf:params:acme:error:badNonce";
+
+/// The ACME error string for a "user action required" error.
+pub const USER_ACTION_REQUIRED: &str = "urn:ietf:params:acme:error:userActionRequired";
+
+/// Error types returned by this crate.
+#[derive(Debug)]
+#[must_use = "unused errors have no effect"]
+pub enum Error {
+ /// A `badNonce` API response. The request should be retried with the new nonce received along
+ /// with this response.
+ BadNonce,
+
+ /// A `userActionRequired` API response. Typically this means there was a change to the ToS and
+ /// the user has to agree to the new terms.
+ UserActionRequired(String),
+
+ /// Other error repsonses from the Acme API not handled specially.
+ Api(crate::request::ErrorResponse),
+
+ /// The Acme API behaved unexpectedly.
+ InvalidApi(String),
+
+ /// Tried to use an `Account` or `AccountCreator` without a private key.
+ MissingKey,
+
+ /// Tried to create an `Account` without providing a single contact info.
+ MissingContactInfo,
+
+ /// Tried to use an empty `Order`.
+ EmptyOrder,
+
+ /// A raw `openssl::PKey` containing an unsupported key was passed.
+ UnsupportedKeyType,
+
+ /// A raw `openssl::PKey` or `openssl::EcKey` with an unsupported curve was passed.
+ UnsupportedGroup,
+
+ /// Failed to parse the account data returned by the API upon account creation.
+ BadAccountData(String),
+
+ /// Failed to parse the order data returned by the API from a new-order request.
+ BadOrderData(String),
+
+ /// An openssl error occurred during a crypto operation.
+ RawSsl(SslErrorStack),
+
+ /// An openssl error occurred during a crypto operation.
+ /// With some textual context.
+ Ssl(&'static str, SslErrorStack),
+
+ /// An otherwise uncaught serde error happened.
+ Json(serde_json::Error),
+
+ /// Failed to parse
+ BadBase64(base64::DecodeError),
+
+ /// Can be used by the user for textual error messages without having to downcast to regular
+ /// acme errors.
+ Custom(String),
+
+ /// If built with the `client` feature, this is where general ureq/network errors end up.
+ /// This is usually a `ureq::Error`, however in order to provide an API which is not
+ /// feature-dependent, this variant is always present and contains a boxed `dyn Error`.
+ HttpClient(Box<dyn std::error::Error + Send + Sync + 'static>),
+
+ /// If built with the `client` feature, this is where client specific errors which are not from
+ /// errors forwarded from `ureq` end up.
+ Client(String),
+
+ /// A non-openssl error occurred while building data for the CSR.
+ Csr(String),
+}
+
+impl Error {
+ /// Create an `Error` from a custom text.
+ pub fn custom<T: std::fmt::Display>(s: T) -> Self {
+ Error::Custom(s.to_string())
+ }
+
+ /// Convenience method to check if this error represents a bad nonce error in which case the
+ /// request needs to be re-created using a new nonce.
+ pub fn is_bad_nonce(&self) -> bool {
+ matches!(self, Error::BadNonce)
+ }
+}
+
+impl std::error::Error for Error {}
+
+impl fmt::Display for Error {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match self {
+ Error::Api(err) => match err.detail.as_deref() {
+ Some(detail) => write!(f, "{}: {}", err.ty, detail),
+ None => fmt::Display::fmt(&err.ty, f),
+ },
+ Error::InvalidApi(err) => write!(f, "Acme Server API misbehaved: {}", err),
+ Error::BadNonce => f.write_str("bad nonce, please retry with a new nonce"),
+ Error::UserActionRequired(err) => write!(f, "user action required: {}", err),
+ Error::MissingKey => f.write_str("cannot build an account without a key"),
+ Error::MissingContactInfo => f.write_str("account requires contact info"),
+ Error::EmptyOrder => f.write_str("cannot make an empty order"),
+ Error::UnsupportedKeyType => f.write_str("unsupported key type"),
+ Error::UnsupportedGroup => f.write_str("unsupported EC group"),
+ Error::BadAccountData(err) => {
+ write!(f, "bad response to account query or creation: {}", err)
+ }
+ Error::BadOrderData(err) => {
+ write!(f, "bad response to new-order query or creation: {}", err)
+ }
+ Error::RawSsl(err) => fmt::Display::fmt(err, f),
+ Error::Ssl(context, err) => {
+ write!(f, "{}: {}", context, err)
+ }
+ Error::Json(err) => fmt::Display::fmt(err, f),
+ Error::Custom(err) => fmt::Display::fmt(err, f),
+ Error::HttpClient(err) => fmt::Display::fmt(err, f),
+ Error::Client(err) => fmt::Display::fmt(err, f),
+ Error::Csr(err) => fmt::Display::fmt(err, f),
+ Error::BadBase64(err) => fmt::Display::fmt(err, f),
+ }
+ }
+}
+
+impl From<SslErrorStack> for Error {
+ fn from(e: SslErrorStack) -> Self {
+ Error::RawSsl(e)
+ }
+}
+
+impl From<serde_json::Error> for Error {
+ fn from(e: serde_json::Error) -> Self {
+ Error::Json(e)
+ }
+}
+
+impl From<crate::request::ErrorResponse> for Error {
+ fn from(e: crate::request::ErrorResponse) -> Self {
+ Error::Api(e)
+ }
+}
+
+impl From<base64::DecodeError> for Error {
+ fn from(e: base64::DecodeError) -> Self {
+ Error::BadBase64(e)
+ }
+}
--- /dev/null
+use openssl::hash::Hasher;
+use serde_json::Value;
+
+use crate::Error;
+
+pub fn to_hash_canonical(value: &Value, output: &mut Hasher) -> Result<(), Error> {
+ match value {
+ Value::Null | Value::String(_) | Value::Number(_) | Value::Bool(_) => {
+ serde_json::to_writer(output, &value)?;
+ }
+ Value::Array(list) => {
+ output.update(b"[")?;
+ let mut iter = list.iter();
+ if let Some(item) = iter.next() {
+ to_hash_canonical(item, output)?;
+ for item in iter {
+ output.update(b",")?;
+ to_hash_canonical(item, output)?;
+ }
+ }
+ output.update(b"]")?;
+ }
+ Value::Object(map) => {
+ output.update(b"{")?;
+ let mut keys: Vec<&str> = map.keys().map(String::as_str).collect();
+ keys.sort_unstable();
+ let mut iter = keys.into_iter();
+ if let Some(key) = iter.next() {
+ serde_json::to_writer(&mut *output, &key)?;
+ output.update(b":")?;
+ to_hash_canonical(&map[key], output)?;
+ for key in iter {
+ output.update(b",")?;
+ serde_json::to_writer(&mut *output, &key)?;
+ output.update(b":")?;
+ to_hash_canonical(&map[key], output)?;
+ }
+ }
+ output.update(b"}")?;
+ }
+ }
+ Ok(())
+}
--- /dev/null
+use std::convert::TryFrom;
+
+use openssl::hash::{Hasher, MessageDigest};
+use openssl::pkey::{HasPrivate, PKeyRef};
+use openssl::sign::Signer;
+use serde::Serialize;
+
+use crate::b64u;
+use crate::key::{Jwk, PublicKey};
+use crate::Error;
+
+#[derive(Debug, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Protected {
+ alg: &'static str,
+ nonce: String,
+ url: String,
+ #[serde(flatten)]
+ key: KeyId,
+}
+
+/// Acme requires to the use of *either* `jwk` *or* `kid` depending on the action taken.
+#[derive(Debug, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub enum KeyId {
+ /// This is the actual JWK structure.
+ Jwk(Jwk),
+
+ /// This should be the account location.
+ Kid(String),
+}
+
+#[derive(Debug, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Jws {
+ protected: String,
+ payload: String,
+ signature: String,
+}
+
+impl Jws {
+ pub fn new<P, T>(
+ key: &PKeyRef<P>,
+ location: Option<String>,
+ url: String,
+ nonce: String,
+ payload: &T,
+ ) -> Result<Self, Error>
+ where
+ P: HasPrivate,
+ T: Serialize,
+ {
+ Self::new_full(
+ key,
+ location,
+ url,
+ nonce,
+ b64u::encode(serde_json::to_string(payload)?.as_bytes()),
+ )
+ }
+
+ pub fn new_full<P: HasPrivate>(
+ key: &PKeyRef<P>,
+ location: Option<String>,
+ url: String,
+ nonce: String,
+ payload: String,
+ ) -> Result<Self, Error> {
+ let jwk = Jwk::try_from(key)?;
+
+ let pubkey = jwk.key.clone();
+ let mut protected = Protected {
+ alg: "",
+ nonce,
+ url,
+ key: match location {
+ Some(location) => KeyId::Kid(location),
+ None => KeyId::Jwk(jwk),
+ },
+ };
+
+ let (digest, ec_order_bytes): (MessageDigest, usize) = match &pubkey {
+ PublicKey::Rsa(_) => (Self::prepare_rsa(key, &mut protected), 0),
+ PublicKey::Ec(_) => Self::prepare_ec(key, &mut protected),
+ };
+
+ let protected_data = b64u::encode(serde_json::to_string(&protected)?.as_bytes());
+
+ let signature = {
+ let prot = protected_data.as_bytes();
+ let payload = payload.as_bytes();
+ match &pubkey {
+ PublicKey::Rsa(_) => Self::sign_rsa(key, digest, prot, payload),
+ PublicKey::Ec(_) => Self::sign_ec(key, digest, ec_order_bytes, prot, payload),
+ }?
+ };
+
+ let signature = b64u::encode(&signature);
+
+ Ok(Jws {
+ protected: protected_data,
+ payload,
+ signature,
+ })
+ }
+
+ fn prepare_rsa<P>(_key: &PKeyRef<P>, protected: &mut Protected) -> MessageDigest
+ where
+ P: HasPrivate,
+ {
+ protected.alg = "RS256";
+ MessageDigest::sha256()
+ }
+
+ /// Returns the digest and the size of the two signature components 'r' and 's'.
+ fn prepare_ec<P>(_key: &PKeyRef<P>, protected: &mut Protected) -> (MessageDigest, usize)
+ where
+ P: HasPrivate,
+ {
+ // Note: if we support >256 bit keys we'll want to also support using ES512 here probably
+ protected.alg = "ES256";
+ // 'r' and 's' are each 256 bit numbers:
+ (MessageDigest::sha256(), 32)
+ }
+
+ fn sign_rsa<P>(
+ key: &PKeyRef<P>,
+ digest: MessageDigest,
+ protected: &[u8],
+ payload: &[u8],
+ ) -> Result<Vec<u8>, Error>
+ where
+ P: HasPrivate,
+ {
+ let mut signer = Signer::new(digest, key)?;
+ signer.set_rsa_padding(openssl::rsa::Padding::PKCS1)?;
+ signer.update(protected)?;
+ signer.update(b".")?;
+ signer.update(payload)?;
+ Ok(signer.sign_to_vec()?)
+ }
+
+ fn sign_ec<P>(
+ key: &PKeyRef<P>,
+ digest: MessageDigest,
+ ec_order_bytes: usize,
+ protected: &[u8],
+ payload: &[u8],
+ ) -> Result<Vec<u8>, Error>
+ where
+ P: HasPrivate,
+ {
+ let mut hasher = Hasher::new(digest)?;
+ hasher.update(protected)?;
+ hasher.update(b".")?;
+ hasher.update(payload)?;
+ let sig =
+ openssl::ecdsa::EcdsaSig::sign(hasher.finish()?.as_ref(), key.ec_key()?.as_ref())?;
+ let r = sig.r().to_vec();
+ let s = sig.s().to_vec();
+ let mut out = Vec::with_capacity(ec_order_bytes * 2);
+ out.extend(std::iter::repeat(0u8).take(ec_order_bytes - r.len()));
+ out.extend(r);
+ out.extend(std::iter::repeat(0u8).take(ec_order_bytes - s.len()));
+ out.extend(s);
+ Ok(out)
+ }
+}
--- /dev/null
+use std::convert::{TryFrom, TryInto};
+
+use openssl::hash::{Hasher, MessageDigest};
+use openssl::pkey::{HasPublic, Id, PKeyRef};
+use serde::Serialize;
+
+use crate::b64u;
+use crate::Error;
+
+/// An RSA public key.
+#[derive(Clone, Debug, Serialize)]
+#[serde(deny_unknown_fields)]
+pub struct RsaPublicKey {
+ #[serde(with = "b64u::bytes")]
+ e: Vec<u8>,
+ #[serde(with = "b64u::bytes")]
+ n: Vec<u8>,
+}
+
+/// An EC public key.
+#[derive(Clone, Debug, Serialize)]
+#[serde(deny_unknown_fields)]
+pub struct EcPublicKey {
+ crv: &'static str,
+ #[serde(with = "b64u::bytes")]
+ x: Vec<u8>,
+ #[serde(with = "b64u::bytes")]
+ y: Vec<u8>,
+}
+
+/// A public key.
+///
+/// Internally tagged, so this already contains the 'kty' member.
+#[derive(Clone, Debug, Serialize)]
+#[serde(tag = "kty")]
+pub enum PublicKey {
+ #[serde(rename = "RSA")]
+ Rsa(RsaPublicKey),
+ #[serde(rename = "EC")]
+ Ec(EcPublicKey),
+}
+
+impl PublicKey {
+ /// The thumbprint is the b64u encoded sha256sum of the *canonical* json representation.
+ pub fn thumbprint(&self) -> Result<String, Error> {
+ let mut hasher = Hasher::new(MessageDigest::sha256())?;
+ crate::json::to_hash_canonical(&serde_json::to_value(self)?, &mut hasher)?;
+ Ok(b64u::encode(hasher.finish()?.as_ref()))
+ }
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct Jwk {
+ #[serde(rename = "use", skip_serializing_if = "Option::is_none")]
+ pub usage: Option<String>,
+
+ /// The key data is internally tagged, we can just flatten it.
+ #[serde(flatten)]
+ pub key: PublicKey,
+}
+
+impl<P: HasPublic> TryFrom<&PKeyRef<P>> for Jwk {
+ type Error = Error;
+
+ fn try_from(key: &PKeyRef<P>) -> Result<Self, Self::Error> {
+ Ok(Self {
+ key: key.try_into()?,
+ usage: None,
+ })
+ }
+}
+
+impl<P: HasPublic> TryFrom<&PKeyRef<P>> for PublicKey {
+ type Error = Error;
+
+ fn try_from(key: &PKeyRef<P>) -> Result<Self, Self::Error> {
+ match key.id() {
+ Id::RSA => Ok(PublicKey::Rsa(RsaPublicKey::try_from(&key.rsa()?)?)),
+ Id::EC => Ok(PublicKey::Ec(EcPublicKey::try_from(&key.ec_key()?)?)),
+ _ => Err(Error::UnsupportedKeyType),
+ }
+ }
+}
+
+impl<P: HasPublic> TryFrom<&openssl::rsa::Rsa<P>> for RsaPublicKey {
+ type Error = Error;
+
+ fn try_from(key: &openssl::rsa::Rsa<P>) -> Result<Self, Self::Error> {
+ Ok(RsaPublicKey {
+ e: key.e().to_vec(),
+ n: key.n().to_vec(),
+ })
+ }
+}
+
+impl<P: HasPublic> TryFrom<&openssl::ec::EcKey<P>> for EcPublicKey {
+ type Error = Error;
+
+ fn try_from(key: &openssl::ec::EcKey<P>) -> Result<Self, Self::Error> {
+ let group = key.group();
+
+ if group.curve_name() != Some(openssl::nid::Nid::X9_62_PRIME256V1) {
+ return Err(Error::UnsupportedGroup);
+ }
+
+ let mut ctx = openssl::bn::BigNumContext::new()?;
+ let mut x = openssl::bn::BigNum::new()?;
+ let mut y = openssl::bn::BigNum::new()?;
+ key.public_key()
+ .affine_coordinates(group, &mut x, &mut y, &mut ctx)?;
+
+ Ok(EcPublicKey {
+ crv: "P-256",
+ x: x.to_vec(),
+ y: y.to_vec(),
+ })
+ }
+}
+
+#[test]
+fn test_key_conversion() -> Result<(), Error> {
+ let key = openssl::ec::EcKey::generate(
+ openssl::ec::EcGroup::from_curve_name(openssl::nid::Nid::X9_62_PRIME256V1)?.as_ref(),
+ )?;
+
+ let _ = EcPublicKey::try_from(&key).expect("failed to jsonify ec key");
+
+ Ok(())
+}
--- /dev/null
+//! ACME protocol helper.
+//!
+//! This is supposed to implement the low level parts of the ACME protocol, providing an [`Account`]
+//! and some other helper types which allow interacting with an ACME server by implementing methods
+//! which create [`Request`]s the user can then combine with a nonce and send to the the ACME
+//! server using whatever http client they choose.
+//!
+//! This is a rather low level crate, and while it provides an optional synchronous client using
+//! curl (for simplicity), users should have basic understanding of the ACME API in order to
+//! implement a client using this.
+//!
+//! The [`Account`] helper supports RSA and ECC keys and provides most of the API methods.
+
+#![deny(missing_docs)]
+
+mod b64u;
+mod eab;
+mod json;
+mod jws;
+mod key;
+mod request;
+
+pub mod account;
+pub mod authorization;
+pub mod directory;
+pub mod error;
+pub mod order;
+pub mod util;
+
+#[doc(inline)]
+pub use account::Account;
+
+#[doc(inline)]
+pub use authorization::{Authorization, Challenge};
+
+#[doc(inline)]
+pub use directory::Directory;
+
+#[doc(inline)]
+pub use error::Error;
+
+#[doc(inline)]
+pub use order::Order;
+
+#[doc(inline)]
+pub use request::Request;
+
+// we don't inline these:
+pub use order::NewOrder;
+pub use request::ErrorResponse;
+
+/// Header name for nonces.
+pub const REPLAY_NONCE: &str = "Replay-Nonce";
+
+/// Header name for locations.
+pub const LOCATION: &str = "Location";
+
+#[cfg(feature = "client")]
+pub mod client;
+#[cfg(feature = "client")]
+pub use client::Client;
--- /dev/null
+//! ACME Orders data and identifiers.
+
+use serde::{Deserialize, Serialize};
+use serde_json::Value;
+
+use crate::request::Request;
+use crate::Error;
+
+/// Status of an [`Order`].
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
+#[serde(rename_all = "lowercase")]
+pub enum Status {
+ /// Invalid, used as a place holder for when sending objects as contrary to account creation,
+ /// the Acme RFC does not require the server to ignore unknown parts of the `Order` object.
+ New,
+
+ /// Authorization failed and it is now invalid.
+ Invalid,
+
+ /// The authorization is pending and the user should look through its challenges.
+ ///
+ /// This is the initial state of a new authorization.
+ Pending,
+
+ /// The ACME provider is processing an authorization validation.
+ Processing,
+
+ /// The requirements for the order have been met and it may be finalized.
+ Ready,
+
+ /// The certificate has been issued and can be downloaded from the URL provided in the
+ /// [`Order`]'s `certificate` field.
+ Valid,
+}
+
+impl Default for Status {
+ fn default() -> Self {
+ Status::New
+ }
+}
+
+impl Status {
+ /// Serde helper
+ fn is_new(&self) -> bool {
+ *self == Status::New
+ }
+
+ /// Convenience method to check if the status is 'pending'.
+ #[inline]
+ pub fn is_pending(self) -> bool {
+ self == Status::Pending
+ }
+
+ /// Convenience method to check if the status is 'valid'.
+ #[inline]
+ pub fn is_valid(self) -> bool {
+ self == Status::Valid
+ }
+}
+
+/// An identifier used for a certificate request.
+///
+/// Currently only supports DNS name identifiers.
+#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
+#[serde(tag = "type", content = "value", rename_all = "lowercase")]
+pub enum Identifier {
+ /// A DNS identifier is used to request a domain name to be added to a certificate.
+ Dns(String),
+}
+
+/// This contains the order data sent to and received from the ACME server.
+///
+/// This is typically filled with a set of domains and then issued as a new-order request via [`Account::new_order`](crate::Account::new_order).
+#[derive(Clone, Debug, Default, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct OrderData {
+ /// The order status.
+ #[serde(skip_serializing_if = "Status::is_new", default)]
+ pub status: Status,
+
+ /// This order's expiration date as RFC3339 formatted time string.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub expires: Option<String>,
+
+ /// List of identifiers to order for the certificate.
+ pub identifiers: Vec<Identifier>,
+
+ /// An RFC3339 formatted time string. It is up to the user to choose a dev dependency for this
+ /// shit.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub not_before: Option<String>,
+
+ /// An RFC3339 formatted time string. It is up to the user to choose a dev dependency for this
+ /// shit.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub not_after: Option<String>,
+
+ /// Possible errors in this order.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub error: Option<Value>,
+
+ /// List of URL's to authorizations the client needs to complete.
+ #[serde(skip_serializing_if = "Vec::is_empty")]
+ pub authorizations: Vec<String>,
+
+ /// URL the final CSR needs to be POSTed to in order to complete the order, once all
+ /// authorizations have been performed.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub finalize: Option<String>,
+
+ /// URL at which the issued certificate can be fetched once it is available.
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub certificate: Option<String>,
+}
+
+impl OrderData {
+ /// Initialize an empty order object.
+ pub fn new() -> Self {
+ Default::default()
+ }
+
+ /// Builder-style method to add a domain identifier to the data.
+ pub fn domain(mut self, domain: String) -> Self {
+ self.identifiers.push(Identifier::Dns(domain));
+ self
+ }
+}
+
+/// Represents an order for a new certificate. This combines the order's own location (URL) with
+/// the [`OrderData`] received from the ACME server.
+#[derive(Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Order {
+ /// Order location URL.
+ pub location: String,
+
+ /// The order's data object.
+ pub data: OrderData,
+}
+
+impl Order {
+ /// Get an authorization URL (or `None` if the index is out of range).
+ pub fn authorization(&self, index: usize) -> Option<&str> {
+ Some(self.data.authorizations.get(index)?)
+ }
+
+ /// Get the number of authorizations in this object.
+ pub fn authorization_len(&self) -> usize {
+ self.data.authorizations.len()
+ }
+}
+
+/// Represents a new in-flight order creation.
+///
+/// This is created via [`Account::new_order`](crate::Account::new_order()).
+pub struct NewOrder {
+ //order: OrderData,
+ /// The request to execute to place the order. When creating a [`NewOrder`] via
+ /// [`Account::new_order`](crate::Account::new_order) this is guaranteed to be `Some`.
+ pub request: Option<Request>,
+}
+
+impl NewOrder {
+ pub(crate) fn new(request: Request) -> Self {
+ Self {
+ //order,
+ request: Some(request),
+ }
+ }
+
+ /// Deal with the response we got from the server.
+ pub fn response(self, location_header: String, response_body: &[u8]) -> Result<Order, Error> {
+ Ok(Order {
+ location: location_header,
+ data: serde_json::from_slice(response_body)
+ .map_err(|err| Error::BadOrderData(err.to_string()))?,
+ })
+ }
+}
--- /dev/null
+use serde::Deserialize;
+
+pub(crate) const JSON_CONTENT_TYPE: &str = "application/jose+json";
+pub(crate) const CREATED: u16 = 201;
+
+/// A request which should be performed on the ACME provider.
+pub struct Request {
+ /// The complete URL to send the request to.
+ pub url: String,
+
+ /// The HTTP method name to use.
+ pub method: &'static str,
+
+ /// The `Content-Type` header to pass along.
+ pub content_type: &'static str,
+
+ /// The body to pass along with request, or an empty string.
+ pub body: String,
+
+ /// The expected status code a compliant ACME provider will return on success.
+ pub expected: u16,
+}
+
+/// An ACME error response contains a specially formatted type string, and can optionally
+/// contain textual details and a set of sub problems.
+#[derive(Clone, Debug, Deserialize)]
+pub struct ErrorResponse {
+ /// The ACME error type string.
+ ///
+ /// Most of the time we're only interested in the "bad nonce" or "user action required"
+ /// errors. When an [`Error`](crate::Error) is built from this error response, it will map
+ /// to the corresponding enum values (eg. [`Error::BadNonce`](crate::Error::BadNonce)).
+ #[serde(rename = "type")]
+ pub ty: String,
+
+ /// A textual detail string optionally provided by the ACME provider to inform the user more
+ /// verbosely about why the error occurred.
+ pub detail: Option<String>,
+
+ /// Additional json data containing information as to why the error occurred.
+ pub subproblems: Option<serde_json::Value>,
+}
--- /dev/null
+//! Certificate utility methods for convenience (such as CSR generation).
+
+use std::collections::HashMap;
+
+use openssl::hash::MessageDigest;
+use openssl::nid::Nid;
+use openssl::pkey::PKey;
+use openssl::rsa::Rsa;
+use openssl::x509::{self, X509Name, X509Req};
+
+use crate::Error;
+
+/// A certificate signing request.
+pub struct Csr {
+ /// DER encoded certificate request.
+ pub data: Vec<u8>,
+
+ /// PEM formatted PKCS#8 private key.
+ pub private_key_pem: Vec<u8>,
+}
+
+impl Csr {
+ /// Generate a CSR in DER format with a PEM formatted PKCS8 private key.
+ ///
+ /// The `identifiers` should be a list of domains. The `attributes` should have standard names
+ /// recognized by openssl.
+ pub fn generate(
+ identifiers: &[impl AsRef<str>],
+ attributes: &HashMap<String, &str>,
+ ) -> Result<Self, Error> {
+ if identifiers.is_empty() {
+ return Err(Error::Csr("cannot generate empty CSR".to_string()));
+ }
+
+ let private_key = Rsa::generate(4096)
+ .and_then(PKey::from_rsa)
+ .map_err(|err| Error::Ssl("failed to generate RSA key: {}", err))?;
+
+ let private_key_pem = private_key
+ .private_key_to_pem_pkcs8()
+ .map_err(|err| Error::Ssl("failed to format private key as PEM pkcs8: {}", err))?;
+
+ let mut name = X509Name::builder()?;
+ if !attributes.contains_key("CN") {
+ name.append_entry_by_nid(Nid::COMMONNAME, identifiers[0].as_ref())?;
+ }
+ for (key, value) in attributes {
+ name.append_entry_by_text(key, value)?;
+ }
+ let name = name.build();
+
+ let mut csr = X509Req::builder()?;
+ csr.set_subject_name(&name)?;
+ csr.set_pubkey(&private_key)?;
+
+ let context = csr.x509v3_context(None);
+ let mut ext = openssl::stack::Stack::new()?;
+ ext.push(x509::extension::BasicConstraints::new().build()?)?;
+ ext.push(
+ x509::extension::KeyUsage::new()
+ .digital_signature()
+ .key_encipherment()
+ .build()?,
+ )?;
+ ext.push(
+ x509::extension::ExtendedKeyUsage::new()
+ .server_auth()
+ .client_auth()
+ .build()?,
+ )?;
+ let mut san = x509::extension::SubjectAlternativeName::new();
+ for dns in identifiers {
+ san.dns(dns.as_ref());
+ }
+ ext.push({ san }.build(&context)?)?;
+ csr.add_extensions(&ext)?;
+
+ csr.sign(&private_key, MessageDigest::sha256())?;
+
+ Ok(Self {
+ data: csr.build().to_der()?,
+ private_key_pem,
+ })
+ }
+}
+++ /dev/null
-edition = "2018"
+++ /dev/null
-//! ACME Account management and creation. The [`Account`] type also contains most of the ACME API
-//! entry point helpers.
-
-use std::collections::HashMap;
-use std::convert::TryFrom;
-
-use openssl::pkey::{PKey, Private};
-use serde::{Deserialize, Serialize};
-use serde_json::Value;
-
-use crate::authorization::{Authorization, GetAuthorization};
-use crate::b64u;
-use crate::directory::Directory;
-use crate::eab::ExternalAccountBinding;
-use crate::jws::Jws;
-use crate::key::{Jwk, PublicKey};
-use crate::order::{NewOrder, Order, OrderData};
-use crate::request::Request;
-use crate::Error;
-
-/// An ACME Account.
-///
-/// This contains the location URL, the account data and the private key for an account.
-/// This can directly be serialized via serde to persist the account.
-///
-/// In order to register a new account with an ACME provider, see the [`Account::creator`] method.
-#[derive(Deserialize, Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct Account {
- /// Account location URL.
- pub location: String,
-
- /// Acme account data.
- pub data: AccountData,
-
- /// base64url encoded PEM formatted private key.
- pub private_key: String,
-}
-
-impl Account {
- /// Rebuild an account from its components.
- pub fn from_parts(location: String, private_key: String, data: AccountData) -> Self {
- Self {
- location,
- data,
- private_key,
- }
- }
-
- /// Builds an [`AccountCreator`]. This handles creation of the private key and account data as
- /// well as handling the response sent by the server for the registration request.
- pub fn creator() -> AccountCreator {
- AccountCreator::default()
- }
-
- /// Place a new order. This will build a [`NewOrder`] representing an in flight order creation
- /// request.
- ///
- /// The returned `NewOrder`'s `request` option is *guaranteed* to be `Some(Request)`.
- pub fn new_order(
- &self,
- order: &OrderData,
- directory: &Directory,
- nonce: &str,
- ) -> Result<NewOrder, Error> {
- let key = PKey::private_key_from_pem(self.private_key.as_bytes())?;
-
- if order.identifiers.is_empty() {
- return Err(Error::EmptyOrder);
- }
-
- let url = directory.new_order_url();
- let body = serde_json::to_string(&Jws::new(
- &key,
- Some(self.location.clone()),
- url.to_owned(),
- nonce.to_owned(),
- order,
- )?)?;
-
- let request = Request {
- url: url.to_owned(),
- method: "POST",
- content_type: crate::request::JSON_CONTENT_TYPE,
- body,
- expected: crate::request::CREATED,
- };
-
- Ok(NewOrder::new(request))
- }
-
- /// Prepare a "POST-as-GET" request to fetch data. Low level helper.
- pub fn get_request(&self, url: &str, nonce: &str) -> Result<Request, Error> {
- let key = PKey::private_key_from_pem(self.private_key.as_bytes())?;
- let body = serde_json::to_string(&Jws::new_full(
- &key,
- Some(self.location.clone()),
- url.to_owned(),
- nonce.to_owned(),
- String::new(),
- )?)?;
-
- Ok(Request {
- url: url.to_owned(),
- method: "POST",
- content_type: crate::request::JSON_CONTENT_TYPE,
- body,
- expected: 200,
- })
- }
-
- /// Prepare a JSON POST request. Low level helper.
- pub fn post_request<T: Serialize>(
- &self,
- url: &str,
- nonce: &str,
- data: &T,
- ) -> Result<Request, Error> {
- let key = PKey::private_key_from_pem(self.private_key.as_bytes())?;
- let body = serde_json::to_string(&Jws::new(
- &key,
- Some(self.location.clone()),
- url.to_owned(),
- nonce.to_owned(),
- data,
- )?)?;
-
- Ok(Request {
- url: url.to_owned(),
- method: "POST",
- content_type: crate::request::JSON_CONTENT_TYPE,
- body,
- expected: 200,
- })
- }
-
- /// Prepare a JSON POST request.
- fn post_request_raw_payload(
- &self,
- url: &str,
- nonce: &str,
- payload: String,
- ) -> Result<Request, Error> {
- let key = PKey::private_key_from_pem(self.private_key.as_bytes())?;
- let body = serde_json::to_string(&Jws::new_full(
- &key,
- Some(self.location.clone()),
- url.to_owned(),
- nonce.to_owned(),
- payload,
- )?)?;
-
- Ok(Request {
- url: url.to_owned(),
- method: "POST",
- content_type: crate::request::JSON_CONTENT_TYPE,
- body,
- expected: 200,
- })
- }
-
- /// Get the "key authorization" for a token.
- pub fn key_authorization(&self, token: &str) -> Result<String, Error> {
- let key = PKey::private_key_from_pem(self.private_key.as_bytes())?;
- let thumbprint = PublicKey::try_from(&*key)?.thumbprint()?;
- Ok(format!("{}.{}", token, thumbprint))
- }
-
- /// Get the TXT field value for a dns-01 token. This is the base64url encoded sha256 digest of
- /// the key authorization value.
- pub fn dns_01_txt_value(&self, token: &str) -> Result<String, Error> {
- let key_authorization = self.key_authorization(token)?;
- let digest = openssl::sha::sha256(key_authorization.as_bytes());
- Ok(b64u::encode(&digest))
- }
-
- /// Prepare a request to update account data.
- ///
- /// This is a rather low level interface. You should know what you're doing.
- pub fn update_account_request<T: Serialize>(
- &self,
- nonce: &str,
- data: &T,
- ) -> Result<Request, Error> {
- self.post_request(&self.location, nonce, data)
- }
-
- /// Prepare a request to deactivate this account.
- pub fn deactivate_account_request<T: Serialize>(&self, nonce: &str) -> Result<Request, Error> {
- self.post_request_raw_payload(
- &self.location,
- nonce,
- r#"{"status":"deactivated"}"#.to_string(),
- )
- }
-
- /// Prepare a request to query an Authorization for an Order.
- ///
- /// Returns `Ok(None)` if `auth_index` is out of out of range. You can query the number of
- /// authorizations from via [`Order::authorization_len`] or by manually inspecting its
- /// `.data.authorization` vector.
- pub fn get_authorization(
- &self,
- order: &Order,
- auth_index: usize,
- nonce: &str,
- ) -> Result<Option<GetAuthorization>, Error> {
- match order.authorization(auth_index) {
- None => Ok(None),
- Some(url) => Ok(Some(GetAuthorization::new(self.get_request(url, nonce)?))),
- }
- }
-
- /// Prepare a request to validate a Challenge from an Authorization.
- ///
- /// Returns `Ok(None)` if `challenge_index` is out of out of range. The challenge count is
- /// available by inspecting the [`Authorization::challenges`] vector.
- ///
- /// This returns a raw `Request` since validation takes some time and the `Authorization`
- /// object has to be re-queried and its `status` inspected.
- pub fn validate_challenge(
- &self,
- authorization: &Authorization,
- challenge_index: usize,
- nonce: &str,
- ) -> Result<Option<Request>, Error> {
- match authorization.challenges.get(challenge_index) {
- None => Ok(None),
- Some(challenge) => self
- .post_request_raw_payload(&challenge.url, nonce, "{}".to_string())
- .map(Some),
- }
- }
-
- /// Prepare a request to revoke a certificate.
- ///
- /// The certificate can be either PEM or DER formatted.
- ///
- /// Note that this uses the account's key for authorization.
- ///
- /// Revocation using a certificate's private key is not yet implemented.
- pub fn revoke_certificate(
- &self,
- certificate: &[u8],
- reason: Option<u32>,
- ) -> Result<CertificateRevocation, Error> {
- let cert = if certificate.starts_with(b"-----BEGIN CERTIFICATE-----") {
- b64u::encode(&openssl::x509::X509::from_pem(certificate)?.to_der()?)
- } else {
- b64u::encode(certificate)
- };
-
- let data = match reason {
- Some(reason) => serde_json::json!({ "certificate": cert, "reason": reason }),
- None => serde_json::json!({ "certificate": cert }),
- };
-
- Ok(CertificateRevocation {
- account: self,
- data,
- })
- }
-}
-
-/// Certificate revocation involves converting the certificate to base64url encoded DER and then
-/// embedding it in a json structure. Since we also need a nonce and possibly retry the request if
-/// a `BadNonce` error happens, this caches the converted data for efficiency.
-pub struct CertificateRevocation<'a> {
- account: &'a Account,
- data: Value,
-}
-
-impl CertificateRevocation<'_> {
- /// Create the revocation request using the specified nonce for the given directory.
- pub fn request(&self, directory: &Directory, nonce: &str) -> Result<Request, Error> {
- self.account
- .post_request(&directory.data.revoke_cert, nonce, &self.data)
- }
-}
-
-/// Status of an ACME account.
-#[derive(Clone, Copy, Eq, PartialEq, Deserialize, Serialize)]
-#[serde(rename_all = "camelCase")]
-pub enum AccountStatus {
- /// This is not part of the ACME API, but a temporary marker for us until the ACME provider
- /// tells us the account's real status.
- #[serde(rename = "<invalid>")]
- New,
-
- /// Means the account is valid and can be used.
- Valid,
-
- /// The account has been deactivated by its user and cannot be used anymore.
- Deactivated,
-
- /// The account has been revoked by the server and cannot be used anymore.
- Revoked,
-}
-
-impl AccountStatus {
- #[inline]
- fn new() -> Self {
- AccountStatus::New
- }
-
- #[inline]
- fn is_new(&self) -> bool {
- *self == AccountStatus::New
- }
-}
-
-/// ACME Account data. This is the part of the account returned from and possibly sent to the ACME
-/// provider. Some fields may be uptdated by the user via a request to the account location, others
-/// may not be changed.
-#[derive(Clone, Deserialize, Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct AccountData {
- /// The current account status.
- #[serde(
- skip_serializing_if = "AccountStatus::is_new",
- default = "AccountStatus::new"
- )]
- pub status: AccountStatus,
-
- /// URLs to currently pending orders.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub orders: Option<String>,
-
- /// The acccount's contact info.
- ///
- /// This usually contains a `"mailto:<email address>"` entry but may also contain some other
- /// data if the server accepts it.
- #[serde(skip_serializing_if = "Vec::is_empty", default)]
- pub contact: Vec<String>,
-
- /// Indicated whether the user agreed to the ACME provider's terms of service.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub terms_of_service_agreed: Option<bool>,
-
- /// External account information.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub external_account_binding: Option<ExternalAccountBinding>,
-
- /// This is only used by the client when querying an account.
- #[serde(default = "default_true", skip_serializing_if = "is_false")]
- pub only_return_existing: bool,
-
- /// Stores unknown fields if there are any.
- #[serde(flatten, default, skip_serializing_if = "HashMap::is_empty")]
- pub extra: HashMap<String, Value>,
-}
-
-#[inline]
-fn default_true() -> bool {
- true
-}
-
-#[inline]
-fn is_false(b: &bool) -> bool {
- !*b
-}
-
-/// Helper to create an account.
-///
-/// This is used to generate a private key and set the contact info for the account. Afterwards the
-/// creation request can be created via the [`request`](AccountCreator::request()) method, giving
-/// it a nonce and a directory. This can be repeated, if necessary, like when the nonce fails.
-///
-/// When the server sends a succesful response, it should be passed to the
-/// [`response`](AccountCreator::response()) method to finish the creation of an [`Account`] which
-/// can then be persisted.
-#[derive(Default)]
-#[must_use = "when creating an account you must pass the response to AccountCreator::response()!"]
-pub struct AccountCreator {
- contact: Vec<String>,
- terms_of_service_agreed: bool,
- key: Option<PKey<Private>>,
- eab_credentials: Option<(String, PKey<Private>)>,
-}
-
-impl AccountCreator {
- /// Replace the contact infor with the provided ACME compatible data.
- pub fn set_contacts(mut self, contact: Vec<String>) -> Self {
- self.contact = contact;
- self
- }
-
- /// Append a contact string.
- pub fn contact(mut self, contact: String) -> Self {
- self.contact.push(contact);
- self
- }
-
- /// Append an email address to the contact list.
- pub fn email(self, email: String) -> Self {
- self.contact(format!("mailto:{}", email))
- }
-
- /// Change whether the account agrees to the terms of service. Use the directory's or client's
- /// `terms_of_service_url()` method to present the user with the Terms of Service.
- pub fn agree_to_tos(mut self, agree: bool) -> Self {
- self.terms_of_service_agreed = agree;
- self
- }
-
- /// Set the EAB credentials for the account registration
- pub fn set_eab_credentials(mut self, kid: String, hmac_key: String) -> Result<Self, Error> {
- let hmac_key = PKey::hmac(&base64::decode(hmac_key)?)?;
- self.eab_credentials = Some((kid, hmac_key));
- Ok(self)
- }
-
- /// Generate a new RSA key of the specified key size.
- pub fn generate_rsa_key(self, bits: u32) -> Result<Self, Error> {
- let key = openssl::rsa::Rsa::generate(bits)?;
- Ok(self.with_key(PKey::from_rsa(key)?))
- }
-
- /// Generate a new P-256 EC key.
- pub fn generate_ec_key(self) -> Result<Self, Error> {
- let key = openssl::ec::EcKey::generate(
- openssl::ec::EcGroup::from_curve_name(openssl::nid::Nid::X9_62_PRIME256V1)?.as_ref(),
- )?;
- Ok(self.with_key(PKey::from_ec_key(key)?))
- }
-
- /// Use an existing key. Note that only RSA and EC keys using the `P-256` curve are currently
- /// supported, however, this will not be checked at this point.
- pub fn with_key(mut self, key: PKey<Private>) -> Self {
- self.key = Some(key);
- self
- }
-
- /// Prepare a HTTP request to create this account.
- ///
- /// Changes to the user data made after this will have no effect on the account generated with
- /// the resulting request.
- /// Changing the private key between using the request and passing the response to
- /// [`response`](AccountCreator::response()) will render the account unusable!
- pub fn request(&self, directory: &Directory, nonce: &str) -> Result<Request, Error> {
- let key = self.key.as_deref().ok_or(Error::MissingKey)?;
- let url = directory.new_account_url();
-
- let external_account_binding = self
- .eab_credentials
- .as_ref()
- .map(|cred| {
- ExternalAccountBinding::new(&cred.0, &cred.1, Jwk::try_from(key)?, url.to_string())
- })
- .transpose()?;
-
- let data = AccountData {
- orders: None,
- status: AccountStatus::New,
- contact: self.contact.clone(),
- terms_of_service_agreed: if self.terms_of_service_agreed {
- Some(true)
- } else {
- None
- },
- external_account_binding,
- only_return_existing: false,
- extra: HashMap::new(),
- };
-
- let body = serde_json::to_string(&Jws::new(
- key,
- None,
- url.to_owned(),
- nonce.to_owned(),
- &data,
- )?)?;
-
- Ok(Request {
- url: url.to_owned(),
- method: "POST",
- content_type: crate::request::JSON_CONTENT_TYPE,
- body,
- expected: crate::request::CREATED,
- })
- }
-
- /// After issuing the request from [`request()`](AccountCreator::request()), the response's
- /// `Location` header and body must be passed to this for verification and to create an account
- /// which is to be persisted!
- pub fn response(self, location_header: String, response_body: &[u8]) -> Result<Account, Error> {
- let private_key = self
- .key
- .ok_or(Error::MissingKey)?
- .private_key_to_pem_pkcs8()?;
- let private_key = String::from_utf8(private_key).map_err(|_| {
- Error::Custom("PEM key contained illegal non-utf-8 characters".to_string())
- })?;
-
- Ok(Account {
- location: location_header,
- data: serde_json::from_slice(response_body)
- .map_err(|err| Error::BadAccountData(err.to_string()))?,
- private_key,
- })
- }
-}
+++ /dev/null
-//! Authorization and Challenge data.
-
-use std::collections::HashMap;
-
-use serde::{Deserialize, Serialize};
-use serde_json::Value;
-
-use crate::order::Identifier;
-use crate::request::Request;
-use crate::Error;
-
-/// Status of an [`Authorization`].
-#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
-#[serde(rename_all = "lowercase")]
-pub enum Status {
- /// The authorization was deactivated by the client.
- Deactivated,
-
- /// The authorization expired.
- Expired,
-
- /// The authorization failed and is now invalid.
- Invalid,
-
- /// Validation is pending.
- Pending,
-
- /// The authorization was revoked by the server.
- Revoked,
-
- /// The identifier is authorized.
- Valid,
-}
-
-impl Status {
- /// Convenience method to check if the status is 'pending'.
- #[inline]
- pub fn is_pending(self) -> bool {
- self == Status::Pending
- }
-
- /// Convenience method to check if the status is 'valid'.
- #[inline]
- pub fn is_valid(self) -> bool {
- self == Status::Valid
- }
-}
-
-/// Represents an authorization state for an order. The user is expected to pick a challenge,
-/// execute it, and the request validation for it.
-#[derive(Deserialize, Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct Authorization {
- /// The identifier (usually domain name) this authorization is for.
- pub identifier: Identifier,
-
- /// The current status of this authorization entry.
- pub status: Status,
-
- /// Expiration date for the authorization.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub expires: Option<String>,
-
- /// List of challenges which can be used to complete this authorization.
- pub challenges: Vec<Challenge>,
-
- /// The authorization is for a wildcard domain.
- #[serde(default, skip_serializing_if = "is_false")]
- pub wildcard: bool,
-}
-
-/// The state of a challenge.
-#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
-#[serde(rename_all = "lowercase")]
-pub enum ChallengeStatus {
- /// The challenge is pending and has not been validated yet.
- Pending,
-
- /// The valiation is in progress.
- Processing,
-
- /// The challenge was successfully validated.
- Valid,
-
- /// Validation of this challenge failed.
- Invalid,
-}
-
-impl ChallengeStatus {
- /// Convenience method to check if the status is 'pending'.
- #[inline]
- pub fn is_pending(self) -> bool {
- self == ChallengeStatus::Pending
- }
-
- /// Convenience method to check if the status is 'valid'.
- #[inline]
- pub fn is_valid(self) -> bool {
- self == ChallengeStatus::Valid
- }
-}
-
-/// A challenge object contains information on how to complete an authorization for an order.
-#[derive(Deserialize, Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct Challenge {
- /// The challenge type (such as `"dns-01"`).
- #[serde(rename = "type")]
- pub ty: String,
-
- /// The current challenge status.
- pub status: ChallengeStatus,
-
- /// The URL used to post to in order to begin the validation for this challenge.
- pub url: String,
-
- /// Contains the remaining fields of the Challenge object, such as the `token`.
- #[serde(flatten)]
- pub data: HashMap<String, Value>,
-}
-
-impl Challenge {
- /// Most challenges have a `token` used for key authorizations. This is a convenience helper to
- /// access it.
- pub fn token(&self) -> Option<&str> {
- self.data.get("token").and_then(Value::as_str)
- }
-}
-
-/// Serde helper
-#[inline]
-fn is_false(b: &bool) -> bool {
- !*b
-}
-
-/// Represents an in-flight query for an authorization.
-///
-/// This is created via [`Account::get_authorization`](crate::Account::get_authorization()).
-pub struct GetAuthorization {
- //order: OrderData,
- /// The request to send to the ACME provider. This is wrapped in an option in order to allow
- /// moving it out instead of copying the contents.
- ///
- /// When generated via [`Account::get_authorization`](crate::Account::get_authorization()),
- /// this is guaranteed to be `Some`.
- ///
- /// The response should be passed to the the [`response`](GetAuthorization::response()) method.
- pub request: Option<Request>,
-}
-
-impl GetAuthorization {
- pub(crate) fn new(request: Request) -> Self {
- Self {
- request: Some(request),
- }
- }
-
- /// Deal with the response we got from the server.
- pub fn response(self, response_body: &[u8]) -> Result<Authorization, Error> {
- Ok(serde_json::from_slice(response_body)?)
- }
-}
+++ /dev/null
-fn config() -> base64::Config {
- base64::Config::new(base64::CharacterSet::UrlSafe, false)
-}
-
-/// Encode bytes as base64url into a `String`.
-pub fn encode(data: &[u8]) -> String {
- base64::encode_config(data, config())
-}
-
-// curiously currently unused as we don't deserialize any of that
-// /// Decode bytes from a base64url string.
-// pub fn decode(data: &str) -> Result<Vec<u8>, base64::DecodeError> {
-// base64::decode_config(data, config())
-// }
-
-/// Our serde module for encoding bytes as base64url encoded strings.
-pub mod bytes {
- use serde::{Serialize, Serializer};
- //use serde::{Deserialize, Deserializer};
-
- pub fn serialize<S>(data: &[u8], serializer: S) -> Result<S::Ok, S::Error>
- where
- S: Serializer,
- {
- super::encode(data).serialize(serializer)
- }
-
- // curiously currently unused as we don't deserialize any of that
- // pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
- // where
- // D: Deserializer<'de>,
- // {
- // use serde::de::Error;
-
- // Ok(super::decode(&String::deserialize(deserializer)?)
- // .map_err(|e| D::Error::custom(e.to_string()))?)
- // }
-}
+++ /dev/null
-//! A blocking higher-level ACME client implementation using 'curl'.
-
-use std::io::Read;
-use std::sync::Arc;
-
-use serde::{Deserialize, Serialize};
-
-use crate::b64u;
-use crate::error;
-use crate::order::OrderData;
-use crate::request::ErrorResponse;
-use crate::{Account, Authorization, Challenge, Directory, Error, Order, Request};
-
-macro_rules! format_err {
- ($($fmt:tt)*) => { Error::Client(format!($($fmt)*)) };
-}
-
-macro_rules! bail {
- ($($fmt:tt)*) => {{ return Err(format_err!($($fmt)*)); }}
-}
-
-/// Low level HTTP response structure.
-pub struct HttpResponse {
- /// The raw HTTP response body as a byte vector.
- pub body: Vec<u8>,
-
- /// The http status code.
- pub status: u16,
-
- /// The headers relevant to the ACME protocol.
- pub headers: Headers,
-}
-
-impl HttpResponse {
- /// Check the HTTP status code for a success code (200..299).
- pub fn is_success(&self) -> bool {
- self.status >= 200 && self.status < 300
- }
-
- /// Convenience shortcut to perform json deserialization of the returned body.
- pub fn json<T: for<'a> Deserialize<'a>>(&self) -> Result<T, Error> {
- Ok(serde_json::from_slice(&self.body)?)
- }
-
- /// Access the raw body as bytes.
- pub fn bytes(&self) -> &[u8] {
- &self.body
- }
-
- /// Get the returned location header. Borrowing shortcut to `self.headers.location`.
- pub fn location(&self) -> Option<&str> {
- self.headers.location.as_deref()
- }
-
- /// Convenience helper to assert that a location header was part of the response.
- pub fn location_required(&mut self) -> Result<String, Error> {
- self.headers
- .location
- .take()
- .ok_or_else(|| format_err!("missing Location header"))
- }
-}
-
-/// Contains headers from the HTTP response which are relevant parts of the Acme API.
-///
-/// Note that access to the `nonce` header is internal to this crate only, since a nonce will
-/// always be moved out of the response into the `Client` whenever a new nonce is received.
-#[derive(Default)]
-pub struct Headers {
- /// The 'Location' header usually encodes the URL where an account or order can be queried from
- /// after they were created.
- pub location: Option<String>,
- nonce: Option<String>,
-}
-
-struct Inner {
- agent: Option<ureq::Agent>,
- nonce: Option<String>,
- proxy: Option<String>,
-}
-
-impl Inner {
- fn agent(&mut self) -> Result<&mut ureq::Agent, Error> {
- if self.agent.is_none() {
- let connector = Arc::new(
- native_tls::TlsConnector::new()
- .map_err(|err| format_err!("failed to create tls connector: {}", err))?,
- );
-
- let mut builder = ureq::AgentBuilder::new().tls_connector(connector);
-
- if let Some(proxy) = self.proxy.as_deref() {
- builder = builder.proxy(
- ureq::Proxy::new(proxy)
- .map_err(|err| format_err!("failed to set proxy: {}", err))?,
- );
- }
-
- self.agent = Some(builder.build());
- }
-
- Ok(self.agent.as_mut().unwrap())
- }
-
- fn new() -> Self {
- Self {
- agent: None,
- nonce: None,
- proxy: None,
- }
- }
-
- fn execute(
- &mut self,
- method: &[u8],
- url: &str,
- request_body: Option<(&str, &[u8])>, // content-type and body
- ) -> Result<HttpResponse, Error> {
- let agent = self.agent()?;
- let req = match method {
- b"POST" => agent.post(url),
- b"GET" => agent.get(url),
- b"HEAD" => agent.head(url),
- other => bail!("invalid http method: {:?}", other),
- };
-
- let response = if let Some((content_type, body)) = request_body {
- req.set("Content-Type", content_type)
- .set("Content-Length", &body.len().to_string())
- .send_bytes(body)
- } else {
- req.call()
- }
- .map_err(|err| format_err!("http request failed: {}", err))?;
-
- let mut headers = Headers::default();
- if let Some(value) = response.header(crate::LOCATION) {
- headers.location = Some(value.to_owned());
- }
-
- if let Some(value) = response.header(crate::REPLAY_NONCE) {
- headers.nonce = Some(value.to_owned());
- }
-
- let status = response.status();
-
- let mut body = Vec::new();
- response
- .into_reader()
- .take(16 * 1024 * 1024) // arbitrary limit
- .read_to_end(&mut body)
- .map_err(|err| format_err!("failed to read response body: {}", err))?;
-
- Ok(HttpResponse {
- status,
- headers,
- body,
- })
- }
-
- pub fn set_proxy(&mut self, proxy: String) {
- self.proxy = Some(proxy);
- self.agent = None;
- }
-
- /// Low-level API to run an API request. This automatically updates the current nonce!
- fn run_request(&mut self, request: Request) -> Result<HttpResponse, Error> {
- let body = if request.body.is_empty() {
- None
- } else {
- Some((request.content_type, request.body.as_bytes()))
- };
-
- let mut response = self
- .execute(request.method.as_bytes(), &request.url, body)
- .map_err({
- // borrow fixup:
- let method = &request.method;
- let url = &request.url;
- move |err| format_err!("failed to execute {} request to {}: {}", method, url, err)
- })?;
-
- let got_nonce = self.update_nonce(&mut response)?;
-
- if response.is_success() {
- if response.status != request.expected {
- return Err(Error::InvalidApi(format!(
- "API server responded with unexpected status code: {:?}",
- response.status
- )));
- }
- return Ok(response);
- }
-
- let error: ErrorResponse = response.json().map_err(|err| {
- format_err!("error status with improper error ACME response: {}", err)
- })?;
-
- if error.ty == error::BAD_NONCE {
- if !got_nonce {
- return Err(Error::InvalidApi(
- "badNonce without a new Replay-Nonce header".to_string(),
- ));
- }
- return Err(Error::BadNonce);
- }
-
- Err(Error::Api(error))
- }
-
- /// If the response contained a nonce, update our nonce and return `true`, otherwise return
- /// `false`.
- fn update_nonce(&mut self, response: &mut HttpResponse) -> Result<bool, Error> {
- match response.headers.nonce.take() {
- Some(nonce) => {
- self.nonce = Some(nonce);
- Ok(true)
- }
- None => Ok(false),
- }
- }
-
- /// Update the nonce, if there isn't one it is an error.
- fn must_update_nonce(&mut self, response: &mut HttpResponse) -> Result<(), Error> {
- if !self.update_nonce(response)? {
- bail!("newNonce URL did not return a nonce");
- }
- Ok(())
- }
-
- /// Update the Nonce.
- fn new_nonce(&mut self, new_nonce_url: &str) -> Result<(), Error> {
- let mut response = self.execute(b"HEAD", new_nonce_url, None).map_err(|err| {
- Error::InvalidApi(format!("failed to get HEAD of newNonce URL: {}", err))
- })?;
-
- if !response.is_success() {
- bail!("HEAD on newNonce URL returned error");
- }
-
- self.must_update_nonce(&mut response)?;
-
- Ok(())
- }
-
- /// Make sure a nonce is available without forcing renewal.
- fn nonce(&mut self, new_nonce_url: &str) -> Result<&str, Error> {
- if self.nonce.is_none() {
- self.new_nonce(new_nonce_url)?;
- }
- self.nonce
- .as_deref()
- .ok_or_else(|| format_err!("failed to get nonce"))
- }
-}
-
-/// A blocking Acme client using curl's `Easy` interface.
-pub struct Client {
- inner: Inner,
- directory: Option<Directory>,
- account: Option<Account>,
- directory_url: String,
-}
-
-impl Client {
- /// Create a new Client. This has no account associated with it yet, so the next step is to
- /// either attach an existing `Account` or create a new one.
- pub fn new(directory_url: String) -> Self {
- Self {
- inner: Inner::new(),
- directory: None,
- account: None,
- directory_url,
- }
- }
-
- /// Get the directory URL without querying the `Directory` structure.
- ///
- /// The difference to [`directory`](Client::directory()) is that this does not
- /// attempt to fetch the directory data from the ACME server.
- pub fn directory_url(&self) -> &str {
- &self.directory_url
- }
-
- /// Set the account this client should use.
- pub fn set_account(&mut self, account: Account) {
- self.account = Some(account);
- }
-
- /// Get the Directory information.
- pub fn directory(&mut self) -> Result<&Directory, Error> {
- Self::get_directory(&mut self.inner, &mut self.directory, &self.directory_url)
- }
-
- /// Get the Directory information.
- fn get_directory<'a>(
- inner: &'_ mut Inner,
- directory: &'a mut Option<Directory>,
- directory_url: &str,
- ) -> Result<&'a Directory, Error> {
- if let Some(d) = directory {
- return Ok(d);
- }
-
- let response = inner
- .execute(b"GET", directory_url, None)
- .map_err(|err| Error::InvalidApi(format!("failed to get directory info: {}", err)))?;
-
- if !response.is_success() {
- bail!(
- "GET on the directory URL returned error status ({})",
- response.status
- );
- }
-
- *directory = Some(Directory::from_parts(
- directory_url.to_string(),
- response.json()?,
- ));
- Ok(directory.as_ref().unwrap())
- }
-
- /// Get the current account, if there is one.
- pub fn account(&self) -> Option<&Account> {
- self.account.as_ref()
- }
-
- /// Convenience method to get the ToS URL from the contained `Directory`.
- ///
- /// This requires mutable self as the directory information may be lazily loaded, which can
- /// fail.
- pub fn terms_of_service_url(&mut self) -> Result<Option<&str>, Error> {
- Ok(self.directory()?.terms_of_service_url())
- }
-
- /// Get a fresh nonce (this should normally not be required as nonces are updated
- /// automatically, even when a `badNonce` error occurs, which according to the ACME API
- /// specification should include a new valid nonce in its headers anyway).
- pub fn new_nonce(&mut self) -> Result<(), Error> {
- let was_none = self.inner.nonce.is_none();
- let directory =
- Self::get_directory(&mut self.inner, &mut self.directory, &self.directory_url)?;
- if was_none && self.inner.nonce.is_some() {
- // this was the first call and we already got a nonce from querying the directory
- return Ok(());
- }
-
- // otherwise actually call up to get a new nonce
- self.inner.new_nonce(directory.new_nonce_url())
- }
-
- /// borrow helper
- fn nonce<'a>(inner: &'a mut Inner, directory: &'_ Directory) -> Result<&'a str, Error> {
- inner.nonce(directory.new_nonce_url())
- }
-
- /// Convenience method to create a new account with a list of ACME compatible contact strings
- /// (eg. `mailto:someone@example.com`).
- ///
- /// Please remember to persist the returned `Account` structure somewhere to not lose access to
- /// the account!
- ///
- /// If an RSA key size is provided, an RSA key will be generated. Otherwise an EC key using the
- /// P-256 curve will be generated.
- pub fn new_account(
- &mut self,
- contact: Vec<String>,
- tos_agreed: bool,
- rsa_bits: Option<u32>,
- eab_creds: Option<(String, String)>,
- ) -> Result<&Account, Error> {
- let mut account = Account::creator()
- .set_contacts(contact)
- .agree_to_tos(tos_agreed);
- if let Some((eab_kid, eab_hmac_key)) = eab_creds {
- account = account.set_eab_credentials(eab_kid, eab_hmac_key)?;
- }
- let account = if let Some(bits) = rsa_bits {
- account.generate_rsa_key(bits)?
- } else {
- account.generate_ec_key()?
- };
-
- self.register_account(account)
- }
-
- /// Register an ACME account.
- ///
- /// This uses an [`AccountCreator`](crate::account::AccountCreator) since it may need to build
- /// the request multiple times in case the we get a `BadNonce` error.
- pub fn register_account(
- &mut self,
- account: crate::account::AccountCreator,
- ) -> Result<&Account, Error> {
- let mut retry = retry();
- let mut response = loop {
- retry.tick()?;
-
- let directory =
- Self::get_directory(&mut self.inner, &mut self.directory, &self.directory_url)?;
- let nonce = Self::nonce(&mut self.inner, directory)?;
- let request = account.request(directory, nonce)?;
- match self.run_request(request) {
- Ok(response) => break response,
- Err(err) if err.is_bad_nonce() => continue,
- Err(err) => return Err(err),
- }
- };
-
- let account = account.response(response.location_required()?, response.bytes().as_ref())?;
-
- self.account = Some(account);
- Ok(self.account.as_ref().unwrap())
- }
-
- fn need_account(account: &Option<Account>) -> Result<&Account, Error> {
- account
- .as_ref()
- .ok_or_else(|| format_err!("cannot use client without an account"))
- }
-
- /// Update account data.
- ///
- /// Low-level version: we allow arbitrary data to be passed to the remote here, it's up to the
- /// user to know what to do for now.
- pub fn update_account<T: Serialize>(&mut self, data: &T) -> Result<&Account, Error> {
- let account = Self::need_account(&self.account)?;
-
- let mut retry = retry();
- let response = loop {
- retry.tick()?;
- let directory =
- Self::get_directory(&mut self.inner, &mut self.directory, &self.directory_url)?;
- let nonce = Self::nonce(&mut self.inner, directory)?;
- let request = account.post_request(&account.location, nonce, data)?;
- let response = match self.inner.run_request(request) {
- Ok(response) => response,
- Err(err) if err.is_bad_nonce() => continue,
- Err(err) => return Err(err),
- };
-
- break response;
- };
-
- // unwrap: we asserted we have an account at the top of the method!
- let account = self.account.as_mut().unwrap();
- account.data = response.json()?;
- Ok(account)
- }
-
- /// Method to create a new order for a set of domains.
- ///
- /// Please remember to persist the order somewhere (ideally along with the account data) in
- /// order to finish & query it later on.
- pub fn new_order(&mut self, domains: Vec<String>) -> Result<Order, Error> {
- let account = Self::need_account(&self.account)?;
-
- let order = domains
- .into_iter()
- .fold(OrderData::new(), |order, domain| order.domain(domain));
-
- let mut retry = retry();
- loop {
- retry.tick()?;
-
- let directory =
- Self::get_directory(&mut self.inner, &mut self.directory, &self.directory_url)?;
- let nonce = Self::nonce(&mut self.inner, directory)?;
- let mut new_order = account.new_order(&order, directory, nonce)?;
- let mut response = match self.inner.run_request(new_order.request.take().unwrap()) {
- Ok(response) => response,
- Err(err) if err.is_bad_nonce() => continue,
- Err(err) => return Err(err),
- };
-
- return new_order.response(response.location_required()?, response.bytes().as_ref());
- }
- }
-
- /// Assuming the provided URL is an 'Authorization' URL, get and deserialize it.
- pub fn get_authorization(&mut self, url: &str) -> Result<Authorization, Error> {
- self.post_as_get(url)?.json()
- }
-
- /// Assuming the provided URL is an 'Order' URL, get and deserialize it.
- pub fn get_order(&mut self, url: &str) -> Result<OrderData, Error> {
- self.post_as_get(url)?.json()
- }
-
- /// Low level "POST-as-GET" request.
- pub fn post_as_get(&mut self, url: &str) -> Result<HttpResponse, Error> {
- let account = Self::need_account(&self.account)?;
-
- let mut retry = retry();
- loop {
- retry.tick()?;
-
- let directory =
- Self::get_directory(&mut self.inner, &mut self.directory, &self.directory_url)?;
- let nonce = Self::nonce(&mut self.inner, directory)?;
- let request = account.get_request(url, nonce)?;
- match self.inner.run_request(request) {
- Ok(response) => return Ok(response),
- Err(err) if err.is_bad_nonce() => continue,
- Err(err) => return Err(err),
- }
- }
- }
-
- /// Low level POST request.
- pub fn post<T: Serialize>(&mut self, url: &str, data: &T) -> Result<HttpResponse, Error> {
- let account = Self::need_account(&self.account)?;
-
- let mut retry = retry();
- loop {
- retry.tick()?;
-
- let directory =
- Self::get_directory(&mut self.inner, &mut self.directory, &self.directory_url)?;
- let nonce = Self::nonce(&mut self.inner, directory)?;
- let request = account.post_request(url, nonce, data)?;
- match self.inner.run_request(request) {
- Ok(response) => return Ok(response),
- Err(err) if err.is_bad_nonce() => continue,
- Err(err) => return Err(err),
- }
- }
- }
-
- /// Request challenge validation. Afterwards, the challenge should be polled.
- pub fn request_challenge_validation(&mut self, url: &str) -> Result<Challenge, Error> {
- self.post(url, &serde_json::json!({}))?.json()
- }
-
- /// Shortcut to `account().ok_or_else(...).key_authorization()`.
- pub fn key_authorization(&self, token: &str) -> Result<String, Error> {
- Self::need_account(&self.account)?.key_authorization(token)
- }
-
- /// Shortcut to `account().ok_or_else(...).dns_01_txt_value()`.
- /// the key authorization value.
- pub fn dns_01_txt_value(&self, token: &str) -> Result<String, Error> {
- Self::need_account(&self.account)?.dns_01_txt_value(token)
- }
-
- /// Low-level API to run an n API request. This automatically updates the current nonce!
- pub fn run_request(&mut self, request: Request) -> Result<HttpResponse, Error> {
- self.inner.run_request(request)
- }
-
- /// Finalize an Order via its `finalize` URL property and the DER encoded CSR.
- pub fn finalize(&mut self, url: &str, csr: &[u8]) -> Result<(), Error> {
- let csr = b64u::encode(csr);
- let data = serde_json::json!({ "csr": csr });
- self.post(url, &data)?;
- Ok(())
- }
-
- /// Download a certificate via its 'certificate' URL property.
- ///
- /// The certificate will be a PEM certificate chain.
- pub fn get_certificate(&mut self, url: &str) -> Result<Vec<u8>, Error> {
- Ok(self.post_as_get(url)?.body)
- }
-
- /// Revoke an existing certificate (PEM or DER formatted).
- pub fn revoke_certificate(
- &mut self,
- certificate: &[u8],
- reason: Option<u32>,
- ) -> Result<(), Error> {
- // TODO: This can also work without an account.
- let account = Self::need_account(&self.account)?;
-
- let revocation = account.revoke_certificate(certificate, reason)?;
-
- let mut retry = retry();
- loop {
- retry.tick()?;
-
- let directory =
- Self::get_directory(&mut self.inner, &mut self.directory, &self.directory_url)?;
- let nonce = Self::nonce(&mut self.inner, directory)?;
- let request = revocation.request(directory, nonce)?;
- match self.inner.run_request(request) {
- Ok(_response) => return Ok(()),
- Err(err) if err.is_bad_nonce() => continue,
- Err(err) => return Err(err),
- }
- }
- }
-
- /// Set a proxy
- pub fn set_proxy(&mut self, proxy: String) {
- self.inner.set_proxy(proxy)
- }
-}
-
-/// bad nonce retry count helper
-struct Retry(usize);
-
-const fn retry() -> Retry {
- Retry(0)
-}
-
-impl Retry {
- fn tick(&mut self) -> Result<(), Error> {
- if self.0 >= 3 {
- bail!("kept getting a badNonce error!");
- }
- self.0 += 1;
- Ok(())
- }
-}
+++ /dev/null
-//! ACME Directory information.
-
-use serde::{Deserialize, Serialize};
-
-/// An ACME Directory. This contains the base URL and the directory data as received via a `GET`
-/// request to the URL.
-pub struct Directory {
- /// The main entry point URL to the ACME directory.
- pub url: String,
-
- /// The json structure received via a `GET` request to the directory URL. This contains the
- /// URLs for various API entry points.
- pub data: DirectoryData,
-}
-
-/// The ACME Directory object structure.
-///
-/// The data in here is typically not relevant to the user of this crate.
-#[derive(Deserialize, Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct DirectoryData {
- /// The entry point to create a new account.
- pub new_account: String,
-
- /// The entry point to retrieve a new nonce, should be used with a `HEAD` request.
- pub new_nonce: String,
-
- /// URL to post new orders to.
- pub new_order: String,
-
- /// URL to use for certificate revocation.
- pub revoke_cert: String,
-
- /// Account key rollover URL.
- pub key_change: String,
-
- /// Metadata object, for additional information which aren't directly part of the API
- /// itself, such as the terms of service.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub meta: Option<Meta>,
-}
-
-/// The directory's "meta" object.
-#[derive(Clone, Debug, Deserialize, Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct Meta {
- /// The terms of service. This is typically in the form of an URL.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub terms_of_service: Option<String>,
-
- /// Flag indicating if EAB is required, None is equivalent to false
- #[serde(skip_serializing_if = "Option::is_none")]
- pub external_account_required: Option<bool>,
-
- /// Website with information about the ACME Server
- #[serde(skip_serializing_if = "Option::is_none")]
- pub website: Option<String>,
-
- /// List of hostnames used by the CA, intended for the use with caa dns records
- #[serde(default, skip_serializing_if = "Vec::is_empty")]
- pub caa_identities: Vec<String>,
-}
-
-impl Directory {
- /// Create a `Directory` given the parsed `DirectoryData` of a `GET` request to the directory
- /// URL.
- pub fn from_parts(url: String, data: DirectoryData) -> Self {
- Self { url, data }
- }
-
- /// Get the ToS URL.
- pub fn terms_of_service_url(&self) -> Option<&str> {
- match &self.data.meta {
- Some(meta) => meta.terms_of_service.as_deref(),
- None => None,
- }
- }
-
- /// Get if external account binding is required
- pub fn external_account_binding_required(&self) -> bool {
- matches!(
- &self.data.meta,
- Some(Meta {
- external_account_required: Some(true),
- ..
- })
- )
- }
-
- /// Get the "newNonce" URL. Use `HEAD` requests on this to get a new nonce.
- pub fn new_nonce_url(&self) -> &str {
- &self.data.new_nonce
- }
-
- pub(crate) fn new_account_url(&self) -> &str {
- &self.data.new_account
- }
-
- pub(crate) fn new_order_url(&self) -> &str {
- &self.data.new_order
- }
-
- /// Access to the in the Acme spec defined metadata structure.
- pub fn meta(&self) -> Option<&Meta> {
- self.data.meta.as_ref()
- }
-}
+++ /dev/null
-use openssl::hash::MessageDigest;
-use openssl::pkey::{HasPrivate, PKeyRef};
-use openssl::sign::Signer;
-use serde::{Deserialize, Serialize};
-
-use crate::key::Jwk;
-use crate::{b64u, Error};
-
-#[derive(Debug, Serialize)]
-#[serde(rename_all = "camelCase")]
-struct Protected {
- alg: &'static str,
- url: String,
- kid: String,
-}
-
-#[derive(Debug, Serialize, Deserialize, Clone)]
-#[serde(rename_all = "camelCase")]
-pub struct ExternalAccountBinding {
- protected: String,
- payload: String,
- signature: String,
-}
-
-impl ExternalAccountBinding {
- pub fn new<P>(
- eab_kid: &str,
- eab_hmac_key: &PKeyRef<P>,
- jwk: Jwk,
- url: String,
- ) -> Result<Self, Error>
- where
- P: HasPrivate,
- {
- let protected = Protected {
- alg: "HS256",
- kid: eab_kid.to_string(),
- url,
- };
- let payload = b64u::encode(serde_json::to_string(&jwk)?.as_bytes());
- let protected_data = b64u::encode(serde_json::to_string(&protected)?.as_bytes());
- let signature = {
- let protected = protected_data.as_bytes();
- let payload = payload.as_bytes();
- Self::sign_hmac(eab_hmac_key, protected, payload)?
- };
-
- let signature = b64u::encode(&signature);
- Ok(ExternalAccountBinding {
- protected: protected_data,
- payload,
- signature,
- })
- }
-
- fn sign_hmac<P>(key: &PKeyRef<P>, protected: &[u8], payload: &[u8]) -> Result<Vec<u8>, Error>
- where
- P: HasPrivate,
- {
- let mut signer = Signer::new(MessageDigest::sha256(), key)?;
- signer.update(protected)?;
- signer.update(b".")?;
- signer.update(payload)?;
- Ok(signer.sign_to_vec()?)
- }
-}
+++ /dev/null
-//! The `Error` type and some ACME error constants for reference.
-
-use std::fmt;
-
-use openssl::error::ErrorStack as SslErrorStack;
-
-/// The ACME error string for a "bad nonce" error.
-pub const BAD_NONCE: &str = "urn:ietf:params:acme:error:badNonce";
-
-/// The ACME error string for a "user action required" error.
-pub const USER_ACTION_REQUIRED: &str = "urn:ietf:params:acme:error:userActionRequired";
-
-/// Error types returned by this crate.
-#[derive(Debug)]
-#[must_use = "unused errors have no effect"]
-pub enum Error {
- /// A `badNonce` API response. The request should be retried with the new nonce received along
- /// with this response.
- BadNonce,
-
- /// A `userActionRequired` API response. Typically this means there was a change to the ToS and
- /// the user has to agree to the new terms.
- UserActionRequired(String),
-
- /// Other error repsonses from the Acme API not handled specially.
- Api(crate::request::ErrorResponse),
-
- /// The Acme API behaved unexpectedly.
- InvalidApi(String),
-
- /// Tried to use an `Account` or `AccountCreator` without a private key.
- MissingKey,
-
- /// Tried to create an `Account` without providing a single contact info.
- MissingContactInfo,
-
- /// Tried to use an empty `Order`.
- EmptyOrder,
-
- /// A raw `openssl::PKey` containing an unsupported key was passed.
- UnsupportedKeyType,
-
- /// A raw `openssl::PKey` or `openssl::EcKey` with an unsupported curve was passed.
- UnsupportedGroup,
-
- /// Failed to parse the account data returned by the API upon account creation.
- BadAccountData(String),
-
- /// Failed to parse the order data returned by the API from a new-order request.
- BadOrderData(String),
-
- /// An openssl error occurred during a crypto operation.
- RawSsl(SslErrorStack),
-
- /// An openssl error occurred during a crypto operation.
- /// With some textual context.
- Ssl(&'static str, SslErrorStack),
-
- /// An otherwise uncaught serde error happened.
- Json(serde_json::Error),
-
- /// Failed to parse
- BadBase64(base64::DecodeError),
-
- /// Can be used by the user for textual error messages without having to downcast to regular
- /// acme errors.
- Custom(String),
-
- /// If built with the `client` feature, this is where general ureq/network errors end up.
- /// This is usually a `ureq::Error`, however in order to provide an API which is not
- /// feature-dependent, this variant is always present and contains a boxed `dyn Error`.
- HttpClient(Box<dyn std::error::Error + Send + Sync + 'static>),
-
- /// If built with the `client` feature, this is where client specific errors which are not from
- /// errors forwarded from `ureq` end up.
- Client(String),
-
- /// A non-openssl error occurred while building data for the CSR.
- Csr(String),
-}
-
-impl Error {
- /// Create an `Error` from a custom text.
- pub fn custom<T: std::fmt::Display>(s: T) -> Self {
- Error::Custom(s.to_string())
- }
-
- /// Convenience method to check if this error represents a bad nonce error in which case the
- /// request needs to be re-created using a new nonce.
- pub fn is_bad_nonce(&self) -> bool {
- matches!(self, Error::BadNonce)
- }
-}
-
-impl std::error::Error for Error {}
-
-impl fmt::Display for Error {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- match self {
- Error::Api(err) => match err.detail.as_deref() {
- Some(detail) => write!(f, "{}: {}", err.ty, detail),
- None => fmt::Display::fmt(&err.ty, f),
- },
- Error::InvalidApi(err) => write!(f, "Acme Server API misbehaved: {}", err),
- Error::BadNonce => f.write_str("bad nonce, please retry with a new nonce"),
- Error::UserActionRequired(err) => write!(f, "user action required: {}", err),
- Error::MissingKey => f.write_str("cannot build an account without a key"),
- Error::MissingContactInfo => f.write_str("account requires contact info"),
- Error::EmptyOrder => f.write_str("cannot make an empty order"),
- Error::UnsupportedKeyType => f.write_str("unsupported key type"),
- Error::UnsupportedGroup => f.write_str("unsupported EC group"),
- Error::BadAccountData(err) => {
- write!(f, "bad response to account query or creation: {}", err)
- }
- Error::BadOrderData(err) => {
- write!(f, "bad response to new-order query or creation: {}", err)
- }
- Error::RawSsl(err) => fmt::Display::fmt(err, f),
- Error::Ssl(context, err) => {
- write!(f, "{}: {}", context, err)
- }
- Error::Json(err) => fmt::Display::fmt(err, f),
- Error::Custom(err) => fmt::Display::fmt(err, f),
- Error::HttpClient(err) => fmt::Display::fmt(err, f),
- Error::Client(err) => fmt::Display::fmt(err, f),
- Error::Csr(err) => fmt::Display::fmt(err, f),
- Error::BadBase64(err) => fmt::Display::fmt(err, f),
- }
- }
-}
-
-impl From<SslErrorStack> for Error {
- fn from(e: SslErrorStack) -> Self {
- Error::RawSsl(e)
- }
-}
-
-impl From<serde_json::Error> for Error {
- fn from(e: serde_json::Error) -> Self {
- Error::Json(e)
- }
-}
-
-impl From<crate::request::ErrorResponse> for Error {
- fn from(e: crate::request::ErrorResponse) -> Self {
- Error::Api(e)
- }
-}
-
-impl From<base64::DecodeError> for Error {
- fn from(e: base64::DecodeError) -> Self {
- Error::BadBase64(e)
- }
-}
+++ /dev/null
-use openssl::hash::Hasher;
-use serde_json::Value;
-
-use crate::Error;
-
-pub fn to_hash_canonical(value: &Value, output: &mut Hasher) -> Result<(), Error> {
- match value {
- Value::Null | Value::String(_) | Value::Number(_) | Value::Bool(_) => {
- serde_json::to_writer(output, &value)?;
- }
- Value::Array(list) => {
- output.update(b"[")?;
- let mut iter = list.iter();
- if let Some(item) = iter.next() {
- to_hash_canonical(item, output)?;
- for item in iter {
- output.update(b",")?;
- to_hash_canonical(item, output)?;
- }
- }
- output.update(b"]")?;
- }
- Value::Object(map) => {
- output.update(b"{")?;
- let mut keys: Vec<&str> = map.keys().map(String::as_str).collect();
- keys.sort_unstable();
- let mut iter = keys.into_iter();
- if let Some(key) = iter.next() {
- serde_json::to_writer(&mut *output, &key)?;
- output.update(b":")?;
- to_hash_canonical(&map[key], output)?;
- for key in iter {
- output.update(b",")?;
- serde_json::to_writer(&mut *output, &key)?;
- output.update(b":")?;
- to_hash_canonical(&map[key], output)?;
- }
- }
- output.update(b"}")?;
- }
- }
- Ok(())
-}
+++ /dev/null
-use std::convert::TryFrom;
-
-use openssl::hash::{Hasher, MessageDigest};
-use openssl::pkey::{HasPrivate, PKeyRef};
-use openssl::sign::Signer;
-use serde::Serialize;
-
-use crate::b64u;
-use crate::key::{Jwk, PublicKey};
-use crate::Error;
-
-#[derive(Debug, Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct Protected {
- alg: &'static str,
- nonce: String,
- url: String,
- #[serde(flatten)]
- key: KeyId,
-}
-
-/// Acme requires to the use of *either* `jwk` *or* `kid` depending on the action taken.
-#[derive(Debug, Serialize)]
-#[serde(rename_all = "camelCase")]
-pub enum KeyId {
- /// This is the actual JWK structure.
- Jwk(Jwk),
-
- /// This should be the account location.
- Kid(String),
-}
-
-#[derive(Debug, Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct Jws {
- protected: String,
- payload: String,
- signature: String,
-}
-
-impl Jws {
- pub fn new<P, T>(
- key: &PKeyRef<P>,
- location: Option<String>,
- url: String,
- nonce: String,
- payload: &T,
- ) -> Result<Self, Error>
- where
- P: HasPrivate,
- T: Serialize,
- {
- Self::new_full(
- key,
- location,
- url,
- nonce,
- b64u::encode(serde_json::to_string(payload)?.as_bytes()),
- )
- }
-
- pub fn new_full<P: HasPrivate>(
- key: &PKeyRef<P>,
- location: Option<String>,
- url: String,
- nonce: String,
- payload: String,
- ) -> Result<Self, Error> {
- let jwk = Jwk::try_from(key)?;
-
- let pubkey = jwk.key.clone();
- let mut protected = Protected {
- alg: "",
- nonce,
- url,
- key: match location {
- Some(location) => KeyId::Kid(location),
- None => KeyId::Jwk(jwk),
- },
- };
-
- let (digest, ec_order_bytes): (MessageDigest, usize) = match &pubkey {
- PublicKey::Rsa(_) => (Self::prepare_rsa(key, &mut protected), 0),
- PublicKey::Ec(_) => Self::prepare_ec(key, &mut protected),
- };
-
- let protected_data = b64u::encode(serde_json::to_string(&protected)?.as_bytes());
-
- let signature = {
- let prot = protected_data.as_bytes();
- let payload = payload.as_bytes();
- match &pubkey {
- PublicKey::Rsa(_) => Self::sign_rsa(key, digest, prot, payload),
- PublicKey::Ec(_) => Self::sign_ec(key, digest, ec_order_bytes, prot, payload),
- }?
- };
-
- let signature = b64u::encode(&signature);
-
- Ok(Jws {
- protected: protected_data,
- payload,
- signature,
- })
- }
-
- fn prepare_rsa<P>(_key: &PKeyRef<P>, protected: &mut Protected) -> MessageDigest
- where
- P: HasPrivate,
- {
- protected.alg = "RS256";
- MessageDigest::sha256()
- }
-
- /// Returns the digest and the size of the two signature components 'r' and 's'.
- fn prepare_ec<P>(_key: &PKeyRef<P>, protected: &mut Protected) -> (MessageDigest, usize)
- where
- P: HasPrivate,
- {
- // Note: if we support >256 bit keys we'll want to also support using ES512 here probably
- protected.alg = "ES256";
- // 'r' and 's' are each 256 bit numbers:
- (MessageDigest::sha256(), 32)
- }
-
- fn sign_rsa<P>(
- key: &PKeyRef<P>,
- digest: MessageDigest,
- protected: &[u8],
- payload: &[u8],
- ) -> Result<Vec<u8>, Error>
- where
- P: HasPrivate,
- {
- let mut signer = Signer::new(digest, key)?;
- signer.set_rsa_padding(openssl::rsa::Padding::PKCS1)?;
- signer.update(protected)?;
- signer.update(b".")?;
- signer.update(payload)?;
- Ok(signer.sign_to_vec()?)
- }
-
- fn sign_ec<P>(
- key: &PKeyRef<P>,
- digest: MessageDigest,
- ec_order_bytes: usize,
- protected: &[u8],
- payload: &[u8],
- ) -> Result<Vec<u8>, Error>
- where
- P: HasPrivate,
- {
- let mut hasher = Hasher::new(digest)?;
- hasher.update(protected)?;
- hasher.update(b".")?;
- hasher.update(payload)?;
- let sig =
- openssl::ecdsa::EcdsaSig::sign(hasher.finish()?.as_ref(), key.ec_key()?.as_ref())?;
- let r = sig.r().to_vec();
- let s = sig.s().to_vec();
- let mut out = Vec::with_capacity(ec_order_bytes * 2);
- out.extend(std::iter::repeat(0u8).take(ec_order_bytes - r.len()));
- out.extend(r);
- out.extend(std::iter::repeat(0u8).take(ec_order_bytes - s.len()));
- out.extend(s);
- Ok(out)
- }
-}
+++ /dev/null
-use std::convert::{TryFrom, TryInto};
-
-use openssl::hash::{Hasher, MessageDigest};
-use openssl::pkey::{HasPublic, Id, PKeyRef};
-use serde::Serialize;
-
-use crate::b64u;
-use crate::Error;
-
-/// An RSA public key.
-#[derive(Clone, Debug, Serialize)]
-#[serde(deny_unknown_fields)]
-pub struct RsaPublicKey {
- #[serde(with = "b64u::bytes")]
- e: Vec<u8>,
- #[serde(with = "b64u::bytes")]
- n: Vec<u8>,
-}
-
-/// An EC public key.
-#[derive(Clone, Debug, Serialize)]
-#[serde(deny_unknown_fields)]
-pub struct EcPublicKey {
- crv: &'static str,
- #[serde(with = "b64u::bytes")]
- x: Vec<u8>,
- #[serde(with = "b64u::bytes")]
- y: Vec<u8>,
-}
-
-/// A public key.
-///
-/// Internally tagged, so this already contains the 'kty' member.
-#[derive(Clone, Debug, Serialize)]
-#[serde(tag = "kty")]
-pub enum PublicKey {
- #[serde(rename = "RSA")]
- Rsa(RsaPublicKey),
- #[serde(rename = "EC")]
- Ec(EcPublicKey),
-}
-
-impl PublicKey {
- /// The thumbprint is the b64u encoded sha256sum of the *canonical* json representation.
- pub fn thumbprint(&self) -> Result<String, Error> {
- let mut hasher = Hasher::new(MessageDigest::sha256())?;
- crate::json::to_hash_canonical(&serde_json::to_value(self)?, &mut hasher)?;
- Ok(b64u::encode(hasher.finish()?.as_ref()))
- }
-}
-
-#[derive(Clone, Debug, Serialize)]
-pub struct Jwk {
- #[serde(rename = "use", skip_serializing_if = "Option::is_none")]
- pub usage: Option<String>,
-
- /// The key data is internally tagged, we can just flatten it.
- #[serde(flatten)]
- pub key: PublicKey,
-}
-
-impl<P: HasPublic> TryFrom<&PKeyRef<P>> for Jwk {
- type Error = Error;
-
- fn try_from(key: &PKeyRef<P>) -> Result<Self, Self::Error> {
- Ok(Self {
- key: key.try_into()?,
- usage: None,
- })
- }
-}
-
-impl<P: HasPublic> TryFrom<&PKeyRef<P>> for PublicKey {
- type Error = Error;
-
- fn try_from(key: &PKeyRef<P>) -> Result<Self, Self::Error> {
- match key.id() {
- Id::RSA => Ok(PublicKey::Rsa(RsaPublicKey::try_from(&key.rsa()?)?)),
- Id::EC => Ok(PublicKey::Ec(EcPublicKey::try_from(&key.ec_key()?)?)),
- _ => Err(Error::UnsupportedKeyType),
- }
- }
-}
-
-impl<P: HasPublic> TryFrom<&openssl::rsa::Rsa<P>> for RsaPublicKey {
- type Error = Error;
-
- fn try_from(key: &openssl::rsa::Rsa<P>) -> Result<Self, Self::Error> {
- Ok(RsaPublicKey {
- e: key.e().to_vec(),
- n: key.n().to_vec(),
- })
- }
-}
-
-impl<P: HasPublic> TryFrom<&openssl::ec::EcKey<P>> for EcPublicKey {
- type Error = Error;
-
- fn try_from(key: &openssl::ec::EcKey<P>) -> Result<Self, Self::Error> {
- let group = key.group();
-
- if group.curve_name() != Some(openssl::nid::Nid::X9_62_PRIME256V1) {
- return Err(Error::UnsupportedGroup);
- }
-
- let mut ctx = openssl::bn::BigNumContext::new()?;
- let mut x = openssl::bn::BigNum::new()?;
- let mut y = openssl::bn::BigNum::new()?;
- key.public_key()
- .affine_coordinates(group, &mut x, &mut y, &mut ctx)?;
-
- Ok(EcPublicKey {
- crv: "P-256",
- x: x.to_vec(),
- y: y.to_vec(),
- })
- }
-}
-
-#[test]
-fn test_key_conversion() -> Result<(), Error> {
- let key = openssl::ec::EcKey::generate(
- openssl::ec::EcGroup::from_curve_name(openssl::nid::Nid::X9_62_PRIME256V1)?.as_ref(),
- )?;
-
- let _ = EcPublicKey::try_from(&key).expect("failed to jsonify ec key");
-
- Ok(())
-}
+++ /dev/null
-//! ACME protocol helper.
-//!
-//! This is supposed to implement the low level parts of the ACME protocol, providing an [`Account`]
-//! and some other helper types which allow interacting with an ACME server by implementing methods
-//! which create [`Request`]s the user can then combine with a nonce and send to the the ACME
-//! server using whatever http client they choose.
-//!
-//! This is a rather low level crate, and while it provides an optional synchronous client using
-//! curl (for simplicity), users should have basic understanding of the ACME API in order to
-//! implement a client using this.
-//!
-//! The [`Account`] helper supports RSA and ECC keys and provides most of the API methods.
-
-#![deny(missing_docs)]
-
-mod b64u;
-mod eab;
-mod json;
-mod jws;
-mod key;
-mod request;
-
-pub mod account;
-pub mod authorization;
-pub mod directory;
-pub mod error;
-pub mod order;
-pub mod util;
-
-#[doc(inline)]
-pub use account::Account;
-
-#[doc(inline)]
-pub use authorization::{Authorization, Challenge};
-
-#[doc(inline)]
-pub use directory::Directory;
-
-#[doc(inline)]
-pub use error::Error;
-
-#[doc(inline)]
-pub use order::Order;
-
-#[doc(inline)]
-pub use request::Request;
-
-// we don't inline these:
-pub use order::NewOrder;
-pub use request::ErrorResponse;
-
-/// Header name for nonces.
-pub const REPLAY_NONCE: &str = "Replay-Nonce";
-
-/// Header name for locations.
-pub const LOCATION: &str = "Location";
-
-#[cfg(feature = "client")]
-pub mod client;
-#[cfg(feature = "client")]
-pub use client::Client;
+++ /dev/null
-//! ACME Orders data and identifiers.
-
-use serde::{Deserialize, Serialize};
-use serde_json::Value;
-
-use crate::request::Request;
-use crate::Error;
-
-/// Status of an [`Order`].
-#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
-#[serde(rename_all = "lowercase")]
-pub enum Status {
- /// Invalid, used as a place holder for when sending objects as contrary to account creation,
- /// the Acme RFC does not require the server to ignore unknown parts of the `Order` object.
- New,
-
- /// Authorization failed and it is now invalid.
- Invalid,
-
- /// The authorization is pending and the user should look through its challenges.
- ///
- /// This is the initial state of a new authorization.
- Pending,
-
- /// The ACME provider is processing an authorization validation.
- Processing,
-
- /// The requirements for the order have been met and it may be finalized.
- Ready,
-
- /// The certificate has been issued and can be downloaded from the URL provided in the
- /// [`Order`]'s `certificate` field.
- Valid,
-}
-
-impl Default for Status {
- fn default() -> Self {
- Status::New
- }
-}
-
-impl Status {
- /// Serde helper
- fn is_new(&self) -> bool {
- *self == Status::New
- }
-
- /// Convenience method to check if the status is 'pending'.
- #[inline]
- pub fn is_pending(self) -> bool {
- self == Status::Pending
- }
-
- /// Convenience method to check if the status is 'valid'.
- #[inline]
- pub fn is_valid(self) -> bool {
- self == Status::Valid
- }
-}
-
-/// An identifier used for a certificate request.
-///
-/// Currently only supports DNS name identifiers.
-#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
-#[serde(tag = "type", content = "value", rename_all = "lowercase")]
-pub enum Identifier {
- /// A DNS identifier is used to request a domain name to be added to a certificate.
- Dns(String),
-}
-
-/// This contains the order data sent to and received from the ACME server.
-///
-/// This is typically filled with a set of domains and then issued as a new-order request via [`Account::new_order`](crate::Account::new_order).
-#[derive(Clone, Debug, Default, Deserialize, Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct OrderData {
- /// The order status.
- #[serde(skip_serializing_if = "Status::is_new", default)]
- pub status: Status,
-
- /// This order's expiration date as RFC3339 formatted time string.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub expires: Option<String>,
-
- /// List of identifiers to order for the certificate.
- pub identifiers: Vec<Identifier>,
-
- /// An RFC3339 formatted time string. It is up to the user to choose a dev dependency for this
- /// shit.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub not_before: Option<String>,
-
- /// An RFC3339 formatted time string. It is up to the user to choose a dev dependency for this
- /// shit.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub not_after: Option<String>,
-
- /// Possible errors in this order.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub error: Option<Value>,
-
- /// List of URL's to authorizations the client needs to complete.
- #[serde(skip_serializing_if = "Vec::is_empty")]
- pub authorizations: Vec<String>,
-
- /// URL the final CSR needs to be POSTed to in order to complete the order, once all
- /// authorizations have been performed.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub finalize: Option<String>,
-
- /// URL at which the issued certificate can be fetched once it is available.
- #[serde(skip_serializing_if = "Option::is_none")]
- pub certificate: Option<String>,
-}
-
-impl OrderData {
- /// Initialize an empty order object.
- pub fn new() -> Self {
- Default::default()
- }
-
- /// Builder-style method to add a domain identifier to the data.
- pub fn domain(mut self, domain: String) -> Self {
- self.identifiers.push(Identifier::Dns(domain));
- self
- }
-}
-
-/// Represents an order for a new certificate. This combines the order's own location (URL) with
-/// the [`OrderData`] received from the ACME server.
-#[derive(Deserialize, Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct Order {
- /// Order location URL.
- pub location: String,
-
- /// The order's data object.
- pub data: OrderData,
-}
-
-impl Order {
- /// Get an authorization URL (or `None` if the index is out of range).
- pub fn authorization(&self, index: usize) -> Option<&str> {
- Some(self.data.authorizations.get(index)?)
- }
-
- /// Get the number of authorizations in this object.
- pub fn authorization_len(&self) -> usize {
- self.data.authorizations.len()
- }
-}
-
-/// Represents a new in-flight order creation.
-///
-/// This is created via [`Account::new_order`](crate::Account::new_order()).
-pub struct NewOrder {
- //order: OrderData,
- /// The request to execute to place the order. When creating a [`NewOrder`] via
- /// [`Account::new_order`](crate::Account::new_order) this is guaranteed to be `Some`.
- pub request: Option<Request>,
-}
-
-impl NewOrder {
- pub(crate) fn new(request: Request) -> Self {
- Self {
- //order,
- request: Some(request),
- }
- }
-
- /// Deal with the response we got from the server.
- pub fn response(self, location_header: String, response_body: &[u8]) -> Result<Order, Error> {
- Ok(Order {
- location: location_header,
- data: serde_json::from_slice(response_body)
- .map_err(|err| Error::BadOrderData(err.to_string()))?,
- })
- }
-}
+++ /dev/null
-use serde::Deserialize;
-
-pub(crate) const JSON_CONTENT_TYPE: &str = "application/jose+json";
-pub(crate) const CREATED: u16 = 201;
-
-/// A request which should be performed on the ACME provider.
-pub struct Request {
- /// The complete URL to send the request to.
- pub url: String,
-
- /// The HTTP method name to use.
- pub method: &'static str,
-
- /// The `Content-Type` header to pass along.
- pub content_type: &'static str,
-
- /// The body to pass along with request, or an empty string.
- pub body: String,
-
- /// The expected status code a compliant ACME provider will return on success.
- pub expected: u16,
-}
-
-/// An ACME error response contains a specially formatted type string, and can optionally
-/// contain textual details and a set of sub problems.
-#[derive(Clone, Debug, Deserialize)]
-pub struct ErrorResponse {
- /// The ACME error type string.
- ///
- /// Most of the time we're only interested in the "bad nonce" or "user action required"
- /// errors. When an [`Error`](crate::Error) is built from this error response, it will map
- /// to the corresponding enum values (eg. [`Error::BadNonce`](crate::Error::BadNonce)).
- #[serde(rename = "type")]
- pub ty: String,
-
- /// A textual detail string optionally provided by the ACME provider to inform the user more
- /// verbosely about why the error occurred.
- pub detail: Option<String>,
-
- /// Additional json data containing information as to why the error occurred.
- pub subproblems: Option<serde_json::Value>,
-}
+++ /dev/null
-//! Certificate utility methods for convenience (such as CSR generation).
-
-use std::collections::HashMap;
-
-use openssl::hash::MessageDigest;
-use openssl::nid::Nid;
-use openssl::pkey::PKey;
-use openssl::rsa::Rsa;
-use openssl::x509::{self, X509Name, X509Req};
-
-use crate::Error;
-
-/// A certificate signing request.
-pub struct Csr {
- /// DER encoded certificate request.
- pub data: Vec<u8>,
-
- /// PEM formatted PKCS#8 private key.
- pub private_key_pem: Vec<u8>,
-}
-
-impl Csr {
- /// Generate a CSR in DER format with a PEM formatted PKCS8 private key.
- ///
- /// The `identifiers` should be a list of domains. The `attributes` should have standard names
- /// recognized by openssl.
- pub fn generate(
- identifiers: &[impl AsRef<str>],
- attributes: &HashMap<String, &str>,
- ) -> Result<Self, Error> {
- if identifiers.is_empty() {
- return Err(Error::Csr("cannot generate empty CSR".to_string()));
- }
-
- let private_key = Rsa::generate(4096)
- .and_then(PKey::from_rsa)
- .map_err(|err| Error::Ssl("failed to generate RSA key: {}", err))?;
-
- let private_key_pem = private_key
- .private_key_to_pem_pkcs8()
- .map_err(|err| Error::Ssl("failed to format private key as PEM pkcs8: {}", err))?;
-
- let mut name = X509Name::builder()?;
- if !attributes.contains_key("CN") {
- name.append_entry_by_nid(Nid::COMMONNAME, identifiers[0].as_ref())?;
- }
- for (key, value) in attributes {
- name.append_entry_by_text(key, value)?;
- }
- let name = name.build();
-
- let mut csr = X509Req::builder()?;
- csr.set_subject_name(&name)?;
- csr.set_pubkey(&private_key)?;
-
- let context = csr.x509v3_context(None);
- let mut ext = openssl::stack::Stack::new()?;
- ext.push(x509::extension::BasicConstraints::new().build()?)?;
- ext.push(
- x509::extension::KeyUsage::new()
- .digital_signature()
- .key_encipherment()
- .build()?,
- )?;
- ext.push(
- x509::extension::ExtendedKeyUsage::new()
- .server_auth()
- .client_auth()
- .build()?,
- )?;
- let mut san = x509::extension::SubjectAlternativeName::new();
- for dns in identifiers {
- san.dns(dns.as_ref());
- }
- ext.push({ san }.build(&context)?)?;
- csr.add_extensions(&ext)?;
-
- csr.sign(&private_key, MessageDigest::sha256())?;
-
- Ok(Self {
- data: csr.build().to_der()?,
- private_key_pem,
- })
- }
-}