mirror of
git://git.proxmox.com/git/proxmox-acme-rs.git
synced 2025-03-10 16:58:42 +03:00
This repository was moved into the proxmox.git
repository
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
parent
6e1e835739
commit
4441cfca8e
@ -1,5 +0,0 @@
|
||||
[source]
|
||||
[source.debian-packages]
|
||||
directory = "/usr/share/cargo/registry"
|
||||
[source.crates-io]
|
||||
replace-with = "debian-packages"
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,2 +0,0 @@
|
||||
/target
|
||||
Cargo.lock
|
33
Cargo.toml
33
Cargo.toml
@ -1,33 +0,0 @@
|
||||
[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"
|
45
Makefile
45
Makefile
@ -1,45 +0,0 @@
|
||||
.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
|
1
README
Normal file
1
README
Normal file
@ -0,0 +1 @@
|
||||
This repository was moved into the `proxmox.git` repository.
|
85
debian/changelog
vendored
85
debian/changelog
vendored
@ -1,85 +0,0 @@
|
||||
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
|
92
debian/control
vendored
92
debian/control
vendored
@ -1,92 +0,0 @@
|
||||
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.
|
16
debian/copyright
vendored
16
debian/copyright
vendored
@ -1,16 +0,0 @@
|
||||
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/>.
|
8
debian/debcargo.toml
vendored
8
debian/debcargo.toml
vendored
@ -1,8 +0,0 @@
|
||||
overlay = "."
|
||||
crate_src_path = ".."
|
||||
maintainer = "Proxmox Support Team <support@proxmox.com>"
|
||||
|
||||
[source]
|
||||
# TODO: update once public
|
||||
vcs_git = ""
|
||||
vcs_browser = ""
|
1
debian/source/format
vendored
1
debian/source/format
vendored
@ -1 +0,0 @@
|
||||
3.0 (native)
|
@ -1 +0,0 @@
|
||||
edition = "2018"
|
502
src/account.rs
502
src/account.rs
@ -1,502 +0,0 @@
|
||||
//! 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,
|
||||
})
|
||||
}
|
||||
}
|
@ -1,162 +0,0 @@
|
||||
//! 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)?)
|
||||
}
|
||||
}
|
38
src/b64u.rs
38
src/b64u.rs
@ -1,38 +0,0 @@
|
||||
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()))?)
|
||||
// }
|
||||
}
|
614
src/client.rs
614
src/client.rs
@ -1,614 +0,0 @@
|
||||
//! 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(())
|
||||
}
|
||||
}
|
107
src/directory.rs
107
src/directory.rs
@ -1,107 +0,0 @@
|
||||
//! 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()
|
||||
}
|
||||
}
|
66
src/eab.rs
66
src/eab.rs
@ -1,66 +0,0 @@
|
||||
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()?)
|
||||
}
|
||||
}
|
154
src/error.rs
154
src/error.rs
@ -1,154 +0,0 @@
|
||||
//! 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)
|
||||
}
|
||||
}
|
43
src/json.rs
43
src/json.rs
@ -1,43 +0,0 @@
|
||||
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(())
|
||||
}
|
168
src/jws.rs
168
src/jws.rs
@ -1,168 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
129
src/key.rs
129
src/key.rs
@ -1,129 +0,0 @@
|
||||
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(())
|
||||
}
|
61
src/lib.rs
61
src/lib.rs
@ -1,61 +0,0 @@
|
||||
//! 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;
|
179
src/order.rs
179
src/order.rs
@ -1,179 +0,0 @@
|
||||
//! 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()))?,
|
||||
})
|
||||
}
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
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>,
|
||||
}
|
85
src/util.rs
85
src/util.rs
@ -1,85 +0,0 @@
|
||||
//! 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,
|
||||
})
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user