From aa23068293b07c4aba370c59286a287a4c6d2d45 Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Thu, 4 Feb 2021 11:39:38 +0100 Subject: [PATCH 01/57] import Signed-off-by: Wolfgang Bumiller --- .cargo/config | 5 + .gitignore | 2 + Cargo.toml | 26 ++ Makefile | 44 ++++ debian/changelog | 5 + debian/control | 62 +++++ debian/copyright | 16 ++ debian/debcargo.toml | 8 + rustfmt.toml | 1 + src/account.rs | 304 ++++++++++++++++++++++ src/authorization.rs | 57 ++++ src/b64u.rs | 38 +++ src/client.rs | 605 +++++++++++++++++++++++++++++++++++++++++++ src/directory.rs | 59 +++++ src/error.rs | 133 ++++++++++ src/json.rs | 43 +++ src/jws.rs | 163 ++++++++++++ src/key.rs | 119 +++++++++ src/lib.rs | 29 +++ src/order.rs | 126 +++++++++ src/request.rs | 23 ++ 21 files changed, 1868 insertions(+) create mode 100644 .cargo/config create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 Makefile create mode 100644 debian/changelog create mode 100644 debian/control create mode 100644 debian/copyright create mode 100644 debian/debcargo.toml create mode 100644 rustfmt.toml create mode 100644 src/account.rs create mode 100644 src/authorization.rs create mode 100644 src/b64u.rs create mode 100644 src/client.rs create mode 100644 src/directory.rs create mode 100644 src/error.rs create mode 100644 src/json.rs create mode 100644 src/jws.rs create mode 100644 src/key.rs create mode 100644 src/lib.rs create mode 100644 src/order.rs create mode 100644 src/request.rs diff --git a/.cargo/config b/.cargo/config new file mode 100644 index 00000000..3b5b6e48 --- /dev/null +++ b/.cargo/config @@ -0,0 +1,5 @@ +[source] +[source.debian-packages] +directory = "/usr/share/cargo/registry" +[source.crates-io] +replace-with = "debian-packages" diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..96ef6c0b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..132c08ac --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "proxmox-acme-rs" +version = "0.1.0" +authors = ["Wolfgang Bumiller "] +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" diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..6301c174 --- /dev/null +++ b/Makefile @@ -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 diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 00000000..2ace5e4a --- /dev/null +++ b/debian/changelog @@ -0,0 +1,5 @@ +rust-proxmox-acme-rs (0.1.0-1) pve; urgency=medium + + * initial release + + -- Proxmox Support Team Tue, 09 Mar 2021 13:01:56 +0100 diff --git a/debian/control b/debian/control new file mode 100644 index 00000000..d3aa336f --- /dev/null +++ b/debian/control @@ -0,0 +1,62 @@ +Source: rust-proxmox-acme-rs +Section: rust +Priority: optional +Build-Depends: debhelper (>= 11), + dh-cargo (>= 18), + cargo:native , + rustc:native , + libstd-rust-dev , + 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 +Maintainer: Proxmox Support Team +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. diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 00000000..477c3058 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,16 @@ +Copyright (C) 2020-2021 Proxmox Server Solutions GmbH + +This software is written by Proxmox Server Solutions GmbH + +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 . diff --git a/debian/debcargo.toml b/debian/debcargo.toml new file mode 100644 index 00000000..703440fc --- /dev/null +++ b/debian/debcargo.toml @@ -0,0 +1,8 @@ +overlay = "." +crate_src_path = ".." +maintainer = "Proxmox Support Team " + +[source] +# TODO: update once public +vcs_git = "" +vcs_browser = "" diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 00000000..32a9786f --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +edition = "2018" diff --git a/src/account.rs b/src/account.rs new file mode 100644 index 00000000..57c22461 --- /dev/null +++ b/src/account.rs @@ -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 { + 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 { + 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( + &self, + url: &str, + nonce: &str, + data: &T, + ) -> Result { + 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 { + 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 { + 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 = "")] + 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, + + #[serde(skip_serializing_if = "Vec::is_empty", default)] + contact: Vec, + + #[serde(skip_serializing_if = "Option::is_none")] + terms_of_service_agreed: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + external_account_binding: Option, + + #[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, + terms_of_service_agreed: bool, + key: Option>, +} + +impl AccountCreator { + /// Replace the contact infor with the provided ACME compatible data. + pub fn set_contacts(mut self, contact: Vec) -> 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 { + 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 { + 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) -> 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 { + 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 { + 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, + }) + } +} diff --git a/src/authorization.rs b/src/authorization.rs new file mode 100644 index 00000000..051306f4 --- /dev/null +++ b/src/authorization.rs @@ -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, + + pub challenges: Vec, + + #[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, +} + +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 +} diff --git a/src/b64u.rs b/src/b64u.rs new file mode 100644 index 00000000..a4f8ce0a --- /dev/null +++ b/src/b64u.rs @@ -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, 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(data: &[u8], serializer: S) -> Result + 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, 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()))?) + // } +} diff --git a/src/client.rs b/src/client.rs new file mode 100644 index 00000000..2ab0a4d2 --- /dev/null +++ b/src/client.rs @@ -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, + 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 Deserialize<'a>>(&self) -> Result { + 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 { + 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, + nonce: Option, +} + +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, +} + +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 { + 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 { + 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 { + 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, + account: Option, + 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_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, 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, + tos_agreed: bool, + rsa_bits: Option, + ) -> 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) -> 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(&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) -> Result { + 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 { + 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 { + Ok(self.post_as_get(url)?.json()?) + } + + /// Low level "POST-as-GET" request. + pub fn post_as_get(&mut self, url: &str) -> Result { + 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(&mut self, url: &str, data: &T) -> Result { + 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 { + Ok(self.post(url, &serde_json::json!({}))?.json()?) + } + + /// Shortcut to `account().ok_or_else(...).key_authorization()`. + pub fn key_authorization(&self, token: &str) -> Result { + 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 { + 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 { + 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, 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, + ) -> 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(()) + } +} diff --git a/src/directory.rs b/src/directory.rs new file mode 100644 index 00000000..50294d26 --- /dev/null +++ b/src/directory.rs @@ -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, +} + +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 + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 00000000..eb6015be --- /dev/null +++ b/src/error.rs @@ -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), + + /// 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(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 for Error { + fn from(e: SslErrorStack) -> Self { + Error::Ssl(e) + } +} + +impl From for Error { + fn from(e: serde_json::Error) -> Self { + Error::Json(e) + } +} + +impl From for Error { + fn from(e: crate::request::ErrorResponse) -> Self { + Error::Api(e) + } +} + +#[cfg(feature = "client")] +impl From for Error { + fn from(e: curl::Error) -> Self { + Error::HttpClient(Box::new(e)) + } +} diff --git a/src/json.rs b/src/json.rs new file mode 100644 index 00000000..e192d679 --- /dev/null +++ b/src/json.rs @@ -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(()) +} diff --git a/src/jws.rs b/src/jws.rs new file mode 100644 index 00000000..5c11dcb3 --- /dev/null +++ b/src/jws.rs @@ -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( + key: &PKeyRef

, + location: Option, + url: String, + nonce: String, + payload: &T, + ) -> Result + 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( + key: &PKeyRef

, + location: Option, + url: String, + nonce: String, + payload: String, + ) -> Result { + 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

(_key: &PKeyRef

, protected: &mut Protected) -> Result + where + P: HasPrivate, + { + protected.alg = "RS256"; + Ok(MessageDigest::sha256()) + } + + fn prepare_ec

(_key: &PKeyRef

, protected: &mut Protected) -> Result + 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

( + key: &PKeyRef

, + digest: MessageDigest, + protected: &[u8], + payload: &[u8], + ) -> Result, 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

( + key: &PKeyRef

, + digest: MessageDigest, + protected: &[u8], + payload: &[u8], + ) -> Result, 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) + } +} diff --git a/src/key.rs b/src/key.rs new file mode 100644 index 00000000..487f65ec --- /dev/null +++ b/src/key.rs @@ -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, + #[serde(with = "b64u::bytes")] + n: Vec, +} + +/// An EC public key. +#[derive(Clone, Debug, Serialize)] +#[serde(deny_unknown_fields)] +pub struct EcPublicKey { + crv: &'static str, + #[serde(with = "b64u::bytes")] + x: Vec, + #[serde(with = "b64u::bytes")] + y: Vec, +} + +/// 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 { + 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, + + /// The key data is internally tagged, we can just flatten it. + #[serde(flatten)] + pub key: PublicKey, +} + +impl TryFrom<&PKeyRef

> for Jwk { + type Error = Error; + + fn try_from(key: &PKeyRef

) -> Result { + Ok(Self { + key: key.try_into()?, + usage: None, + }) + } +} + +impl TryFrom<&PKeyRef

> for PublicKey { + type Error = Error; + + fn try_from(key: &PKeyRef

) -> Result { + 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 TryFrom<&openssl::rsa::Rsa

> for RsaPublicKey { + type Error = Error; + + fn try_from(key: &openssl::rsa::Rsa

) -> Result { + Ok(RsaPublicKey { + e: key.e().to_vec(), + n: key.n().to_vec(), + }) + } +} + +impl TryFrom<&openssl::ec::EcKey

> for EcPublicKey { + type Error = Error; + + fn try_from(key: &openssl::ec::EcKey

) -> Result { + 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(), + }) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 00000000..77dc383d --- /dev/null +++ b/src/lib.rs @@ -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; diff --git a/src/order.rs b/src/order.rs new file mode 100644 index 00000000..f5950943 --- /dev/null +++ b/src/order.rs @@ -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, + + /// List of identifiers to order for the certificate. + pub identifiers: Vec, + + /// 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, + + /// 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, + + /// Possible errors in this order. + #[serde(skip_serializing_if = "Option::is_none")] + pub error: Option, + + /// List of URL's to authorizations the client needs to complete. + #[serde(skip_serializing_if = "Vec::is_empty")] + pub authorizations: Vec, + + /// 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, + + /// URL at which the issued certificate can be fetched once it is available. + #[serde(skip_serializing_if = "Option::is_none")] + pub certificate: Option, +} + +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, +} + +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 { + Ok(Order { + location: location_header, + data: serde_json::from_slice(response_body) + .map_err(|err| Error::BadOrderData(err.to_string()))?, + }) + } +} diff --git a/src/request.rs b/src/request.rs new file mode 100644 index 00000000..4cd929ab --- /dev/null +++ b/src/request.rs @@ -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, + pub subproblems: Option, +} From cbfeb58ce46f4964d90344882e50d15de3d579c3 Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Tue, 9 Mar 2021 13:22:52 +0100 Subject: [PATCH 02/57] make AccountData fields public Signed-off-by: Wolfgang Bumiller --- src/account.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/account.rs b/src/account.rs index 57c22461..747f22e3 100644 --- a/src/account.rs +++ b/src/account.rs @@ -161,22 +161,22 @@ pub struct AccountData { skip_serializing_if = "AccountStatus::is_new", default = "AccountStatus::new" )] - status: AccountStatus, + pub status: AccountStatus, #[serde(skip_serializing_if = "Option::is_none")] - orders: Option, + pub orders: Option, #[serde(skip_serializing_if = "Vec::is_empty", default)] - contact: Vec, + pub contact: Vec, #[serde(skip_serializing_if = "Option::is_none")] - terms_of_service_agreed: Option, + pub terms_of_service_agreed: Option, #[serde(skip_serializing_if = "Option::is_none")] - external_account_binding: Option, + pub external_account_binding: Option, #[serde(default = "default_true", skip_serializing_if = "is_false")] - only_return_existing: bool, + pub only_return_existing: bool, } #[inline] From c9f137a093eecb6fe6ec1ba3a0de195a516193ea Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Tue, 9 Mar 2021 13:23:27 +0100 Subject: [PATCH 03/57] bump version to 0.1.1-1 Signed-off-by: Wolfgang Bumiller --- Cargo.toml | 2 +- debian/changelog | 6 ++++++ debian/control | 8 ++++---- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 132c08ac..d092e16f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proxmox-acme-rs" -version = "0.1.0" +version = "0.1.1" authors = ["Wolfgang Bumiller "] edition = "2018" license = "AGPL-3" diff --git a/debian/changelog b/debian/changelog index 2ace5e4a..9966b611 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +rust-proxmox-acme-rs (0.1.1-1) pve; urgency=medium + + * make AccountData fields public + + -- Proxmox Support Team Tue, 09 Mar 2021 13:22:55 +0100 + rust-proxmox-acme-rs (0.1.0-1) pve; urgency=medium * initial release diff --git a/debian/control b/debian/control index d3aa336f..ca065da1 100644 --- a/debian/control +++ b/debian/control @@ -34,8 +34,8 @@ Provides: 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}) + librust-proxmox-acme-rs-0.1.1-dev (= ${binary:Version}), + librust-proxmox-acme-rs-0.1.1+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. @@ -53,8 +53,8 @@ Provides: 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}) + librust-proxmox-acme-rs-0.1.1+client-dev (= ${binary:Version}), + librust-proxmox-acme-rs-0.1.1+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. From ca0f57290f2f3f0aa541b3e0b3fae9c787d07903 Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Wed, 10 Mar 2021 12:00:59 +0100 Subject: [PATCH 04/57] explicitly pass Content-Length header pebble refuses to cooperate without it Signed-off-by: Wolfgang Bumiller --- src/client.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/client.rs b/src/client.rs index 2ab0a4d2..56500bd4 100644 --- a/src/client.rs +++ b/src/client.rs @@ -154,6 +154,7 @@ impl Inner { let body = if !request.content_type.is_empty() { let mut headers = easy::List::new(); headers.append(&format!("Content-Type: {}", request.content_type))?; + headers.append(&format!("Content-Length: {}", request.body.len()))?; self.easy .http_headers(headers) .map_err(|err| format_err!("curl error: {}", err))?; From 15fae62c15a9514ab32c960882685619fc8c5a86 Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Fri, 12 Mar 2021 15:43:40 +0100 Subject: [PATCH 05/57] bump version to 0.1.2-1 Signed-off-by: Wolfgang Bumiller --- Cargo.toml | 2 +- debian/changelog | 6 ++++++ debian/control | 8 ++++---- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d092e16f..7b50c1a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proxmox-acme-rs" -version = "0.1.1" +version = "0.1.2" authors = ["Wolfgang Bumiller "] edition = "2018" license = "AGPL-3" diff --git a/debian/changelog b/debian/changelog index 9966b611..2f670285 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +rust-proxmox-acme-rs (0.1.2-1) pve; urgency=medium + + * include Content-length header in requests + + -- Proxmox Support Team Fri, 12 Mar 2021 15:43:01 +0100 + rust-proxmox-acme-rs (0.1.1-1) pve; urgency=medium * make AccountData fields public diff --git a/debian/control b/debian/control index ca065da1..28d93d88 100644 --- a/debian/control +++ b/debian/control @@ -34,8 +34,8 @@ Provides: 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.1-dev (= ${binary:Version}), - librust-proxmox-acme-rs-0.1.1+default-dev (= ${binary:Version}) + librust-proxmox-acme-rs-0.1.2-dev (= ${binary:Version}), + librust-proxmox-acme-rs-0.1.2+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. @@ -53,8 +53,8 @@ Provides: 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.1+client-dev (= ${binary:Version}), - librust-proxmox-acme-rs-0.1.1+curl-dev (= ${binary:Version}) + librust-proxmox-acme-rs-0.1.2+client-dev (= ${binary:Version}), + librust-proxmox-acme-rs-0.1.2+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. From 549b52cf688699ddb7635c3fac711c200f21d2c9 Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Wed, 17 Mar 2021 12:24:21 +0100 Subject: [PATCH 06/57] don't serialize 'null' for 'expires' when ordering Signed-off-by: Wolfgang Bumiller --- src/order.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/order.rs b/src/order.rs index f5950943..fd6fb865 100644 --- a/src/order.rs +++ b/src/order.rs @@ -45,6 +45,7 @@ pub struct OrderData { pub status: Status, /// This order's expiration date as RFC3339 formatted time string. + #[serde(skip_serializing_if = "Option::is_none")] pub expires: Option, /// List of identifiers to order for the certificate. From 937a99a2e1c81bd759f23e9dc43d2da86cd41cf5 Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Wed, 17 Mar 2021 13:11:56 +0100 Subject: [PATCH 07/57] fix ec signature padding Signed-off-by: Wolfgang Bumiller --- src/jws.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/jws.rs b/src/jws.rs index 5c11dcb3..1044ee5c 100644 --- a/src/jws.rs +++ b/src/jws.rs @@ -79,8 +79,8 @@ impl Jws { }, }; - let digest: MessageDigest = match &pubkey { - PublicKey::Rsa(_) => Self::prepare_rsa(key, &mut protected)?, + 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)?, }; @@ -91,7 +91,7 @@ impl Jws { 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), + PublicKey::Ec(_) => Self::sign_ec(key, digest, ec_order_bytes, prot, payload), }? }; @@ -112,13 +112,18 @@ impl Jws { Ok(MessageDigest::sha256()) } - fn prepare_ec

(_key: &PKeyRef

, protected: &mut Protected) -> Result + /// Returns the digest and the size of the two signature components 'r' and 's'. + fn prepare_ec

( + _key: &PKeyRef

, + protected: &mut Protected, + ) -> Result<(MessageDigest, usize), 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()) + // 'r' and 's' are each 256 bit numbers: + Ok((MessageDigest::sha256(), 32)) } fn sign_rsa

( @@ -141,6 +146,7 @@ impl Jws { fn sign_ec

( key: &PKeyRef

, digest: MessageDigest, + ec_order_bytes: usize, protected: &[u8], payload: &[u8], ) -> Result, Error> @@ -156,7 +162,9 @@ impl Jws { let r = sig.r().to_vec(); let s = sig.s().to_vec(); let mut out = Vec::with_capacity(r.len() + s.len()); + 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) } From d369de863669b0da3603798f787835287052be31 Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Wed, 17 Mar 2021 13:35:49 +0100 Subject: [PATCH 08/57] bump version to 0.1.3-1 Signed-off-by: Wolfgang Bumiller --- Cargo.toml | 2 +- debian/changelog | 6 ++++++ debian/control | 8 ++++---- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7b50c1a4..6856646f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proxmox-acme-rs" -version = "0.1.2" +version = "0.1.3" authors = ["Wolfgang Bumiller "] edition = "2018" license = "AGPL-3" diff --git a/debian/changelog b/debian/changelog index 2f670285..3adbcadf 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +rust-proxmox-acme-rs (0.1.3-1) pve; urgency=medium + + * fix padding in ecdsa signatures + + -- Proxmox Support Team 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 diff --git a/debian/control b/debian/control index 28d93d88..be497ffe 100644 --- a/debian/control +++ b/debian/control @@ -34,8 +34,8 @@ Provides: 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.2-dev (= ${binary:Version}), - librust-proxmox-acme-rs-0.1.2+default-dev (= ${binary:Version}) + librust-proxmox-acme-rs-0.1.3-dev (= ${binary:Version}), + librust-proxmox-acme-rs-0.1.3+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. @@ -53,8 +53,8 @@ Provides: 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.2+client-dev (= ${binary:Version}), - librust-proxmox-acme-rs-0.1.2+curl-dev (= ${binary:Version}) + librust-proxmox-acme-rs-0.1.3+client-dev (= ${binary:Version}), + librust-proxmox-acme-rs-0.1.3+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. From 5aee14ac00e08bbd5722f3925f190f6b59a6c341 Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Wed, 17 Mar 2021 15:20:50 +0100 Subject: [PATCH 09/57] collect extra account fields in AccountData Signed-off-by: Wolfgang Bumiller --- src/account.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/account.rs b/src/account.rs index 747f22e3..b6a94716 100644 --- a/src/account.rs +++ b/src/account.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::convert::TryFrom; use openssl::pkey::{PKey, Private}; @@ -177,6 +178,9 @@ pub struct AccountData { #[serde(default = "default_true", skip_serializing_if = "is_false")] pub only_return_existing: bool, + + #[serde(flatten, default, skip_serializing_if = "HashMap::is_empty")] + pub extra: HashMap, } #[inline] @@ -263,6 +267,7 @@ impl AccountCreator { }, external_account_binding: None, only_return_existing: false, + extra: HashMap::new(), }; let url = directory.new_account_url(); From 02ecbb499c5782fb22723b665730a72fa60e97a5 Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Wed, 17 Mar 2021 15:28:43 +0100 Subject: [PATCH 10/57] bump version to 0.1.4-1 Signed-off-by: Wolfgang Bumiller --- Cargo.toml | 2 +- debian/changelog | 7 +++++++ debian/control | 8 ++++---- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6856646f..daa1085b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proxmox-acme-rs" -version = "0.1.3" +version = "0.1.4" authors = ["Wolfgang Bumiller "] edition = "2018" license = "AGPL-3" diff --git a/debian/changelog b/debian/changelog index 3adbcadf..4e7a93eb 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,10 @@ +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 Wed, 17 Mar 2021 15:28:09 +0100 + rust-proxmox-acme-rs (0.1.3-1) pve; urgency=medium * fix padding in ecdsa signatures diff --git a/debian/control b/debian/control index be497ffe..97068130 100644 --- a/debian/control +++ b/debian/control @@ -34,8 +34,8 @@ Provides: 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.3-dev (= ${binary:Version}), - librust-proxmox-acme-rs-0.1.3+default-dev (= ${binary:Version}) + librust-proxmox-acme-rs-0.1.4-dev (= ${binary:Version}), + librust-proxmox-acme-rs-0.1.4+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. @@ -53,8 +53,8 @@ Provides: 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.3+client-dev (= ${binary:Version}), - librust-proxmox-acme-rs-0.1.3+curl-dev (= ${binary:Version}) + librust-proxmox-acme-rs-0.1.4+client-dev (= ${binary:Version}), + librust-proxmox-acme-rs-0.1.4+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. From afc59f6d15e17a0b9de4fd7349662b9c3b1ca186 Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Thu, 18 Mar 2021 11:36:42 +0100 Subject: [PATCH 11/57] add more of the ACME workflow to 'Account' Signed-off-by: Wolfgang Bumiller --- src/account.rs | 89 +++++++++++++++++++++++++++++++++++++++++++- src/authorization.rs | 36 ++++++++++++++++++ src/order.rs | 12 ++++++ 3 files changed, 136 insertions(+), 1 deletion(-) diff --git a/src/account.rs b/src/account.rs index b6a94716..83e01ad9 100644 --- a/src/account.rs +++ b/src/account.rs @@ -5,11 +5,12 @@ 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::jws::Jws; use crate::key::PublicKey; -use crate::order::{NewOrder, OrderData}; +use crate::order::{NewOrder, Order, OrderData}; use crate::request::Request; use crate::Error; @@ -117,6 +118,31 @@ impl Account { }) } + /// Prepare a JSON POST request. + fn post_request_raw_payload( + &self, + url: &str, + nonce: &str, + payload: String, + ) -> Result { + 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 { let key = PKey::private_key_from_pem(self.private_key.as_bytes())?; @@ -131,6 +157,67 @@ impl Account { 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( + &self, + nonce: &str, + data: &T, + ) -> Result { + self.post_request(&self.location, nonce, data) + } + + /// Prepare a request to deactivate this account. + /// + /// This is a rather low level interface. You should know what you're doing. + pub fn deactivate_account_request(&self, nonce: &str) -> Result { + 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, 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. You can query the number of + /// challenges from via [`Authorization::challenge_len`] or by manually inspecting its + /// `.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, Error> { + match authorization.challenges.get(challenge_index) { + None => Ok(None), + Some(challenge) => self + .post_request_raw_payload(&challenge.url, nonce, "{}".to_string()) + .map(Some), + } + } } #[derive(Clone, Copy, Eq, PartialEq, Deserialize, Serialize)] diff --git a/src/authorization.rs b/src/authorization.rs index 051306f4..f42a4cca 100644 --- a/src/authorization.rs +++ b/src/authorization.rs @@ -4,6 +4,8 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use crate::order::Identifier; +use crate::request::Request; +use crate::Error; #[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "lowercase")] @@ -32,12 +34,25 @@ pub struct Authorization { pub wildcard: bool, } +#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum ChallengeStatus { + Pending, + Processing, + Valid, + Invalid, +} + #[derive(Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Challenge { #[serde(rename = "type")] pub ty: String, + pub status: ChallengeStatus, + + pub url: String, + #[serde(flatten)] pub data: HashMap, } @@ -55,3 +70,24 @@ impl Challenge { fn is_false(b: &bool) -> bool { !*b } + +/// Represents an in-flight query for an authorization. +/// +/// This is created via [`Account::get_authorization`]. +pub struct GetAuthorization { + //order: OrderData, + pub request: Option, +} + +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 { + Ok(serde_json::from_slice(response_body)?) + } +} diff --git a/src/order.rs b/src/order.rs index fd6fb865..3576b96e 100644 --- a/src/order.rs +++ b/src/order.rs @@ -100,6 +100,18 @@ pub struct Order { 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`]. From 5a70f0c3922511a491666015114c0ca8daa9e9d3 Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Mon, 12 Apr 2021 13:32:34 +0200 Subject: [PATCH 12/57] add is_pending to all Status enums Signed-off-by: Wolfgang Bumiller --- src/authorization.rs | 16 ++++++++++++++++ src/order.rs | 6 ++++++ 2 files changed, 22 insertions(+) diff --git a/src/authorization.rs b/src/authorization.rs index f42a4cca..b7a61884 100644 --- a/src/authorization.rs +++ b/src/authorization.rs @@ -18,6 +18,14 @@ pub enum Status { Valid, } +impl Status { + /// Convenience method to check if the status is 'pending'. + #[inline] + pub fn is_pending(self) -> bool { + self == Status::Pending + } +} + #[derive(Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Authorization { @@ -43,6 +51,14 @@ pub enum ChallengeStatus { Invalid, } +impl ChallengeStatus { + /// Convenience method to check if the status is 'pending'. + #[inline] + pub fn is_pending(self) -> bool { + self == ChallengeStatus::Pending + } +} + #[derive(Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Challenge { diff --git a/src/order.rs b/src/order.rs index 3576b96e..872799da 100644 --- a/src/order.rs +++ b/src/order.rs @@ -29,6 +29,12 @@ impl Status { pub 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 + } } #[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] From 37b4c4f6547a28afa1a3d1cc7121d1de17e04088 Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Mon, 12 Apr 2021 13:38:05 +0200 Subject: [PATCH 13/57] add is_valid to all Status enums Signed-off-by: Wolfgang Bumiller --- src/authorization.rs | 12 ++++++++++++ src/order.rs | 6 ++++++ 2 files changed, 18 insertions(+) diff --git a/src/authorization.rs b/src/authorization.rs index b7a61884..519372ce 100644 --- a/src/authorization.rs +++ b/src/authorization.rs @@ -24,6 +24,12 @@ impl Status { 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 + } } #[derive(Deserialize, Serialize)] @@ -57,6 +63,12 @@ impl ChallengeStatus { 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 + } } #[derive(Deserialize, Serialize)] diff --git a/src/order.rs b/src/order.rs index 872799da..9236c20f 100644 --- a/src/order.rs +++ b/src/order.rs @@ -35,6 +35,12 @@ impl Status { 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 + } } #[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] From a947050ec1906b959db014027a65fa388b71360a Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Mon, 12 Apr 2021 14:04:10 +0200 Subject: [PATCH 14/57] add util::Csr for CSR generation This is essentially taken from pmg-rs and should be used from there. Signed-off-by: Wolfgang Bumiller --- src/error.rs | 17 +++++++-- src/lib.rs | 1 + src/util.rs | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 src/util.rs diff --git a/src/error.rs b/src/error.rs index eb6015be..89fa7ab6 100644 --- a/src/error.rs +++ b/src/error.rs @@ -44,7 +44,11 @@ pub enum Error { BadOrderData(String), /// An openssl error occurred during a crypto operation. - Ssl(SslErrorStack), + 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), @@ -61,6 +65,9 @@ pub enum Error { /// If built with the `client` feature, this is where client specific errors which are not from /// errors forwarded from `curl` end up. Client(String), + + /// A non-openssl error occurred while building data for the CSR. + Csr(String), } impl Error { @@ -98,18 +105,22 @@ impl fmt::Display for Error { Error::BadOrderData(err) => { write!(f, "bad response to new-order query or creation: {}", err) } - Error::Ssl(err) => fmt::Display::fmt(err, f), + 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), } } } impl From for Error { fn from(e: SslErrorStack) -> Self { - Error::Ssl(e) + Error::RawSsl(e) } } diff --git a/src/lib.rs b/src/lib.rs index 77dc383d..27370191 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,7 @@ pub mod authorization; pub mod directory; pub mod error; pub mod order; +pub mod util; pub use account::Account; pub use authorization::{Authorization, Challenge}; diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 00000000..bfef364d --- /dev/null +++ b/src/util.rs @@ -0,0 +1,98 @@ +use std::collections::HashMap; + +use openssl::hash::MessageDigest; +use openssl::nid::Nid; +use openssl::pkey::PKey; +use openssl::rsa::Rsa; +use openssl::x509::{X509Extension, X509Name, X509Req}; + +use crate::Error; + +pub struct Csr { + /// DER encoded certificate request. + pub data: Vec, + + /// PEM formatted PKCS#8 private key. + pub private_key_pem: Vec, +} + +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], + attributes: &HashMap, + ) -> Result { + if identifiers.is_empty() { + return Err(Error::Csr(format!("cannot generate empty CSR"))); + } + + 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(X509Extension::new_nid( + None, + None, + Nid::BASIC_CONSTRAINTS, + "CA:FALSE", + )?)?; + ext.push(X509Extension::new_nid( + None, + None, + Nid::KEY_USAGE, + "digitalSignature,keyEncipherment", + )?)?; + ext.push(X509Extension::new_nid( + None, + None, + Nid::EXT_KEY_USAGE, + "serverAuth,clientAuth", + )?)?; + ext.push(X509Extension::new_nid( + None, + Some(&context), + Nid::SUBJECT_ALT_NAME, + &identifiers + .into_iter() + .try_fold(String::new(), |mut acc, dns| { + if !acc.is_empty() { + acc.push(','); + } + use std::fmt::Write; + write!(acc, "DNS:{}", dns.as_ref())?; + Ok::<_, std::fmt::Error>(acc) + }) + .map_err(|err| Error::Csr(err.to_string()))?, + )?)?; + csr.add_extensions(&ext)?; + + csr.sign(&private_key, MessageDigest::sha256())?; + + Ok(Self { + data: csr.build().to_der()?, + private_key_pem, + }) + } +} From b624fa1f3c1716d643c5f428b5559a3ad0fba53b Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Mon, 12 Apr 2021 13:08:31 +0200 Subject: [PATCH 15/57] bump version to 0.2.0-1 Signed-off-by: Wolfgang Bumiller --- Cargo.toml | 2 +- debian/changelog | 10 ++++++++++ debian/control | 16 ++++++++-------- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index daa1085b..353eac6b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proxmox-acme-rs" -version = "0.1.4" +version = "0.2.0" authors = ["Wolfgang Bumiller "] edition = "2018" license = "AGPL-3" diff --git a/debian/changelog b/debian/changelog index 4e7a93eb..0682b3ae 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,13 @@ +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 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) diff --git a/debian/control b/debian/control index 97068130..df8ff275 100644 --- a/debian/control +++ b/debian/control @@ -32,10 +32,10 @@ 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.4-dev (= ${binary:Version}), - librust-proxmox-acme-rs-0.1.4+default-dev (= ${binary:Version}) + librust-proxmox-acme-rs-0.2-dev (= ${binary:Version}), + librust-proxmox-acme-rs-0.2+default-dev (= ${binary:Version}), + librust-proxmox-acme-rs-0.2.0-dev (= ${binary:Version}), + librust-proxmox-acme-rs-0.2.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. @@ -51,10 +51,10 @@ 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.4+client-dev (= ${binary:Version}), - librust-proxmox-acme-rs-0.1.4+curl-dev (= ${binary:Version}) + librust-proxmox-acme-rs-0.2+client-dev (= ${binary:Version}), + librust-proxmox-acme-rs-0.2+curl-dev (= ${binary:Version}), + librust-proxmox-acme-rs-0.2.0+client-dev (= ${binary:Version}), + librust-proxmox-acme-rs-0.2.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. From 558f51a167f5a0b1db7285ee219e563ae45c025d Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Wed, 14 Apr 2021 14:56:40 +0200 Subject: [PATCH 16/57] make revocation workflow accessible without client Signed-off-by: Wolfgang Bumiller --- src/account.rs | 43 +++++++++++++++++++++++++++++++++++++++++++ src/client.rs | 13 ++----------- 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/src/account.rs b/src/account.rs index 83e01ad9..326bdd2d 100644 --- a/src/account.rs +++ b/src/account.rs @@ -218,6 +218,49 @@ impl Account { .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, + ) -> Result { + 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<'_> { + pub fn request(&self, directory: &Directory, nonce: &str) -> Result { + self.account.post_request(&directory.data.revoke_cert, nonce, &self.data) + } } #[derive(Clone, Copy, Eq, PartialEq, Deserialize, Serialize)] diff --git a/src/client.rs b/src/client.rs index 56500bd4..58baffad 100644 --- a/src/client.rs +++ b/src/client.rs @@ -548,16 +548,7 @@ impl Client { // 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 revocation = account.revoke_certificate(certificate, reason)?; let mut retry = retry(); loop { @@ -566,7 +557,7 @@ impl Client { 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)?; + let request = revocation.request(&directory, nonce)?; match self.inner.run_request(request) { Ok(_response) => return Ok(()), Err(err) if err.is_bad_nonce() => continue, From a6ff69404b9f8e80d78d2a29eda977a3d8f90bfd Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Wed, 14 Apr 2021 14:57:08 +0200 Subject: [PATCH 17/57] bump version to 0.2.1-1 Signed-off-by: Wolfgang Bumiller --- Cargo.toml | 2 +- debian/changelog | 6 ++++++ debian/control | 8 ++++---- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 353eac6b..f71dc2a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proxmox-acme-rs" -version = "0.2.0" +version = "0.2.1" authors = ["Wolfgang Bumiller "] edition = "2018" license = "AGPL-3" diff --git a/debian/changelog b/debian/changelog index 0682b3ae..f22a4453 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +rust-proxmox-acme-rs (0.2.1-1) pve; urgency=medium + + * make revocation workflow accessible without client + + -- Proxmox Support Team 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` diff --git a/debian/control b/debian/control index df8ff275..4030fed8 100644 --- a/debian/control +++ b/debian/control @@ -34,8 +34,8 @@ Provides: librust-proxmox-acme-rs-0+default-dev (= ${binary:Version}), librust-proxmox-acme-rs-0.2-dev (= ${binary:Version}), librust-proxmox-acme-rs-0.2+default-dev (= ${binary:Version}), - librust-proxmox-acme-rs-0.2.0-dev (= ${binary:Version}), - librust-proxmox-acme-rs-0.2.0+default-dev (= ${binary:Version}) + librust-proxmox-acme-rs-0.2.1-dev (= ${binary:Version}), + librust-proxmox-acme-rs-0.2.1+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. @@ -53,8 +53,8 @@ Provides: librust-proxmox-acme-rs-0+curl-dev (= ${binary:Version}), librust-proxmox-acme-rs-0.2+client-dev (= ${binary:Version}), librust-proxmox-acme-rs-0.2+curl-dev (= ${binary:Version}), - librust-proxmox-acme-rs-0.2.0+client-dev (= ${binary:Version}), - librust-proxmox-acme-rs-0.2.0+curl-dev (= ${binary:Version}) + librust-proxmox-acme-rs-0.2.1+client-dev (= ${binary:Version}), + librust-proxmox-acme-rs-0.2.1+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. From 7c67886e1fb95a43dea00e3178fe9ea424ce9201 Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Tue, 4 May 2021 09:11:18 +0200 Subject: [PATCH 18/57] add top level doc Signed-off-by: Wolfgang Bumiller --- src/lib.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 27370191..ab9258ba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,16 @@ +//! 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. + mod b64u; mod json; mod jws; From 47af324d94e749ba23414a7cbce8dd27c3921e8a Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Tue, 4 May 2021 11:04:17 +0200 Subject: [PATCH 19/57] Some documentation Signed-off-by: Wolfgang Bumiller --- src/account.rs | 53 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/src/account.rs b/src/account.rs index 326bdd2d..e599b94a 100644 --- a/src/account.rs +++ b/src/account.rs @@ -1,3 +1,8 @@ +//! ACME Account management and creation. The [`Account`] type also contains most of the ACME API +//! entry point helpers. + +#![deny(missing_docs)] + use std::collections::HashMap; use std::convert::TryFrom; @@ -14,6 +19,12 @@ 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 { @@ -28,6 +39,7 @@ pub struct Account { } impl Account { + /// Rebuild an account from its components. pub fn from_parts(location: String, private_key: String, data: AccountData) -> Self { Self { location, @@ -36,10 +48,15 @@ impl Account { } } + /// 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, @@ -73,7 +90,7 @@ impl Account { Ok(NewOrder::new(request)) } - /// Prepare a "POST-as-GET" request to fetch data. + /// Prepare a "POST-as-GET" request to fetch data. Low level helper. pub fn get_request(&self, url: &str, nonce: &str) -> Result { let key = PKey::private_key_from_pem(self.private_key.as_bytes())?; let body = serde_json::to_string(&Jws::new_full( @@ -93,7 +110,7 @@ impl Account { }) } - /// Prepare a JSON POST request. + /// Prepare a JSON POST request. Low level helper. pub fn post_request( &self, url: &str, @@ -258,18 +275,28 @@ pub struct CertificateRevocation<'a> { } impl CertificateRevocation<'_> { + /// Create the revocation request using the specified nonce for the given directory. pub fn request(&self, directory: &Directory, nonce: &str) -> Result { 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 = "")] 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, } @@ -285,30 +312,44 @@ impl AccountStatus { } } +/// 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, + /// The acccount's contact info. + /// + /// This usually contains a `"mailto:"` entry but may also contain some other + /// data if the server accepts it. #[serde(skip_serializing_if = "Vec::is_empty", default)] pub contact: Vec, + /// 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, + /// External account information. This is currently not directly supported in any way and only + /// stored to completeness. #[serde(skip_serializing_if = "Option::is_none")] pub external_account_binding: Option, + /// 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, } @@ -323,6 +364,14 @@ 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`] 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`] 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 { From 7bd0bfe1b2c9e844a705f91a5e9d141c8062f8e6 Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Tue, 4 May 2021 11:20:11 +0200 Subject: [PATCH 20/57] more documentation Signed-off-by: Wolfgang Bumiller --- src/account.rs | 21 ++++++++++---------- src/authorization.rs | 46 +++++++++++++++++++++++++++++++++++++++++++- src/order.rs | 6 +++++- 3 files changed, 61 insertions(+), 12 deletions(-) diff --git a/src/account.rs b/src/account.rs index e599b94a..80c844d7 100644 --- a/src/account.rs +++ b/src/account.rs @@ -216,9 +216,8 @@ impl Account { /// Prepare a request to validate a Challenge from an Authorization. /// - /// Returns `Ok(None)` if `challenge_index` is out of out of range. You can query the number of - /// challenges from via [`Authorization::challenge_len`] or by manually inspecting its - /// `.challenges` vector. + /// 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. @@ -367,11 +366,12 @@ fn is_false(b: &bool) -> bool { /// 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`] method, giving it a nonce and a directory. -/// This can be repeated, if necessary, like when the nonce fails. +/// 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`] method to -/// finish the creation of an [`Account`] which can then be persisted. +/// 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 { @@ -431,7 +431,7 @@ impl AccountCreator { /// 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! + /// [`response`](AccountCreator::response()) will render the account unusable! pub fn request(&self, directory: &Directory, nonce: &str) -> Result { let key = self.key.as_deref().ok_or_else(|| Error::MissingKey)?; @@ -467,8 +467,9 @@ impl AccountCreator { }) } - /// 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! + /// 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 { let private_key = self .key diff --git a/src/authorization.rs b/src/authorization.rs index 519372ce..98633dd1 100644 --- a/src/authorization.rs +++ b/src/authorization.rs @@ -1,3 +1,7 @@ +//! Authorization and Challenge data. + +#![deny(missing_docs)] + use std::collections::HashMap; use serde::{Deserialize, Serialize}; @@ -7,14 +11,26 @@ use crate::order::Identifier; use crate::request::Request; use crate::Error; +/// Authorization states. #[derive(Clone, 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, } @@ -32,28 +48,43 @@ impl Status { } } +/// 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, + /// List of challenges which can be used to complete this authorization. pub challenges: Vec, + /// The authorization is for a wildcard domain. #[serde(default, skip_serializing_if = "is_false")] pub wildcard: bool, } +/// The state of a challenge. #[derive(Clone, 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, } @@ -71,16 +102,21 @@ impl ChallengeStatus { } } +/// 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, } @@ -101,9 +137,17 @@ fn is_false(b: &bool) -> bool { /// Represents an in-flight query for an authorization. /// -/// This is created via [`Account::get_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, } diff --git a/src/order.rs b/src/order.rs index 9236c20f..a736c45a 100644 --- a/src/order.rs +++ b/src/order.rs @@ -43,9 +43,13 @@ impl Status { } } +/// 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), } @@ -126,7 +130,7 @@ impl Order { /// Represents a new in-flight order creation. /// -/// This is created via [`Account::new_order`]. +/// This is created via [`Account::new_order`](crate::Account::new_order()). pub struct NewOrder { //order: OrderData, pub request: Option, From 357c0614cfb9647bbcb4aee8d0c4b3c740ae092f Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Tue, 4 May 2021 11:21:30 +0200 Subject: [PATCH 21/57] even more documentation Signed-off-by: Wolfgang Bumiller --- src/util.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/util.rs b/src/util.rs index bfef364d..6b48e717 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,3 +1,7 @@ +//! Certificate utility methods for convenience (such as CSR generation). + +#![deny(missing_docs)] + use std::collections::HashMap; use openssl::hash::MessageDigest; @@ -8,6 +12,7 @@ use openssl::x509::{X509Extension, X509Name, X509Req}; use crate::Error; +/// A certificate signing request. pub struct Csr { /// DER encoded certificate request. pub data: Vec, From 5f0ba968109ada1bfe828a8c7116ef2654176e20 Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Wed, 5 May 2021 11:13:40 +0200 Subject: [PATCH 22/57] finish docs and #[deny(missing_docs)] at the top level Signed-off-by: Wolfgang Bumiller --- src/account.rs | 2 -- src/authorization.rs | 4 +--- src/directory.rs | 23 +++++++++++++++++++++++ src/error.rs | 6 ++++++ src/lib.rs | 21 +++++++++++++++++++-- src/order.rs | 27 ++++++++++++++++++++++++++- src/request.rs | 25 ++++++++++++++++++++++--- src/util.rs | 2 -- 8 files changed, 97 insertions(+), 13 deletions(-) diff --git a/src/account.rs b/src/account.rs index 80c844d7..bee5bc88 100644 --- a/src/account.rs +++ b/src/account.rs @@ -1,8 +1,6 @@ //! ACME Account management and creation. The [`Account`] type also contains most of the ACME API //! entry point helpers. -#![deny(missing_docs)] - use std::collections::HashMap; use std::convert::TryFrom; diff --git a/src/authorization.rs b/src/authorization.rs index 98633dd1..991077e0 100644 --- a/src/authorization.rs +++ b/src/authorization.rs @@ -1,7 +1,5 @@ //! Authorization and Challenge data. -#![deny(missing_docs)] - use std::collections::HashMap; use serde::{Deserialize, Serialize}; @@ -11,7 +9,7 @@ use crate::order::Identifier; use crate::request::Request; use crate::Error; -/// Authorization states. +/// Status of an [`Authorization`]. #[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "lowercase")] pub enum Status { diff --git a/src/directory.rs b/src/directory.rs index 50294d26..474b6150 100644 --- a/src/directory.rs +++ b/src/directory.rs @@ -1,19 +1,41 @@ +//! 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. pub meta: Meta, } @@ -21,6 +43,7 @@ pub struct DirectoryData { #[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, } diff --git a/src/error.rs b/src/error.rs index 89fa7ab6..56168404 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,8 +1,13 @@ +//! 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. @@ -71,6 +76,7 @@ pub enum Error { } impl Error { + /// Create an `Error` from a custom text. pub fn custom(s: T) -> Self { Error::Custom(s.to_string()) } diff --git a/src/lib.rs b/src/lib.rs index ab9258ba..3533b298 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,6 +11,8 @@ //! //! The [`Account`] helper supports RSA and ECC keys and provides most of the API methods. +#![deny(missing_docs)] + mod b64u; mod json; mod jws; @@ -24,12 +26,27 @@ 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; -pub use order::{NewOrder, Order}; -pub use request::{ErrorResponse, Request}; + +#[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"; diff --git a/src/order.rs b/src/order.rs index a736c45a..b92c06ce 100644 --- a/src/order.rs +++ b/src/order.rs @@ -1,9 +1,12 @@ +//! 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, Debug, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "lowercase")] pub enum Status { @@ -11,10 +14,22 @@ pub enum Status { /// 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, } @@ -26,7 +41,7 @@ impl Default for Status { impl Status { /// Serde helper - pub fn is_new(&self) -> bool { + fn is_new(&self) -> bool { *self == Status::New } @@ -53,6 +68,9 @@ pub enum Identifier { 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 { @@ -96,16 +114,20 @@ pub struct OrderData { } 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 { @@ -133,6 +155,9 @@ impl Order { /// 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, } diff --git a/src/request.rs b/src/request.rs index 4cd929ab..78a90913 100644 --- a/src/request.rs +++ b/src/request.rs @@ -1,23 +1,42 @@ use serde::Deserialize; -pub const JSON_CONTENT_TYPE: &str = "application/jose+json"; - -pub const CREATED: u16 = 201; +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, + + /// Additional json data containing information as to why the error occurred. pub subproblems: Option, } diff --git a/src/util.rs b/src/util.rs index 6b48e717..7242b643 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,7 +1,5 @@ //! Certificate utility methods for convenience (such as CSR generation). -#![deny(missing_docs)] - use std::collections::HashMap; use openssl::hash::MessageDigest; From 0ef3c3353860a163943825d263eadfb3994bdfff Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Wed, 5 May 2021 11:21:16 +0200 Subject: [PATCH 23/57] doc fixup Signed-off-by: Wolfgang Bumiller --- src/account.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/account.rs b/src/account.rs index bee5bc88..9d4b75b1 100644 --- a/src/account.rs +++ b/src/account.rs @@ -185,8 +185,6 @@ impl Account { } /// Prepare a request to deactivate this account. - /// - /// This is a rather low level interface. You should know what you're doing. pub fn deactivate_account_request(&self, nonce: &str) -> Result { self.post_request_raw_payload( &self.location, From 2dfc6a74bbfdd9db608286e2819c3f86f8371c72 Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Fri, 7 May 2021 11:39:04 +0200 Subject: [PATCH 24/57] mark Error as must_use Signed-off-by: Wolfgang Bumiller --- src/error.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/error.rs b/src/error.rs index 56168404..54af9c8d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -12,6 +12,7 @@ pub const USER_ACTION_REQUIRED: &str = "urn:ietf:params:acme:error:userActionReq /// 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. From dbabed6842314188b9d03208443ebd4e8eef1393 Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Fri, 7 May 2021 11:47:45 +0200 Subject: [PATCH 25/57] derive Copy for the simple status enums Signed-off-by: Wolfgang Bumiller --- src/authorization.rs | 4 ++-- src/order.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/authorization.rs b/src/authorization.rs index 991077e0..0fe5194e 100644 --- a/src/authorization.rs +++ b/src/authorization.rs @@ -10,7 +10,7 @@ use crate::request::Request; use crate::Error; /// Status of an [`Authorization`]. -#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] +#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)] #[serde(rename_all = "lowercase")] pub enum Status { /// The authorization was deactivated by the client. @@ -70,7 +70,7 @@ pub struct Authorization { } /// The state of a challenge. -#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] +#[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. diff --git a/src/order.rs b/src/order.rs index b92c06ce..53ac6884 100644 --- a/src/order.rs +++ b/src/order.rs @@ -7,7 +7,7 @@ use crate::request::Request; use crate::Error; /// Status of an [`Order`]. -#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] +#[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, From 9538126247d89aee12936ba69783e92757fadd3a Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Fri, 7 May 2021 11:47:56 +0200 Subject: [PATCH 26/57] clippy fixes Signed-off-by: Wolfgang Bumiller --- src/account.rs | 4 ++-- src/jws.rs | 15 ++++++--------- src/util.rs | 4 ++-- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/account.rs b/src/account.rs index 9d4b75b1..456c3d08 100644 --- a/src/account.rs +++ b/src/account.rs @@ -429,7 +429,7 @@ impl AccountCreator { /// 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 { - let key = self.key.as_deref().ok_or_else(|| Error::MissingKey)?; + let key = self.key.as_deref().ok_or(Error::MissingKey)?; let data = AccountData { orders: None, @@ -472,7 +472,7 @@ impl AccountCreator { .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")) + Error::Custom("PEM key contained illegal non-utf-8 characters".to_string()) })?; Ok(Account { diff --git a/src/jws.rs b/src/jws.rs index 1044ee5c..e61ff69b 100644 --- a/src/jws.rs +++ b/src/jws.rs @@ -80,8 +80,8 @@ impl Jws { }; 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)?, + 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()); @@ -104,26 +104,23 @@ impl Jws { }) } - fn prepare_rsa

(_key: &PKeyRef

, protected: &mut Protected) -> Result + fn prepare_rsa

(_key: &PKeyRef

, protected: &mut Protected) -> MessageDigest where P: HasPrivate, { protected.alg = "RS256"; - Ok(MessageDigest::sha256()) + MessageDigest::sha256() } /// Returns the digest and the size of the two signature components 'r' and 's'. - fn prepare_ec

( - _key: &PKeyRef

, - protected: &mut Protected, - ) -> Result<(MessageDigest, usize), Error> + fn prepare_ec

(_key: &PKeyRef

, 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: - Ok((MessageDigest::sha256(), 32)) + (MessageDigest::sha256(), 32) } fn sign_rsa

( diff --git a/src/util.rs b/src/util.rs index 7242b643..29fafd4e 100644 --- a/src/util.rs +++ b/src/util.rs @@ -29,7 +29,7 @@ impl Csr { attributes: &HashMap, ) -> Result { if identifiers.is_empty() { - return Err(Error::Csr(format!("cannot generate empty CSR"))); + return Err(Error::Csr("cannot generate empty CSR".to_string())); } let private_key = Rsa::generate(4096) @@ -78,7 +78,7 @@ impl Csr { Some(&context), Nid::SUBJECT_ALT_NAME, &identifiers - .into_iter() + .iter() .try_fold(String::new(), |mut acc, dns| { if !acc.is_empty() { acc.push(','); From f406e6fb34e2b4ae68fb7ba185c889359e07dbfa Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Fri, 7 May 2021 11:49:26 +0200 Subject: [PATCH 27/57] formatting fixups Signed-off-by: Wolfgang Bumiller --- src/account.rs | 3 ++- src/authorization.rs | 1 - src/order.rs | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/account.rs b/src/account.rs index 456c3d08..8fe22726 100644 --- a/src/account.rs +++ b/src/account.rs @@ -272,7 +272,8 @@ pub struct CertificateRevocation<'a> { impl CertificateRevocation<'_> { /// Create the revocation request using the specified nonce for the given directory. pub fn request(&self, directory: &Directory, nonce: &str) -> Result { - self.account.post_request(&directory.data.revoke_cert, nonce, &self.data) + self.account + .post_request(&directory.data.revoke_cert, nonce, &self.data) } } diff --git a/src/authorization.rs b/src/authorization.rs index 0fe5194e..fee3614d 100644 --- a/src/authorization.rs +++ b/src/authorization.rs @@ -138,7 +138,6 @@ fn is_false(b: &bool) -> bool { /// 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. /// diff --git a/src/order.rs b/src/order.rs index 53ac6884..def5a380 100644 --- a/src/order.rs +++ b/src/order.rs @@ -155,7 +155,6 @@ impl Order { /// 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, From 1d1f80f5ca399c08828839454aab0b4b6599dea6 Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Fri, 7 May 2021 11:50:54 +0200 Subject: [PATCH 28/57] and another *new* clippy fixup Signed-off-by: Wolfgang Bumiller --- src/account.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/account.rs b/src/account.rs index 8fe22726..8144d39b 100644 --- a/src/account.rs +++ b/src/account.rs @@ -41,8 +41,8 @@ impl Account { pub fn from_parts(location: String, private_key: String, data: AccountData) -> Self { Self { location, - private_key, data, + private_key, } } From 5f4b5714504f657db9d0cb02ac31018fcf23c91b Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Fri, 7 May 2021 12:21:15 +0200 Subject: [PATCH 29/57] add Client::directory_url helper allows to drop a `mut` requirement in pmg-rs without having to store the URL twice Signed-off-by: Wolfgang Bumiller --- src/client.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/client.rs b/src/client.rs index 58baffad..d30b626b 100644 --- a/src/client.rs +++ b/src/client.rs @@ -266,6 +266,14 @@ impl Client { } } + /// Get the directory URL without querying the `Directory` structure. + /// + /// The difference to [`get_directory`](Client::get_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); From a145553557c31055bb85525cbd9caafbf2395415 Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Fri, 7 May 2021 12:26:56 +0200 Subject: [PATCH 30/57] finish client documentation Signed-off-by: Wolfgang Bumiller --- src/client.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/client.rs b/src/client.rs index d30b626b..8aeff979 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,3 +1,5 @@ +//! A blocking higher-level ACME client implementation using 'curl'. + use std::convert::TryFrom; use curl::easy; @@ -19,8 +21,13 @@ macro_rules! bail { /// Low level HTTP response structure. pub struct HttpResponse { + /// The raw HTTP response body as a byte vector. pub body: Vec, + + /// The http status code. pub status: u16, + + /// The headers relevant to the ACME protocol. pub headers: Headers, } @@ -60,6 +67,8 @@ impl HttpResponse { /// 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, nonce: Option, } @@ -372,6 +381,10 @@ impl Client { 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, From ee7fe8f93c9ca5823ec65ce87dc301f2d8c6a55c Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Fri, 7 May 2021 13:54:36 +0200 Subject: [PATCH 31/57] bump version to 0.2.2-1 Signed-off-by: Wolfgang Bumiller --- Cargo.toml | 2 +- debian/changelog | 13 +++++++++++++ debian/control | 8 ++++---- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f71dc2a8..ab1b31dd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proxmox-acme-rs" -version = "0.2.1" +version = "0.2.2" authors = ["Wolfgang Bumiller "] edition = "2018" license = "AGPL-3" diff --git a/debian/changelog b/debian/changelog index f22a4453..0d96cb99 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,16 @@ +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 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 diff --git a/debian/control b/debian/control index 4030fed8..3d57a02b 100644 --- a/debian/control +++ b/debian/control @@ -34,8 +34,8 @@ Provides: librust-proxmox-acme-rs-0+default-dev (= ${binary:Version}), librust-proxmox-acme-rs-0.2-dev (= ${binary:Version}), librust-proxmox-acme-rs-0.2+default-dev (= ${binary:Version}), - librust-proxmox-acme-rs-0.2.1-dev (= ${binary:Version}), - librust-proxmox-acme-rs-0.2.1+default-dev (= ${binary:Version}) + librust-proxmox-acme-rs-0.2.2-dev (= ${binary:Version}), + librust-proxmox-acme-rs-0.2.2+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. @@ -53,8 +53,8 @@ Provides: librust-proxmox-acme-rs-0+curl-dev (= ${binary:Version}), librust-proxmox-acme-rs-0.2+client-dev (= ${binary:Version}), librust-proxmox-acme-rs-0.2+curl-dev (= ${binary:Version}), - librust-proxmox-acme-rs-0.2.1+client-dev (= ${binary:Version}), - librust-proxmox-acme-rs-0.2.1+curl-dev (= ${binary:Version}) + librust-proxmox-acme-rs-0.2.2+client-dev (= ${binary:Version}), + librust-proxmox-acme-rs-0.2.2+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. From f28a85da5e9658b84956136d2789a0596c0abbc9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Gr=C3=BCnbichler?= Date: Fri, 11 Jun 2021 14:00:55 +0200 Subject: [PATCH 32/57] build: upload to buster and bullseye MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fabian Grünbichler --- Makefile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 6301c174..383146f0 100644 --- a/Makefile +++ b/Makefile @@ -40,5 +40,7 @@ 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 + | 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 buster; \ + 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 From 4c5d899c3aaea1f84aded5cb0eab365b241b2e05 Mon Sep 17 00:00:00 2001 From: Dominik Csapak Date: Thu, 21 Oct 2021 11:10:43 +0200 Subject: [PATCH 33/57] directory: make meta object optional some custom ACME endpoints do not have a TOS, and thus do not return a meta property at all Signed-off-by: Dominik Csapak Signed-off-by: Wolfgang Bumiller --- src/directory.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/directory.rs b/src/directory.rs index 474b6150..755ea8c0 100644 --- a/src/directory.rs +++ b/src/directory.rs @@ -36,7 +36,8 @@ pub struct DirectoryData { /// Metadata object, for additional information which aren't directly part of the API /// itself, such as the terms of service. - pub meta: Meta, + #[serde(skip_serializing_if = "Option::is_none")] + pub meta: Option, } /// The directory's "meta" object. @@ -57,7 +58,10 @@ impl Directory { /// Get the ToS URL. pub fn terms_of_service_url(&self) -> Option<&str> { - self.data.meta.terms_of_service.as_deref() + match &self.data.meta { + Some(meta) => meta.terms_of_service.as_deref(), + None => None, + } } /// Get the "newNonce" URL. Use `HEAD` requests on this to get a new nonce. @@ -76,7 +80,7 @@ impl Directory { /// 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 + pub fn meta(&self) -> Option<&Meta> { + self.data.meta.as_ref() } } From cb89d97df137781f1cb0fe7fff2bc27bb8043205 Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Thu, 21 Oct 2021 13:11:21 +0200 Subject: [PATCH 34/57] bump version to 0.3.0 Signed-off-by: Wolfgang Bumiller --- Cargo.toml | 2 +- debian/changelog | 6 ++++++ debian/control | 23 ++++++++++++----------- debian/source/format | 1 + 4 files changed, 20 insertions(+), 12 deletions(-) create mode 100644 debian/source/format diff --git a/Cargo.toml b/Cargo.toml index ab1b31dd..3953fd20 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proxmox-acme-rs" -version = "0.2.2" +version = "0.3.0" authors = ["Wolfgang Bumiller "] edition = "2018" license = "AGPL-3" diff --git a/debian/changelog b/debian/changelog index 0d96cb99..56446b8f 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +rust-proxmox-acme-rs (0.3.0) pve; urgency=medium + + * directory: make metadata optional + + -- Proxmox Support Team Thu, 21 Oct 2021 13:10:27 +0200 + rust-proxmox-acme-rs (0.2.2-1) pve; urgency=medium * improve crate documentation diff --git a/debian/control b/debian/control index 3d57a02b..4f463208 100644 --- a/debian/control +++ b/debian/control @@ -1,8 +1,8 @@ Source: rust-proxmox-acme-rs Section: rust Priority: optional -Build-Depends: debhelper (>= 11), - dh-cargo (>= 18), +Build-Depends: debhelper (>= 12), + dh-cargo (>= 24), cargo:native , rustc:native , libstd-rust-dev , @@ -12,9 +12,10 @@ Build-Depends: debhelper (>= 11), librust-serde-1+derive-dev , librust-serde-json-1+default-dev Maintainer: Proxmox Support Team -Standards-Version: 4.4.1 +Standards-Version: 4.5.1 Vcs-Git: Vcs-Browser: +Rules-Requires-Root: no Package: librust-proxmox-acme-rs-dev Architecture: any @@ -32,10 +33,10 @@ 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.2-dev (= ${binary:Version}), - librust-proxmox-acme-rs-0.2+default-dev (= ${binary:Version}), - librust-proxmox-acme-rs-0.2.2-dev (= ${binary:Version}), - librust-proxmox-acme-rs-0.2.2+default-dev (= ${binary:Version}) + librust-proxmox-acme-rs-0.3-dev (= ${binary:Version}), + librust-proxmox-acme-rs-0.3+default-dev (= ${binary:Version}), + librust-proxmox-acme-rs-0.3.0-dev (= ${binary:Version}), + librust-proxmox-acme-rs-0.3.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. @@ -51,10 +52,10 @@ 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.2+client-dev (= ${binary:Version}), - librust-proxmox-acme-rs-0.2+curl-dev (= ${binary:Version}), - librust-proxmox-acme-rs-0.2.2+client-dev (= ${binary:Version}), - librust-proxmox-acme-rs-0.2.2+curl-dev (= ${binary:Version}) + librust-proxmox-acme-rs-0.3+client-dev (= ${binary:Version}), + librust-proxmox-acme-rs-0.3+curl-dev (= ${binary:Version}), + librust-proxmox-acme-rs-0.3.0+client-dev (= ${binary:Version}), + librust-proxmox-acme-rs-0.3.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. diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 00000000..89ae9db8 --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (native) From ba2da9d125e3ea74c88460bd6b83d0f913886d9b Mon Sep 17 00:00:00 2001 From: Stoiko Ivanov Date: Tue, 9 Nov 2021 16:54:16 +0000 Subject: [PATCH 35/57] client: add support for proxies by storing the proxy url as string in the struct and setting it on each invocation of `execute`, since execute calls reset on the curl::easy::Easy object. Signed-off-by: Stoiko Ivanov Signed-off-by: Wolfgang Bumiller --- src/client.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/client.rs b/src/client.rs index 8aeff979..9853fb61 100644 --- a/src/client.rs +++ b/src/client.rs @@ -91,6 +91,7 @@ impl Headers { struct Inner { easy: easy::Easy, nonce: Option, + proxy: Option, } impl Inner { @@ -98,6 +99,7 @@ impl Inner { Self { easy: easy::Easy::new(), nonce: None, + proxy: None, } } @@ -120,6 +122,10 @@ impl Inner { self.easy.url(url)?; + if let Some(p) = &self.proxy { + self.easy.proxy(&p)?; + } + { let mut transfer = self.easy.transfer(); @@ -156,6 +162,10 @@ impl Inner { }) } + pub fn set_proxy(&mut self, proxy: String) { + self.proxy = Some(proxy); + } + /// Low-level API to run an n API request. This automatically updates the current nonce! fn run_request(&mut self, request: Request) -> Result { self.easy.reset(); @@ -586,6 +596,11 @@ impl Client { } } } + + /// Set a proxy + pub fn set_proxy(&mut self, proxy: String) { + self.inner.set_proxy(proxy) + } } fn parse_header(data: &[u8]) -> Option<(&str, &str)> { From 467d5675459eddff012a5e00b137b8f390cd9362 Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Thu, 18 Nov 2021 09:46:01 +0100 Subject: [PATCH 36/57] clippy and formatting fixups Signed-off-by: Wolfgang Bumiller --- src/client.rs | 26 +++++++++++++------------- src/order.rs | 2 +- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/client.rs b/src/client.rs index 9853fb61..4582738c 100644 --- a/src/client.rs +++ b/src/client.rs @@ -123,7 +123,7 @@ impl Inner { self.easy.url(url)?; if let Some(p) = &self.proxy { - self.easy.proxy(&p)?; + self.easy.proxy(p)?; } { @@ -163,7 +163,7 @@ impl Inner { } pub fn set_proxy(&mut self, proxy: String) { - self.proxy = Some(proxy); + self.proxy = Some(proxy); } /// Low-level API to run an n API request. This automatically updates the current nonce! @@ -410,7 +410,7 @@ impl Client { match self.run_request(request) { Ok(response) => break response, Err(err) if err.is_bad_nonce() => continue, - Err(err) => return Err(err.into()), + Err(err) => return Err(err), } }; @@ -439,11 +439,11 @@ impl Client { 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 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()), + Err(err) => return Err(err), }; break response; @@ -477,7 +477,7 @@ impl Client { 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()), + Err(err) => return Err(err), }; return new_order.response(response.location_required()?, response.bytes().as_ref()); @@ -486,12 +486,12 @@ impl Client { /// Assuming the provided URL is an 'Authorization' URL, get and deserialize it. pub fn get_authorization(&mut self, url: &str) -> Result { - Ok(self.post_as_get(url)?.json()?) + 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 { - Ok(self.post_as_get(url)?.json()?) + self.post_as_get(url)?.json() } /// Low level "POST-as-GET" request. @@ -509,7 +509,7 @@ impl Client { 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()), + Err(err) => return Err(err), } } } @@ -529,14 +529,14 @@ impl Client { 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()), + Err(err) => return Err(err), } } } /// Request challenge validation. Afterwards, the challenge should be polled. pub fn request_challenge_validation(&mut self, url: &str) -> Result { - Ok(self.post(url, &serde_json::json!({}))?.json()?) + self.post(url, &serde_json::json!({}))?.json() } /// Shortcut to `account().ok_or_else(...).key_authorization()`. @@ -588,11 +588,11 @@ impl Client { 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)?; + 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.into()), + Err(err) => return Err(err), } } } diff --git a/src/order.rs b/src/order.rs index def5a380..404d4ae7 100644 --- a/src/order.rs +++ b/src/order.rs @@ -141,7 +141,7 @@ pub struct Order { 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)?) + Some(self.data.authorizations.get(index)?) } /// Get the number of authorizations in this object. From 06f5106145a464c95e1d2d468e836552ed031ee3 Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Thu, 18 Nov 2021 09:49:30 +0100 Subject: [PATCH 37/57] bump version to 0.3.1 Signed-off-by: Wolfgang Bumiller --- Cargo.toml | 2 +- debian/changelog | 6 ++++++ debian/control | 8 ++++---- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3953fd20..ec24f848 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proxmox-acme-rs" -version = "0.3.0" +version = "0.3.1" authors = ["Wolfgang Bumiller "] edition = "2018" license = "AGPL-3" diff --git a/debian/changelog b/debian/changelog index 56446b8f..0171e9b6 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +rust-proxmox-acme-rs (0.3.1) pve; urgency=medium + + * add proxy support + + -- Proxmox Support Team Thu, 18 Nov 2021 09:46:34 +0100 + rust-proxmox-acme-rs (0.3.0) pve; urgency=medium * directory: make metadata optional diff --git a/debian/control b/debian/control index 4f463208..73ad9d21 100644 --- a/debian/control +++ b/debian/control @@ -35,8 +35,8 @@ Provides: librust-proxmox-acme-rs-0+default-dev (= ${binary:Version}), librust-proxmox-acme-rs-0.3-dev (= ${binary:Version}), librust-proxmox-acme-rs-0.3+default-dev (= ${binary:Version}), - librust-proxmox-acme-rs-0.3.0-dev (= ${binary:Version}), - librust-proxmox-acme-rs-0.3.0+default-dev (= ${binary:Version}) + librust-proxmox-acme-rs-0.3.1-dev (= ${binary:Version}), + librust-proxmox-acme-rs-0.3.1+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. @@ -54,8 +54,8 @@ Provides: librust-proxmox-acme-rs-0+curl-dev (= ${binary:Version}), librust-proxmox-acme-rs-0.3+client-dev (= ${binary:Version}), librust-proxmox-acme-rs-0.3+curl-dev (= ${binary:Version}), - librust-proxmox-acme-rs-0.3.0+client-dev (= ${binary:Version}), - librust-proxmox-acme-rs-0.3.0+curl-dev (= ${binary:Version}) + librust-proxmox-acme-rs-0.3.1+client-dev (= ${binary:Version}), + librust-proxmox-acme-rs-0.3.1+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. From cbc90fe9fc53144c36c463f3c284f67a1a174629 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Gr=C3=BCnbichler?= Date: Thu, 18 Nov 2021 12:48:40 +0100 Subject: [PATCH 38/57] bump base64 dep to 0.13 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fabian Grünbichler --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index ec24f848..7f2f792c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ exclude = [ ] [dependencies] -base64 = "0.12.0" +base64 = "0.13.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" openssl = "0.10.29" From fb547f59352155bdc7a9738237e4df8fa0cda10d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Gr=C3=BCnbichler?= Date: Thu, 18 Nov 2021 12:49:36 +0100 Subject: [PATCH 39/57] bump version to 0.3.2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fabian Grünbichler --- Cargo.toml | 2 +- debian/changelog | 6 ++++++ debian/control | 12 ++++++------ 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7f2f792c..573a3a4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proxmox-acme-rs" -version = "0.3.1" +version = "0.3.2" authors = ["Wolfgang Bumiller "] edition = "2018" license = "AGPL-3" diff --git a/debian/changelog b/debian/changelog index 0171e9b6..8a61585b 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +rust-proxmox-acme-rs (0.3.2) pve; urgency=medium + + * rebuild with base64 0.13 + + -- Proxmox Support Team Thu, 18 Nov 2021 12:49:25 +0100 + rust-proxmox-acme-rs (0.3.1) pve; urgency=medium * add proxy support diff --git a/debian/control b/debian/control index 73ad9d21..c95cf0ac 100644 --- a/debian/control +++ b/debian/control @@ -6,7 +6,7 @@ Build-Depends: debhelper (>= 12), cargo:native , rustc:native , libstd-rust-dev , - librust-base64-0.12+default-dev , + 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 , @@ -22,7 +22,7 @@ Architecture: any Multi-Arch: same Depends: ${misc:Depends}, - librust-base64-0.12+default-dev, + 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, @@ -35,8 +35,8 @@ Provides: librust-proxmox-acme-rs-0+default-dev (= ${binary:Version}), librust-proxmox-acme-rs-0.3-dev (= ${binary:Version}), librust-proxmox-acme-rs-0.3+default-dev (= ${binary:Version}), - librust-proxmox-acme-rs-0.3.1-dev (= ${binary:Version}), - librust-proxmox-acme-rs-0.3.1+default-dev (= ${binary:Version}) + librust-proxmox-acme-rs-0.3.2-dev (= ${binary:Version}), + librust-proxmox-acme-rs-0.3.2+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. @@ -54,8 +54,8 @@ Provides: librust-proxmox-acme-rs-0+curl-dev (= ${binary:Version}), librust-proxmox-acme-rs-0.3+client-dev (= ${binary:Version}), librust-proxmox-acme-rs-0.3+curl-dev (= ${binary:Version}), - librust-proxmox-acme-rs-0.3.1+client-dev (= ${binary:Version}), - librust-proxmox-acme-rs-0.3.1+curl-dev (= ${binary:Version}) + librust-proxmox-acme-rs-0.3.2+client-dev (= ${binary:Version}), + librust-proxmox-acme-rs-0.3.2+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. From 7622380dd79f38ddde0ec0d61984bff8308104da Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Tue, 1 Feb 2022 10:18:23 +0100 Subject: [PATCH 40/57] switch from curl to ureq Signed-off-by: Wolfgang Bumiller --- Cargo.toml | 11 +++- src/client.rs | 149 +++++++++++++++++++++----------------------------- src/error.rs | 13 +---- 3 files changed, 75 insertions(+), 98 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 573a3a4c..dd16e170 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,11 +16,18 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" openssl = "0.10.29" -curl = { version = "0.4.33", optional = true } +# 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 = ["curl"] +client = ["ureq", "native-tls"] [dev-dependencies] anyhow = "1.0" diff --git a/src/client.rs b/src/client.rs index 4582738c..93721fa3 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,8 +1,8 @@ //! A blocking higher-level ACME client implementation using 'curl'. -use std::convert::TryFrom; +use std::io::Read; +use std::sync::Arc; -use curl::easy; use serde::{Deserialize, Serialize}; use crate::b64u; @@ -73,113 +73,102 @@ pub struct Headers { nonce: Option, } -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, + agent: Option, nonce: Option, proxy: Option, } impl Inner { - pub fn new() -> Self { + 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 { - easy: easy::Easy::new(), + agent: None, nonce: None, proxy: None, } } - pub fn execute( + fn execute( &mut self, method: &[u8], url: &str, - request_body: Option<&[u8]>, + request_body: Option<(&str, &[u8])>, // content-type and body ) -> Result { - 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)?, + 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()); } - self.easy.url(url)?; - - if let Some(p) = &self.proxy { - self.easy.proxy(p)?; + if let Some(value) = response.header(crate::REPLAY_NONCE) { + headers.nonce = Some(value.to_owned()); } - { - let mut transfer = self.easy.transfer(); + let status = response.status(); - transfer.write_function(|data| { - body.extend(data); - Ok(data.len()) - })?; + 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))?; - 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, + body, }) } pub fn set_proxy(&mut self, proxy: String) { self.proxy = Some(proxy); + self.agent = None; } - /// Low-level API to run an n API request. This automatically updates the current nonce! + /// Low-level API to run an API request. This automatically updates the current nonce! fn run_request(&mut self, request: Request) -> Result { - 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))?; - headers.append(&format!("Content-Length: {}", request.body.len()))?; - self.easy - .http_headers(headers) - .map_err(|err| format_err!("curl error: {}", err))?; - Some(request.body.as_bytes()) - } else { + let body = if request.body.is_empty() { None + } else { + Some((request.content_type, request.body.as_bytes())) }; let mut response = self @@ -603,18 +592,6 @@ impl Client { } } -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); diff --git a/src/error.rs b/src/error.rs index 54af9c8d..bcfaed08 100644 --- a/src/error.rs +++ b/src/error.rs @@ -63,13 +63,13 @@ pub enum Error { /// 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 + /// 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), /// If built with the `client` feature, this is where client specific errors which are not from - /// errors forwarded from `curl` end up. + /// errors forwarded from `ureq` end up. Client(String), /// A non-openssl error occurred while building data for the CSR. @@ -142,10 +142,3 @@ impl From for Error { Error::Api(e) } } - -#[cfg(feature = "client")] -impl From for Error { - fn from(e: curl::Error) -> Self { - Error::HttpClient(Box::new(e)) - } -} From fe6294cd3a7f5efeee4a2bf8a54272a944084efc Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Tue, 1 Feb 2022 10:18:32 +0100 Subject: [PATCH 41/57] bump edition to 2021 Signed-off-by: Wolfgang Bumiller --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index dd16e170..b5482967 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "proxmox-acme-rs" version = "0.3.2" authors = ["Wolfgang Bumiller "] -edition = "2018" +edition = "2021" license = "AGPL-3" description = "ACME client library" exclude = [ From abc0bdd09d5c3501534510d49da0ae8fa5c05c05 Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Tue, 1 Feb 2022 10:19:17 +0100 Subject: [PATCH 42/57] bump version to 0.4.0 Signed-off-by: Wolfgang Bumiller --- Cargo.toml | 2 +- debian/changelog | 8 +++++++ debian/control | 60 +++++++++++++++++++++++++++++++++++------------- 3 files changed, 53 insertions(+), 17 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b5482967..ce60b02a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proxmox-acme-rs" -version = "0.3.2" +version = "0.4.0" authors = ["Wolfgang Bumiller "] edition = "2021" license = "AGPL-3" diff --git a/debian/changelog b/debian/changelog index 8a61585b..88590ca6 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,11 @@ +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 Tue, 01 Feb 2022 10:19:29 +0100 + rust-proxmox-acme-rs (0.3.2) pve; urgency=medium * rebuild with base64 0.13 diff --git a/debian/control b/debian/control index c95cf0ac..5ea2f973 100644 --- a/debian/control +++ b/debian/control @@ -2,7 +2,7 @@ Source: rust-proxmox-acme-rs Section: rust Priority: optional Build-Depends: debhelper (>= 12), - dh-cargo (>= 24), + dh-cargo (>= 25), cargo:native , rustc:native , libstd-rust-dev , @@ -28,15 +28,17 @@ Depends: librust-serde-1+derive-dev, librust-serde-json-1+default-dev Suggests: - librust-proxmox-acme-rs+client-dev (= ${binary:Version}) + 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.3-dev (= ${binary:Version}), - librust-proxmox-acme-rs-0.3+default-dev (= ${binary:Version}), - librust-proxmox-acme-rs-0.3.2-dev (= ${binary:Version}), - librust-proxmox-acme-rs-0.3.2+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. @@ -47,17 +49,43 @@ Multi-Arch: same Depends: ${misc:Depends}, librust-proxmox-acme-rs-dev (= ${binary:Version}), - librust-curl-0.4+default-dev (>= 0.4.33-~~) + librust-proxmox-acme-rs+ureq-dev (= ${binary:Version}), + librust-proxmox-acme-rs+native-tls-dev (= ${binary:Version}) 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.3+client-dev (= ${binary:Version}), - librust-proxmox-acme-rs-0.3+curl-dev (= ${binary:Version}), - librust-proxmox-acme-rs-0.3.2+client-dev (= ${binary:Version}), - librust-proxmox-acme-rs-0.3.2+curl-dev (= ${binary:Version}) -Description: ACME client library - feature "client" and 1 more + 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. - . - Additionally, this package also provides the "curl" 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. From 4cab29c57a4a59529e088827a87cd83168450449 Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Tue, 1 Feb 2022 11:10:00 +0100 Subject: [PATCH 43/57] make upload: drop buster Signed-off-by: Wolfgang Bumiller --- Makefile | 1 - 1 file changed, 1 deletion(-) diff --git a/Makefile b/Makefile index 383146f0..6d738f27 100644 --- a/Makefile +++ b/Makefile @@ -41,6 +41,5 @@ upload: deb 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 buster; \ 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 From 65f05daf7e20aca8969ed6efd8997d6d2c83b6bb Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Wed, 2 Feb 2022 14:13:19 +0100 Subject: [PATCH 44/57] doc fixup Signed-off-by: Wolfgang Bumiller --- src/client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index 93721fa3..78c83a20 100644 --- a/src/client.rs +++ b/src/client.rs @@ -276,7 +276,7 @@ impl Client { /// Get the directory URL without querying the `Directory` structure. /// - /// The difference to [`get_directory`](Client::get_directory()) is that this does not + /// 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 From e499b084c8d5c73cfa0a0e28d31b310a8f295bc9 Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Wed, 17 Aug 2022 08:58:44 +0200 Subject: [PATCH 45/57] replace deprecated 'affine_coordinates_gfp' call Signed-off-by: Wolfgang Bumiller --- src/key.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/key.rs b/src/key.rs index 487f65ec..5dbc5460 100644 --- a/src/key.rs +++ b/src/key.rs @@ -106,9 +106,8 @@ impl TryFrom<&openssl::ec::EcKey

> for EcPublicKey { 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)?; + key.public_key() + .affine_coordinates(group, &mut x, &mut y, &mut ctx)?; Ok(EcPublicKey { crv: "P-256", @@ -117,3 +116,14 @@ impl TryFrom<&openssl::ec::EcKey

> for EcPublicKey { }) } } + +#[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(()) +} From ff3fcebda768e3295bf94dde766ac334d16cf1c0 Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Wed, 17 Aug 2022 09:07:41 +0200 Subject: [PATCH 46/57] jws: allocate exact capacity Signed-off-by: Wolfgang Bumiller --- src/jws.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/jws.rs b/src/jws.rs index e61ff69b..b867f714 100644 --- a/src/jws.rs +++ b/src/jws.rs @@ -158,7 +158,7 @@ impl Jws { 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()); + 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())); From ac1f71eddb6564dc56110eb1a713a92809318525 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Gr=C3=BCnbichler?= Date: Thu, 5 Jan 2023 15:07:21 +0100 Subject: [PATCH 47/57] update d/control MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit after debcargo update Signed-off-by: Fabian Grünbichler --- debian/control | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/debian/control b/debian/control index 5ea2f973..760f015b 100644 --- a/debian/control +++ b/debian/control @@ -12,9 +12,10 @@ Build-Depends: debhelper (>= 12), librust-serde-1+derive-dev , librust-serde-json-1+default-dev Maintainer: Proxmox Support Team -Standards-Version: 4.5.1 +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 From 7e6aa2733acd0c2218fe2b71e9579ca663bcbadd Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Tue, 3 Oct 2023 13:45:10 +0200 Subject: [PATCH 48/57] replace deprecated X509Extension::new_nid Signed-off-by: Wolfgang Bumiller --- src/util.rs | 54 +++++++++++++++++++---------------------------------- 1 file changed, 19 insertions(+), 35 deletions(-) diff --git a/src/util.rs b/src/util.rs index 29fafd4e..57acf852 100644 --- a/src/util.rs +++ b/src/util.rs @@ -6,7 +6,7 @@ use openssl::hash::MessageDigest; use openssl::nid::Nid; use openssl::pkey::PKey; use openssl::rsa::Rsa; -use openssl::x509::{X509Extension, X509Name, X509Req}; +use openssl::x509::{self, X509Name, X509Req}; use crate::Error; @@ -55,40 +55,24 @@ impl Csr { let context = csr.x509v3_context(None); let mut ext = openssl::stack::Stack::new()?; - ext.push(X509Extension::new_nid( - None, - None, - Nid::BASIC_CONSTRAINTS, - "CA:FALSE", - )?)?; - ext.push(X509Extension::new_nid( - None, - None, - Nid::KEY_USAGE, - "digitalSignature,keyEncipherment", - )?)?; - ext.push(X509Extension::new_nid( - None, - None, - Nid::EXT_KEY_USAGE, - "serverAuth,clientAuth", - )?)?; - ext.push(X509Extension::new_nid( - None, - Some(&context), - Nid::SUBJECT_ALT_NAME, - &identifiers - .iter() - .try_fold(String::new(), |mut acc, dns| { - if !acc.is_empty() { - acc.push(','); - } - use std::fmt::Write; - write!(acc, "DNS:{}", dns.as_ref())?; - Ok::<_, std::fmt::Error>(acc) - }) - .map_err(|err| Error::Csr(err.to_string()))?, - )?)?; + 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())?; From 88f7e190eccbca6c235a5ff072f3815645fa59c8 Mon Sep 17 00:00:00 2001 From: Folke Gleumes Date: Tue, 14 Nov 2023 15:14:00 +0100 Subject: [PATCH 49/57] add external account binding Functionality was added as a additional setter function, which hopefully prevents any breakages. Since a placeholder Option an the AccountData was already present, but has never been used, replacing the field with an Option of a fully defined type should also be minimally intrusive. Signed-off-by: Folke Gleumes --- src/account.rs | 28 ++++++++++++++++----- src/eab.rs | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/error.rs | 10 ++++++++ src/lib.rs | 1 + 4 files changed, 99 insertions(+), 6 deletions(-) create mode 100644 src/eab.rs diff --git a/src/account.rs b/src/account.rs index 8144d39b..9f3af264 100644 --- a/src/account.rs +++ b/src/account.rs @@ -11,8 +11,9 @@ 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::PublicKey; +use crate::key::{Jwk, PublicKey}; use crate::order::{NewOrder, Order, OrderData}; use crate::request::Request; use crate::Error; @@ -336,10 +337,9 @@ pub struct AccountData { #[serde(skip_serializing_if = "Option::is_none")] pub terms_of_service_agreed: Option, - /// External account information. This is currently not directly supported in any way and only - /// stored to completeness. + /// External account information. #[serde(skip_serializing_if = "Option::is_none")] - pub external_account_binding: Option, + pub external_account_binding: Option, /// This is only used by the client when querying an account. #[serde(default = "default_true", skip_serializing_if = "is_false")] @@ -375,6 +375,7 @@ pub struct AccountCreator { contact: Vec, terms_of_service_agreed: bool, key: Option>, + eab_credentials: Option<(String, PKey)>, } impl AccountCreator { @@ -402,6 +403,13 @@ impl AccountCreator { self } + /// Set the EAB credentials for the account registration + pub fn set_eab_credentials(mut self, kid: String, hmac_key: String) -> Result { + 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 { let key = openssl::rsa::Rsa::generate(bits)?; @@ -431,6 +439,15 @@ impl AccountCreator { /// [`response`](AccountCreator::response()) will render the account unusable! pub fn request(&self, directory: &Directory, nonce: &str) -> Result { 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, @@ -441,12 +458,11 @@ impl AccountCreator { } else { None }, - external_account_binding: None, + external_account_binding, only_return_existing: false, extra: HashMap::new(), }; - let url = directory.new_account_url(); let body = serde_json::to_string(&Jws::new( key, None, diff --git a/src/eab.rs b/src/eab.rs new file mode 100644 index 00000000..a4c06424 --- /dev/null +++ b/src/eab.rs @@ -0,0 +1,66 @@ +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

( + eab_kid: &str, + eab_hmac_key: &PKeyRef

, + jwk: Jwk, + url: String, + ) -> Result + 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

(key: &PKeyRef

, protected: &[u8], payload: &[u8]) -> Result, 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()?) + } +} diff --git a/src/error.rs b/src/error.rs index bcfaed08..59da3ea1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -59,6 +59,9 @@ pub enum Error { /// 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), @@ -121,6 +124,7 @@ impl fmt::Display for Error { 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), } } } @@ -142,3 +146,9 @@ impl From for Error { Error::Api(e) } } + +impl From for Error { + fn from(e: base64::DecodeError) -> Self { + Error::BadBase64(e) + } +} diff --git a/src/lib.rs b/src/lib.rs index 3533b298..98ad04ec 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,6 +14,7 @@ #![deny(missing_docs)] mod b64u; +mod eab; mod json; mod jws; mod key; From 53416e358fdd0d9c0ebcf60b788072bfbc4f0a62 Mon Sep 17 00:00:00 2001 From: Folke Gleumes Date: Tue, 14 Nov 2023 15:14:01 +0100 Subject: [PATCH 50/57] add meta fields returned by the directory According to the rfc, the meta field contains additional fields that weren't covered by the Meta struct. Of the additional fields, only external_account_required will be used in the near future, but others were added for completeness and the case that they might be used in the future. Signed-off-by: Folke Gleumes --- src/directory.rs | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/directory.rs b/src/directory.rs index 755ea8c0..a9d31f27 100644 --- a/src/directory.rs +++ b/src/directory.rs @@ -47,6 +47,18 @@ 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, + + /// Flag indicating if EAB is required, None is equivalent to false + #[serde(skip_serializing_if = "Option::is_none")] + pub external_account_required: Option, + + /// Website with information about the ACME Server + #[serde(skip_serializing_if = "Option::is_none")] + pub website: Option, + + /// List of hostnames used by the CA, intended for the use with caa dns records + #[serde(skip_serializing_if = "Option::is_none")] + pub caa_identities: Option>, } impl Directory { @@ -64,6 +76,17 @@ impl Directory { } } + /// 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 @@ -78,8 +101,6 @@ impl Directory { } /// 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) -> Option<&Meta> { self.data.meta.as_ref() } From d07e4fdb9a3d1d05f43f968aea1b7d815ac4a2dc Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Mon, 4 Dec 2023 10:13:46 +0100 Subject: [PATCH 51/57] Option> -> Vec<> Signed-off-by: Wolfgang Bumiller --- src/directory.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/directory.rs b/src/directory.rs index a9d31f27..ed8203f9 100644 --- a/src/directory.rs +++ b/src/directory.rs @@ -57,8 +57,8 @@ pub struct Meta { pub website: Option, /// List of hostnames used by the CA, intended for the use with caa dns records - #[serde(skip_serializing_if = "Option::is_none")] - pub caa_identities: Option>, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub caa_identities: Vec, } impl Directory { From 6e1e835739012d4b4ce75938e886ed9c7f983ef2 Mon Sep 17 00:00:00 2001 From: Folke Gleumes Date: Tue, 14 Nov 2023 15:14:02 +0100 Subject: [PATCH 52/57] expand helper function by eab credentials Signed-off-by: Folke Gleumes --- src/client.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/client.rs b/src/client.rs index 78c83a20..53f2688a 100644 --- a/src/client.rs +++ b/src/client.rs @@ -367,10 +367,14 @@ impl Client { contact: Vec, tos_agreed: bool, rsa_bits: Option, + eab_creds: Option<(String, String)>, ) -> Result<&Account, Error> { - let account = Account::creator() + 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 { From a1b59c8a23b63f506db3113b7820a5493eceea31 Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Mon, 4 Dec 2023 11:41:59 +0100 Subject: [PATCH 53/57] move to proxmox-acme Signed-off-by: Wolfgang Bumiller --- .gitignore | 2 - Makefile | 45 ------------------- Cargo.toml => proxmox-acme/Cargo.toml | 0 {debian => proxmox-acme/debian}/changelog | 0 {debian => proxmox-acme/debian}/control | 0 {debian => proxmox-acme/debian}/copyright | 0 {debian => proxmox-acme/debian}/debcargo.toml | 0 {debian => proxmox-acme/debian}/source/format | 0 rustfmt.toml => proxmox-acme/rustfmt.toml | 0 {src => proxmox-acme/src}/account.rs | 0 {src => proxmox-acme/src}/authorization.rs | 0 {src => proxmox-acme/src}/b64u.rs | 0 {src => proxmox-acme/src}/client.rs | 0 {src => proxmox-acme/src}/directory.rs | 0 {src => proxmox-acme/src}/eab.rs | 0 {src => proxmox-acme/src}/error.rs | 0 {src => proxmox-acme/src}/json.rs | 0 {src => proxmox-acme/src}/jws.rs | 0 {src => proxmox-acme/src}/key.rs | 0 {src => proxmox-acme/src}/lib.rs | 0 {src => proxmox-acme/src}/order.rs | 0 {src => proxmox-acme/src}/request.rs | 0 {src => proxmox-acme/src}/util.rs | 0 23 files changed, 47 deletions(-) delete mode 100644 .gitignore delete mode 100644 Makefile rename Cargo.toml => proxmox-acme/Cargo.toml (100%) rename {debian => proxmox-acme/debian}/changelog (100%) rename {debian => proxmox-acme/debian}/control (100%) rename {debian => proxmox-acme/debian}/copyright (100%) rename {debian => proxmox-acme/debian}/debcargo.toml (100%) rename {debian => proxmox-acme/debian}/source/format (100%) rename rustfmt.toml => proxmox-acme/rustfmt.toml (100%) rename {src => proxmox-acme/src}/account.rs (100%) rename {src => proxmox-acme/src}/authorization.rs (100%) rename {src => proxmox-acme/src}/b64u.rs (100%) rename {src => proxmox-acme/src}/client.rs (100%) rename {src => proxmox-acme/src}/directory.rs (100%) rename {src => proxmox-acme/src}/eab.rs (100%) rename {src => proxmox-acme/src}/error.rs (100%) rename {src => proxmox-acme/src}/json.rs (100%) rename {src => proxmox-acme/src}/jws.rs (100%) rename {src => proxmox-acme/src}/key.rs (100%) rename {src => proxmox-acme/src}/lib.rs (100%) rename {src => proxmox-acme/src}/order.rs (100%) rename {src => proxmox-acme/src}/request.rs (100%) rename {src => proxmox-acme/src}/util.rs (100%) diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 96ef6c0b..00000000 --- a/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/target -Cargo.lock diff --git a/Makefile b/Makefile deleted file mode 100644 index 6d738f27..00000000 --- a/Makefile +++ /dev/null @@ -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 diff --git a/Cargo.toml b/proxmox-acme/Cargo.toml similarity index 100% rename from Cargo.toml rename to proxmox-acme/Cargo.toml diff --git a/debian/changelog b/proxmox-acme/debian/changelog similarity index 100% rename from debian/changelog rename to proxmox-acme/debian/changelog diff --git a/debian/control b/proxmox-acme/debian/control similarity index 100% rename from debian/control rename to proxmox-acme/debian/control diff --git a/debian/copyright b/proxmox-acme/debian/copyright similarity index 100% rename from debian/copyright rename to proxmox-acme/debian/copyright diff --git a/debian/debcargo.toml b/proxmox-acme/debian/debcargo.toml similarity index 100% rename from debian/debcargo.toml rename to proxmox-acme/debian/debcargo.toml diff --git a/debian/source/format b/proxmox-acme/debian/source/format similarity index 100% rename from debian/source/format rename to proxmox-acme/debian/source/format diff --git a/rustfmt.toml b/proxmox-acme/rustfmt.toml similarity index 100% rename from rustfmt.toml rename to proxmox-acme/rustfmt.toml diff --git a/src/account.rs b/proxmox-acme/src/account.rs similarity index 100% rename from src/account.rs rename to proxmox-acme/src/account.rs diff --git a/src/authorization.rs b/proxmox-acme/src/authorization.rs similarity index 100% rename from src/authorization.rs rename to proxmox-acme/src/authorization.rs diff --git a/src/b64u.rs b/proxmox-acme/src/b64u.rs similarity index 100% rename from src/b64u.rs rename to proxmox-acme/src/b64u.rs diff --git a/src/client.rs b/proxmox-acme/src/client.rs similarity index 100% rename from src/client.rs rename to proxmox-acme/src/client.rs diff --git a/src/directory.rs b/proxmox-acme/src/directory.rs similarity index 100% rename from src/directory.rs rename to proxmox-acme/src/directory.rs diff --git a/src/eab.rs b/proxmox-acme/src/eab.rs similarity index 100% rename from src/eab.rs rename to proxmox-acme/src/eab.rs diff --git a/src/error.rs b/proxmox-acme/src/error.rs similarity index 100% rename from src/error.rs rename to proxmox-acme/src/error.rs diff --git a/src/json.rs b/proxmox-acme/src/json.rs similarity index 100% rename from src/json.rs rename to proxmox-acme/src/json.rs diff --git a/src/jws.rs b/proxmox-acme/src/jws.rs similarity index 100% rename from src/jws.rs rename to proxmox-acme/src/jws.rs diff --git a/src/key.rs b/proxmox-acme/src/key.rs similarity index 100% rename from src/key.rs rename to proxmox-acme/src/key.rs diff --git a/src/lib.rs b/proxmox-acme/src/lib.rs similarity index 100% rename from src/lib.rs rename to proxmox-acme/src/lib.rs diff --git a/src/order.rs b/proxmox-acme/src/order.rs similarity index 100% rename from src/order.rs rename to proxmox-acme/src/order.rs diff --git a/src/request.rs b/proxmox-acme/src/request.rs similarity index 100% rename from src/request.rs rename to proxmox-acme/src/request.rs diff --git a/src/util.rs b/proxmox-acme/src/util.rs similarity index 100% rename from src/util.rs rename to proxmox-acme/src/util.rs From 6773460d8932997442a9d58e71d958039e05df01 Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Mon, 4 Dec 2023 11:42:42 +0100 Subject: [PATCH 54/57] drop -rs suffix Signed-off-by: Wolfgang Bumiller --- proxmox-acme/Cargo.toml | 2 +- proxmox-acme/debian/control | 68 ++++++++++++++++++------------------- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/proxmox-acme/Cargo.toml b/proxmox-acme/Cargo.toml index ce60b02a..c735844a 100644 --- a/proxmox-acme/Cargo.toml +++ b/proxmox-acme/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "proxmox-acme-rs" +name = "proxmox-acme" version = "0.4.0" authors = ["Wolfgang Bumiller "] edition = "2021" diff --git a/proxmox-acme/debian/control b/proxmox-acme/debian/control index 760f015b..5ef643be 100644 --- a/proxmox-acme/debian/control +++ b/proxmox-acme/debian/control @@ -1,4 +1,4 @@ -Source: rust-proxmox-acme-rs +Source: rust-proxmox-acme Section: rust Priority: optional Build-Depends: debhelper (>= 12), @@ -15,10 +15,10 @@ Maintainer: Proxmox Support Team Standards-Version: 4.6.1 Vcs-Git: Vcs-Browser: -X-Cargo-Crate: proxmox-acme-rs +X-Cargo-Crate: proxmox-acme Rules-Requires-Root: no -Package: librust-proxmox-acme-rs-dev +Package: librust-proxmox-acme-dev Architecture: any Multi-Arch: same Depends: @@ -29,64 +29,64 @@ Depends: 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}) + librust-proxmox-acme+client-dev (= ${binary:Version}), + librust-proxmox-acme+native-tls-dev (= ${binary:Version}), + librust-proxmox-acme+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}) + librust-proxmox-acme+default-dev (= ${binary:Version}), + librust-proxmox-acme-0-dev (= ${binary:Version}), + librust-proxmox-acme-0+default-dev (= ${binary:Version}), + librust-proxmox-acme-0.4-dev (= ${binary:Version}), + librust-proxmox-acme-0.4+default-dev (= ${binary:Version}), + librust-proxmox-acme-0.4.0-dev (= ${binary:Version}), + librust-proxmox-acme-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 + This package contains the source for the Rust proxmox-acme crate, packaged by debcargo for use with cargo and dh-cargo. -Package: librust-proxmox-acme-rs+client-dev +Package: librust-proxmox-acme+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}) + librust-proxmox-acme-dev (= ${binary:Version}), + librust-proxmox-acme+ureq-dev (= ${binary:Version}), + librust-proxmox-acme+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}) + librust-proxmox-acme-0+client-dev (= ${binary:Version}), + librust-proxmox-acme-0.4+client-dev (= ${binary:Version}), + librust-proxmox-acme-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, + This metapackage enables feature "client" for the Rust proxmox-acme crate, by pulling in any additional dependencies needed by that feature. -Package: librust-proxmox-acme-rs+native-tls-dev +Package: librust-proxmox-acme+native-tls-dev Architecture: any Multi-Arch: same Depends: ${misc:Depends}, - librust-proxmox-acme-rs-dev (= ${binary:Version}), + librust-proxmox-acme-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}) + librust-proxmox-acme-0+native-tls-dev (= ${binary:Version}), + librust-proxmox-acme-0.4+native-tls-dev (= ${binary:Version}), + librust-proxmox-acme-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 + This metapackage enables feature "native-tls" for the Rust proxmox-acme crate, by pulling in any additional dependencies needed by that feature. -Package: librust-proxmox-acme-rs+ureq-dev +Package: librust-proxmox-acme+ureq-dev Architecture: any Multi-Arch: same Depends: ${misc:Depends}, - librust-proxmox-acme-rs-dev (= ${binary:Version}), + librust-proxmox-acme-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}) + librust-proxmox-acme-0+ureq-dev (= ${binary:Version}), + librust-proxmox-acme-0.4+ureq-dev (= ${binary:Version}), + librust-proxmox-acme-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 + This metapackage enables feature "ureq" for the Rust proxmox-acme crate, by pulling in any additional dependencies needed by that feature. From b212febefc1c90af8adab1e9b18edb164157610f Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Mon, 4 Dec 2023 11:42:54 +0100 Subject: [PATCH 55/57] drop rustfmt.toml Signed-off-by: Wolfgang Bumiller --- proxmox-acme/rustfmt.toml | 1 - 1 file changed, 1 deletion(-) delete mode 100644 proxmox-acme/rustfmt.toml diff --git a/proxmox-acme/rustfmt.toml b/proxmox-acme/rustfmt.toml deleted file mode 100644 index 32a9786f..00000000 --- a/proxmox-acme/rustfmt.toml +++ /dev/null @@ -1 +0,0 @@ -edition = "2018" From e7037250499deed555352f7161659d29d7d59efa Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Mon, 4 Dec 2023 11:45:00 +0100 Subject: [PATCH 56/57] add proxmox-acme to workspace Signed-off-by: Wolfgang Bumiller --- Cargo.toml | 1 + proxmox-acme/Cargo.toml | 26 +++++++++++++------------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ceae47ef..a394e7dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ + "proxmox-acme", "proxmox-api-macro", "proxmox-apt", "proxmox-async", diff --git a/proxmox-acme/Cargo.toml b/proxmox-acme/Cargo.toml index c735844a..34c409a3 100644 --- a/proxmox-acme/Cargo.toml +++ b/proxmox-acme/Cargo.toml @@ -1,23 +1,23 @@ [package] name = "proxmox-acme" version = "0.4.0" -authors = ["Wolfgang Bumiller "] -edition = "2021" -license = "AGPL-3" description = "ACME client library" -exclude = [ - "build", - "debian", -] +authors.workspace = true +license.workspace = true +edition.workspace = true +homepage.workspace = true +repository.workspace = true + +exclude = [ "debian" ] [dependencies] -base64 = "0.13.0" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -openssl = "0.10.29" +base64.workspace = true +serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true +openssl.workspace = true # For the client -native-tls = { version = "0.2", optional = true } +native-tls = { workspace = true, optional = true } [dependencies.ureq] optional = true @@ -30,4 +30,4 @@ default = [] client = ["ureq", "native-tls"] [dev-dependencies] -anyhow = "1.0" +anyhow.workspace = true From 54784e591ef3e3ceafaef3688231231447f1e82a Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Mon, 4 Dec 2023 11:46:38 +0100 Subject: [PATCH 57/57] bump proxmox-acme to 0.5.0 Signed-off-by: Wolfgang Bumiller --- proxmox-acme/Cargo.toml | 2 +- proxmox-acme/debian/changelog | 14 +++++++++++++ proxmox-acme/debian/control | 37 ++++++++++++++++++----------------- 3 files changed, 34 insertions(+), 19 deletions(-) diff --git a/proxmox-acme/Cargo.toml b/proxmox-acme/Cargo.toml index 34c409a3..8f8f6e1c 100644 --- a/proxmox-acme/Cargo.toml +++ b/proxmox-acme/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "proxmox-acme" -version = "0.4.0" +version = "0.5.0" description = "ACME client library" authors.workspace = true license.workspace = true diff --git a/proxmox-acme/debian/changelog b/proxmox-acme/debian/changelog index 88590ca6..902c2459 100644 --- a/proxmox-acme/debian/changelog +++ b/proxmox-acme/debian/changelog @@ -1,3 +1,17 @@ +rust-proxmox-acme (0.5.0) bookworm; urgency=medium + + * add external account binding support + + * add a few more standard fields to Meta + + * update deprecated openssl calls + + * documentation fixups + + * general code improvements and cleanups + + -- Proxmox Support Team Mon, 04 Dec 2023 11:46:26 +0100 + rust-proxmox-acme-rs (0.4.0) pve; urgency=medium * switch from curl to ureq with native-tls diff --git a/proxmox-acme/debian/control b/proxmox-acme/debian/control index 5ef643be..f30db43b 100644 --- a/proxmox-acme/debian/control +++ b/proxmox-acme/debian/control @@ -7,7 +7,7 @@ Build-Depends: debhelper (>= 12), rustc:native , libstd-rust-dev , librust-base64-0.13+default-dev , - librust-openssl-0.10+default-dev (>= 0.10.29-~~) , + librust-openssl-0.10+default-dev , librust-serde-1+default-dev , librust-serde-1+derive-dev , librust-serde-json-1+default-dev @@ -15,6 +15,7 @@ Maintainer: Proxmox Support Team Standards-Version: 4.6.1 Vcs-Git: Vcs-Browser: +Homepage: https://proxmox.com X-Cargo-Crate: proxmox-acme Rules-Requires-Root: no @@ -24,7 +25,7 @@ Multi-Arch: same Depends: ${misc:Depends}, librust-base64-0.13+default-dev, - librust-openssl-0.10+default-dev (>= 0.10.29-~~), + librust-openssl-0.10+default-dev, librust-serde-1+default-dev, librust-serde-1+derive-dev, librust-serde-json-1+default-dev @@ -36,13 +37,13 @@ Provides: librust-proxmox-acme+default-dev (= ${binary:Version}), librust-proxmox-acme-0-dev (= ${binary:Version}), librust-proxmox-acme-0+default-dev (= ${binary:Version}), - librust-proxmox-acme-0.4-dev (= ${binary:Version}), - librust-proxmox-acme-0.4+default-dev (= ${binary:Version}), - librust-proxmox-acme-0.4.0-dev (= ${binary:Version}), - librust-proxmox-acme-0.4.0+default-dev (= ${binary:Version}) + librust-proxmox-acme-0.5-dev (= ${binary:Version}), + librust-proxmox-acme-0.5+default-dev (= ${binary:Version}), + librust-proxmox-acme-0.5.0-dev (= ${binary:Version}), + librust-proxmox-acme-0.5.0+default-dev (= ${binary:Version}) Description: ACME client library - Rust source code - This package contains the source for the Rust proxmox-acme crate, packaged - by debcargo for use with cargo and dh-cargo. + This package contains the source for the Rust proxmox-acme crate, packaged by + debcargo for use with cargo and dh-cargo. Package: librust-proxmox-acme+client-dev Architecture: any @@ -54,11 +55,11 @@ Depends: librust-proxmox-acme+native-tls-dev (= ${binary:Version}) Provides: librust-proxmox-acme-0+client-dev (= ${binary:Version}), - librust-proxmox-acme-0.4+client-dev (= ${binary:Version}), - librust-proxmox-acme-0.4.0+client-dev (= ${binary:Version}) + librust-proxmox-acme-0.5+client-dev (= ${binary:Version}), + librust-proxmox-acme-0.5.0+client-dev (= ${binary:Version}) Description: ACME client library - feature "client" - This metapackage enables feature "client" for the Rust proxmox-acme crate, - by pulling in any additional dependencies needed by that feature. + This metapackage enables feature "client" for the Rust proxmox-acme crate, by + pulling in any additional dependencies needed by that feature. Package: librust-proxmox-acme+native-tls-dev Architecture: any @@ -69,11 +70,11 @@ Depends: librust-native-tls-0.2+default-dev Provides: librust-proxmox-acme-0+native-tls-dev (= ${binary:Version}), - librust-proxmox-acme-0.4+native-tls-dev (= ${binary:Version}), - librust-proxmox-acme-0.4.0+native-tls-dev (= ${binary:Version}) + librust-proxmox-acme-0.5+native-tls-dev (= ${binary:Version}), + librust-proxmox-acme-0.5.0+native-tls-dev (= ${binary:Version}) Description: ACME client library - feature "native-tls" - This metapackage enables feature "native-tls" for the Rust proxmox-acme - crate, by pulling in any additional dependencies needed by that feature. + This metapackage enables feature "native-tls" for the Rust proxmox-acme crate, + by pulling in any additional dependencies needed by that feature. Package: librust-proxmox-acme+ureq-dev Architecture: any @@ -85,8 +86,8 @@ Depends: librust-ureq-2+native-tls-dev (>= 2.4-~~) Provides: librust-proxmox-acme-0+ureq-dev (= ${binary:Version}), - librust-proxmox-acme-0.4+ureq-dev (= ${binary:Version}), - librust-proxmox-acme-0.4.0+ureq-dev (= ${binary:Version}) + librust-proxmox-acme-0.5+ureq-dev (= ${binary:Version}), + librust-proxmox-acme-0.5.0+ureq-dev (= ${binary:Version}) Description: ACME client library - feature "ureq" This metapackage enables feature "ureq" for the Rust proxmox-acme crate, by pulling in any additional dependencies needed by that feature.