Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
Wolfgang Bumiller 2021-02-04 11:39:38 +01:00
commit aa23068293
21 changed files with 1868 additions and 0 deletions

5
.cargo/config Normal file
View File

@ -0,0 +1,5 @@
[source]
[source.debian-packages]
directory = "/usr/share/cargo/registry"
[source.crates-io]
replace-with = "debian-packages"

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
Cargo.lock

26
Cargo.toml Normal file
View File

@ -0,0 +1,26 @@
[package]
name = "proxmox-acme-rs"
version = "0.1.0"
authors = ["Wolfgang Bumiller <w.bumiller@proxmox.com>"]
edition = "2018"
license = "AGPL-3"
description = "ACME client library"
exclude = [
"build",
"debian",
]
[dependencies]
base64 = "0.12.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
openssl = "0.10.29"
curl = { version = "0.4.33", optional = true }
[features]
default = []
client = ["curl"]
[dev-dependencies]
anyhow = "1.0"

44
Makefile Normal file
View File

@ -0,0 +1,44 @@
.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- -T- \
| ssh -X repoman@repo.proxmox.com upload --product devel --dist buster

5
debian/changelog vendored Normal file
View File

@ -0,0 +1,5 @@
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

62
debian/control vendored Normal file
View File

@ -0,0 +1,62 @@
Source: rust-proxmox-acme-rs
Section: rust
Priority: optional
Build-Depends: debhelper (>= 11),
dh-cargo (>= 18),
cargo:native <!nocheck>,
rustc:native <!nocheck>,
libstd-rust-dev <!nocheck>,
librust-base64-0.12+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.4.1
Vcs-Git:
Vcs-Browser:
Package: librust-proxmox-acme-rs-dev
Architecture: any
Multi-Arch: same
Depends:
${misc:Depends},
librust-base64-0.12+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})
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.1-dev (= ${binary:Version}),
librust-proxmox-acme-rs-0.1+default-dev (= ${binary:Version}),
librust-proxmox-acme-rs-0.1.0-dev (= ${binary:Version}),
librust-proxmox-acme-rs-0.1.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-curl-0.4+default-dev (>= 0.4.33-~~)
Provides:
librust-proxmox-acme-rs+curl-dev (= ${binary:Version}),
librust-proxmox-acme-rs-0+client-dev (= ${binary:Version}),
librust-proxmox-acme-rs-0+curl-dev (= ${binary:Version}),
librust-proxmox-acme-rs-0.1+client-dev (= ${binary:Version}),
librust-proxmox-acme-rs-0.1+curl-dev (= ${binary:Version}),
librust-proxmox-acme-rs-0.1.0+client-dev (= ${binary:Version}),
librust-proxmox-acme-rs-0.1.0+curl-dev (= ${binary:Version})
Description: ACME client library - feature "client" and 1 more
This metapackage enables feature "client" for the Rust proxmox-acme-rs crate,
by pulling in any additional dependencies needed by that feature.
.
Additionally, this package also provides the "curl" feature.

16
debian/copyright vendored Normal file
View File

@ -0,0 +1,16 @@
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 Normal file
View File

@ -0,0 +1,8 @@
overlay = "."
crate_src_path = ".."
maintainer = "Proxmox Support Team <support@proxmox.com>"
[source]
# TODO: update once public
vcs_git = ""
vcs_browser = ""

1
rustfmt.toml Normal file
View File

@ -0,0 +1 @@
edition = "2018"

304
src/account.rs Normal file
View File

@ -0,0 +1,304 @@
use std::convert::TryFrom;
use openssl::pkey::{PKey, Private};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::b64u;
use crate::directory::Directory;
use crate::jws::Jws;
use crate::key::PublicKey;
use crate::order::{NewOrder, OrderData};
use crate::request::Request;
use crate::Error;
#[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 {
pub fn from_parts(location: String, private_key: String, data: AccountData) -> Self {
Self {
location,
private_key,
data,
}
}
pub fn creator() -> AccountCreator {
AccountCreator::default()
}
/// 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.
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.
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,
})
}
/// 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))
}
}
#[derive(Clone, Copy, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub enum AccountStatus {
#[serde(rename = "<invalid>")]
New,
Valid,
Deactivated,
Revoked,
}
impl AccountStatus {
#[inline]
fn new() -> Self {
AccountStatus::New
}
#[inline]
fn is_new(&self) -> bool {
*self == AccountStatus::New
}
}
#[derive(Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AccountData {
#[serde(
skip_serializing_if = "AccountStatus::is_new",
default = "AccountStatus::new"
)]
status: AccountStatus,
#[serde(skip_serializing_if = "Option::is_none")]
orders: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
contact: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
terms_of_service_agreed: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
external_account_binding: Option<Value>,
#[serde(default = "default_true", skip_serializing_if = "is_false")]
only_return_existing: bool,
}
#[inline]
fn default_true() -> bool {
true
}
#[inline]
fn is_false(b: &bool) -> bool {
!*b
}
#[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>>,
}
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
}
/// 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()`] will render the account unusable!
pub fn request(&self, directory: &Directory, nonce: &str) -> Result<Request, Error> {
let key = self.key.as_deref().ok_or_else(|| Error::MissingKey)?;
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: None,
only_return_existing: false,
};
let url = directory.new_account_url();
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()`], 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(format!("PEM key contained illegal non-utf-8 characters"))
})?;
Ok(Account {
location: location_header,
data: serde_json::from_slice(response_body)
.map_err(|err| Error::BadAccountData(err.to_string()))?,
private_key,
})
}
}

57
src/authorization.rs Normal file
View File

@ -0,0 +1,57 @@
use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::order::Identifier;
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Status {
Deactivated,
Expired,
Invalid,
Pending,
Revoked,
Valid,
}
#[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Authorization {
pub identifier: Identifier,
pub status: Status,
#[serde(skip_serializing_if = "Option::is_none")]
pub expires: Option<String>,
pub challenges: Vec<Challenge>,
#[serde(default, skip_serializing_if = "is_false")]
pub wildcard: bool,
}
#[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Challenge {
#[serde(rename = "type")]
pub ty: String,
#[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
}

38
src/b64u.rs Normal file
View File

@ -0,0 +1,38 @@
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()))?)
// }
}

605
src/client.rs Normal file
View File

@ -0,0 +1,605 @@
use std::convert::TryFrom;
use curl::easy;
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 {
pub body: Vec<u8>,
pub status: u16,
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 {
pub location: Option<String>,
nonce: Option<String>,
}
impl Headers {
fn read_header(&mut self, header: &[u8]) {
let (name, value) = match parse_header(header) {
Some(h) => h,
None => return,
};
if name.eq_ignore_ascii_case(crate::REPLAY_NONCE) {
self.nonce = Some(value.to_owned());
} else if name.eq_ignore_ascii_case(crate::LOCATION) {
self.location = Some(value.to_owned());
}
}
}
struct Inner {
easy: easy::Easy,
nonce: Option<String>,
}
impl Inner {
pub fn new() -> Self {
Self {
easy: easy::Easy::new(),
nonce: None,
}
}
pub fn execute(
&mut self,
method: &[u8],
url: &str,
request_body: Option<&[u8]>,
) -> Result<HttpResponse, Error> {
let mut body = Vec::new();
let mut headers = Headers::default();
let mut upload;
match method {
b"POST" => self.easy.post(true)?,
b"GET" => self.easy.get(true)?,
b"HEAD" => self.easy.nobody(true)?,
other => bail!("invalid http method: {:?}", other),
}
self.easy.url(url)?;
{
let mut transfer = self.easy.transfer();
transfer.write_function(|data| {
body.extend(data);
Ok(data.len())
})?;
transfer.header_function(|data| {
headers.read_header(data);
true
})?;
if let Some(body) = request_body {
upload = body;
transfer.read_function(|dest| {
let len = upload.len().min(dest.len());
dest[..len].copy_from_slice(&upload[..len]);
upload = &upload[len..];
Ok(len)
})?;
}
transfer.perform()?;
}
let status = self.easy.response_code()?;
let status =
u16::try_from(status).map_err(|_| format_err!("invalid status code: {}", status))?;
Ok(HttpResponse {
body,
status,
headers,
})
}
/// Low-level API to run an n API request. This automatically updates the current nonce!
fn run_request(&mut self, request: Request) -> Result<HttpResponse, Error> {
self.easy.reset();
let body = if !request.content_type.is_empty() {
let mut headers = easy::List::new();
headers.append(&format!("Content-Type: {}", request.content_type))?;
self.easy
.http_headers(headers)
.map_err(|err| format_err!("curl error: {}", err))?;
Some(request.body.as_bytes())
} else {
None
};
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,
}
}
/// 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>,
) -> Result<&Account, Error> {
let account = Account::creator()
.set_contacts(contact)
.agree_to_tos(tos_agreed);
let account = if let Some(bits) = rsa_bits {
account.generate_rsa_key(bits)?
} else {
account.generate_ec_key()?
};
self.register_account(account)
}
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.into()),
}
};
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.into()),
};
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.into()),
};
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> {
Ok(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> {
Ok(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.into()),
}
}
}
/// 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.into()),
}
}
}
/// Request challenge validation. Afterwards, the challenge should be polled.
pub fn request_challenge_validation(&mut self, url: &str) -> Result<Challenge, Error> {
Ok(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 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 }),
};
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(&directory.data.revoke_cert, nonce, &data)?;
match self.inner.run_request(request) {
Ok(_response) => return Ok(()),
Err(err) if err.is_bad_nonce() => continue,
Err(err) => return Err(err.into()),
}
}
}
}
fn parse_header(data: &[u8]) -> Option<(&str, &str)> {
let colon = data.iter().position(|&b| b == b':')?;
let name = std::str::from_utf8(&data[..colon]).ok()?;
let value = &data[(colon + 1)..];
let value_start = value.iter().position(|&b| !b.is_ascii_whitespace())?;
let value = std::str::from_utf8(&value[value_start..]).ok()?;
Some((name.trim(), value.trim()))
}
/// 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(())
}
}

59
src/directory.rs Normal file
View File

@ -0,0 +1,59 @@
use serde::{Deserialize, Serialize};
pub struct Directory {
pub url: String,
pub data: DirectoryData,
}
/// The ACME Directory object structure.
#[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DirectoryData {
pub new_account: String,
pub new_nonce: String,
pub new_order: String,
pub revoke_cert: String,
pub key_change: String,
pub meta: Meta,
}
/// The directory's "meta" object.
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Meta {
#[serde(skip_serializing_if = "Option::is_none")]
pub terms_of_service: Option<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> {
self.data.meta.terms_of_service.as_deref()
}
/// 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.
/// Currently only contains the ToS URL already exposed via the `terms_of_service_url()`
/// method.
pub fn meta(&self) -> &Meta {
&self.data.meta
}
}

133
src/error.rs Normal file
View File

@ -0,0 +1,133 @@
use std::fmt;
use openssl::error::ErrorStack as SslErrorStack;
pub const BAD_NONCE: &str = "urn:ietf:params:acme:error:badNonce";
pub const USER_ACTION_REQUIRED: &str = "urn:ietf:params:acme:error:userActionRequired";
/// Error types returned by this crate.
#[derive(Debug)]
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.
Ssl(SslErrorStack),
/// An otherwise uncaught serde error happened.
Json(serde_json::Error),
/// 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 curl/network errors end up.
/// This is usually a `curl::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 `curl` end up.
Client(String),
}
impl Error {
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::Ssl(err) => fmt::Display::fmt(err, f),
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),
}
}
}
impl From<SslErrorStack> for Error {
fn from(e: SslErrorStack) -> Self {
Error::Ssl(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)
}
}
#[cfg(feature = "client")]
impl From<curl::Error> for Error {
fn from(e: curl::Error) -> Self {
Error::HttpClient(Box::new(e))
}
}

43
src/json.rs Normal file
View File

@ -0,0 +1,43 @@
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(())
}

163
src/jws.rs Normal file
View File

@ -0,0 +1,163 @@
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: MessageDigest = match &pubkey {
PublicKey::Rsa(_) => Self::prepare_rsa(key, &mut protected)?,
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, prot, payload),
}?
};
let signature = b64u::encode(&signature);
Ok(Jws {
protected: protected_data,
payload,
signature,
})
}
fn prepare_rsa<P>(_key: &PKeyRef<P>, protected: &mut Protected) -> Result<MessageDigest, Error>
where
P: HasPrivate,
{
protected.alg = "RS256";
Ok(MessageDigest::sha256())
}
fn prepare_ec<P>(_key: &PKeyRef<P>, protected: &mut Protected) -> Result<MessageDigest, Error>
where
P: HasPrivate,
{
// Note: if we support >256 bit keys we'll want to also support using ES512 here probably
protected.alg = "ES256";
Ok(MessageDigest::sha256())
}
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,
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(r.len() + s.len());
out.extend(r);
out.extend(s);
Ok(out)
}
}

119
src/key.rs Normal file
View File

@ -0,0 +1,119 @@
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()?;
let _: () = key
.public_key()
.affine_coordinates_gfp(group, &mut x, &mut y, &mut ctx)?;
Ok(EcPublicKey {
crv: "P-256",
x: x.to_vec(),
y: y.to_vec(),
})
}
}

29
src/lib.rs Normal file
View File

@ -0,0 +1,29 @@
mod b64u;
mod json;
mod jws;
mod key;
mod request;
pub mod account;
pub mod authorization;
pub mod directory;
pub mod error;
pub mod order;
pub use account::Account;
pub use authorization::{Authorization, Challenge};
pub use directory::Directory;
pub use error::Error;
pub use order::{NewOrder, Order};
pub use request::{ErrorResponse, Request};
/// 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;

126
src/order.rs Normal file
View File

@ -0,0 +1,126 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::request::Request;
use crate::Error;
#[derive(Clone, 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,
Invalid,
Pending,
Processing,
Ready,
Valid,
}
impl Default for Status {
fn default() -> Self {
Status::New
}
}
impl Status {
/// Serde helper
pub fn is_new(&self) -> bool {
*self == Status::New
}
}
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
#[serde(tag = "type", content = "value", rename_all = "lowercase")]
pub enum Identifier {
Dns(String),
}
#[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.
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 {
pub fn new() -> Self {
Default::default()
}
pub fn domain(mut self, domain: String) -> Self {
self.identifiers.push(Identifier::Dns(domain));
self
}
}
#[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Order {
/// Order location URL.
pub location: String,
/// The order's data object.
pub data: OrderData,
}
/// Represents a new in-flight order creation.
///
/// This is created via [`Account::new_order`].
pub struct NewOrder {
//order: OrderData,
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()))?,
})
}
}

23
src/request.rs Normal file
View File

@ -0,0 +1,23 @@
use serde::Deserialize;
pub const JSON_CONTENT_TYPE: &str = "application/jose+json";
pub const CREATED: u16 = 201;
/// A request which should be performed on the ACME provider.
pub struct Request {
pub url: String,
pub method: &'static str,
pub content_type: &'static str,
pub body: String,
pub expected: u16,
}
#[derive(Clone, Debug, Deserialize)]
pub struct ErrorResponse {
#[serde(rename = "type")]
pub ty: String,
pub detail: Option<String>,
pub subproblems: Option<serde_json::Value>,
}