]> git.proxmox.com Git - proxmox.git/commitdiff
move to proxmox-acme
authorWolfgang Bumiller <w.bumiller@proxmox.com>
Mon, 4 Dec 2023 10:41:59 +0000 (11:41 +0100)
committerWolfgang Bumiller <w.bumiller@proxmox.com>
Mon, 4 Dec 2023 10:41:59 +0000 (11:41 +0100)
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
44 files changed:
.gitignore [deleted file]
Cargo.toml [deleted file]
Makefile [deleted file]
debian/changelog [deleted file]
debian/control [deleted file]
debian/copyright [deleted file]
debian/debcargo.toml [deleted file]
debian/source/format [deleted file]
proxmox-acme/Cargo.toml [new file with mode: 0644]
proxmox-acme/debian/changelog [new file with mode: 0644]
proxmox-acme/debian/control [new file with mode: 0644]
proxmox-acme/debian/copyright [new file with mode: 0644]
proxmox-acme/debian/debcargo.toml [new file with mode: 0644]
proxmox-acme/debian/source/format [new file with mode: 0644]
proxmox-acme/rustfmt.toml [new file with mode: 0644]
proxmox-acme/src/account.rs [new file with mode: 0644]
proxmox-acme/src/authorization.rs [new file with mode: 0644]
proxmox-acme/src/b64u.rs [new file with mode: 0644]
proxmox-acme/src/client.rs [new file with mode: 0644]
proxmox-acme/src/directory.rs [new file with mode: 0644]
proxmox-acme/src/eab.rs [new file with mode: 0644]
proxmox-acme/src/error.rs [new file with mode: 0644]
proxmox-acme/src/json.rs [new file with mode: 0644]
proxmox-acme/src/jws.rs [new file with mode: 0644]
proxmox-acme/src/key.rs [new file with mode: 0644]
proxmox-acme/src/lib.rs [new file with mode: 0644]
proxmox-acme/src/order.rs [new file with mode: 0644]
proxmox-acme/src/request.rs [new file with mode: 0644]
proxmox-acme/src/util.rs [new file with mode: 0644]
rustfmt.toml [deleted file]
src/account.rs [deleted file]
src/authorization.rs [deleted file]
src/b64u.rs [deleted file]
src/client.rs [deleted file]
src/directory.rs [deleted file]
src/eab.rs [deleted file]
src/error.rs [deleted file]
src/json.rs [deleted file]
src/jws.rs [deleted file]
src/key.rs [deleted file]
src/lib.rs [deleted file]
src/order.rs [deleted file]
src/request.rs [deleted file]
src/util.rs [deleted file]

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