all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Wolfgang Bumiller <w.bumiller@proxmox.com>
To: pmg-devel@lists.proxmox.com
Subject: [pmg-devel] [PATCH perl-rs 4/7] import pmg-rs
Date: Fri, 26 Nov 2021 14:55:15 +0100	[thread overview]
Message-ID: <20211126135524.117846-12-w.bumiller@proxmox.com> (raw)
In-Reply-To: <20211126135524.117846-1-w.bumiller@proxmox.com>

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
---
 Cargo.toml                     |   1 +
 Makefile                       |   7 +-
 pmg-rs/Cargo.toml              |  33 +++
 pmg-rs/Makefile                |  75 ++++++
 pmg-rs/debian/changelog        |  50 ++++
 pmg-rs/debian/compat           |   1 +
 pmg-rs/debian/control          |  27 +++
 pmg-rs/debian/copyright        |  16 ++
 pmg-rs/debian/debcargo.toml    |  10 +
 pmg-rs/debian/rules            |   7 +
 pmg-rs/debian/source/format    |   1 +
 pmg-rs/debian/triggers         |   1 +
 pmg-rs/src/acme.rs             | 430 +++++++++++++++++++++++++++++++++
 pmg-rs/src/apt/mod.rs          |   1 +
 pmg-rs/src/apt/repositories.rs | 162 +++++++++++++
 pmg-rs/src/csr.rs              |  24 ++
 pmg-rs/src/lib.rs              |   3 +
 pmg-rs/test.pl                 | 172 +++++++++++++
 18 files changed, 1018 insertions(+), 3 deletions(-)
 create mode 100644 pmg-rs/Cargo.toml
 create mode 100644 pmg-rs/Makefile
 create mode 100644 pmg-rs/debian/changelog
 create mode 100644 pmg-rs/debian/compat
 create mode 100644 pmg-rs/debian/control
 create mode 100644 pmg-rs/debian/copyright
 create mode 100644 pmg-rs/debian/debcargo.toml
 create mode 100755 pmg-rs/debian/rules
 create mode 100644 pmg-rs/debian/source/format
 create mode 100644 pmg-rs/debian/triggers
 create mode 100644 pmg-rs/src/acme.rs
 create mode 100644 pmg-rs/src/apt/mod.rs
 create mode 100644 pmg-rs/src/apt/repositories.rs
 create mode 100644 pmg-rs/src/csr.rs
 create mode 100644 pmg-rs/src/lib.rs
 create mode 100644 pmg-rs/test.pl

diff --git a/Cargo.toml b/Cargo.toml
index 8556b45..6b869d4 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -2,6 +2,7 @@
 exclude = [ "build", "perl-*" ]
 members = [
     "pve-rs",
+    "pmg-rs",
 ]
 
 [patch.crates-io]
diff --git a/Makefile b/Makefile
index f8dd85a..2cc02fe 100644
--- a/Makefile
+++ b/Makefile
@@ -28,14 +28,15 @@ build:
 	echo system >build/rust-toolchain
 	cp -a ./perl-* ./build/
 	cp -a ./pve-rs ./build
+	cp -a ./pmg-rs ./build
 
 pve-deb: build
 	cd ./build/pve-rs && dpkg-buildpackage -b -uc -us
 	touch $@
 
-# pmg-deb: build
-# 	cd ./build/pmg-rs && dpkg-buildpackage -b -uc -us
-# 	touch $@
+pmg-deb: build
+	cd ./build/pmg-rs && dpkg-buildpackage -b -uc -us
+	touch $@
 
 %-upload: %-deb
 	cd build; \
diff --git a/pmg-rs/Cargo.toml b/pmg-rs/Cargo.toml
new file mode 100644
index 0000000..02f59de
--- /dev/null
+++ b/pmg-rs/Cargo.toml
@@ -0,0 +1,33 @@
+[package]
+name = "pmg-rs"
+version = "0.3.2"
+authors = [
+    "Proxmox Support Team <support@proxmox.com>",
+    "Wolfgang Bumiller <w.bumiller@proxmox.com>",
+    "Fabian Ebner <f.ebner@proxmox.com>",
+]
+edition = "2018"
+license = "AGPL-3"
+description = "PMG parts which have been ported to rust"
+exclude = [
+    "build",
+    "debian",
+    "PMG",
+]
+
+[lib]
+crate-type = [ "cdylib" ]
+
+[dependencies]
+anyhow = "1.0"
+hex = "0.4"
+openssl = "0.10.32"
+serde = "1.0"
+serde_bytes = "0.11.3"
+serde_json = "1.0"
+
+perlmod = { version = "0.8.1", features = [ "exporter" ] }
+
+proxmox-acme-rs = { version = "0.3.1", features = ["client"] }
+
+proxmox-apt = "0.8.0"
diff --git a/pmg-rs/Makefile b/pmg-rs/Makefile
new file mode 100644
index 0000000..a290544
--- /dev/null
+++ b/pmg-rs/Makefile
@@ -0,0 +1,75 @@
+include /usr/share/dpkg/default.mk
+
+PACKAGE=libpmg-rs-perl
+
+ARCH:=$(shell dpkg-architecture -qDEB_BUILD_ARCH)
+export GITVERSION:=$(shell git rev-parse HEAD)
+
+PERL_INSTALLVENDORARCH != perl -MConfig -e 'print $$Config{installvendorarch};'
+PERL_INSTALLVENDORLIB != perl -MConfig -e 'print $$Config{installvendorlib};'
+
+MAIN_DEB=${PACKAGE}_${DEB_VERSION}_${ARCH}.deb
+DBGSYM_DEB=${PACKAGE}-dbgsym_${DEB_VERSION}_${ARCH}.deb
+DEBS=$(MAIN_DEB) $(DBGSYM_DEB)
+
+DESTDIR=
+
+PM_DIRS := \
+	PMG/RS/APT
+
+PM_FILES := \
+	PMG/RS/Acme.pm \
+	PMG/RS/APT/Repositories.pm \
+	PMG/RS/CSR.pm
+
+ifeq ($(BUILD_MODE), release)
+CARGO_BUILD_ARGS += --release
+endif
+
+all:
+ifneq ($(BUILD_MODE), skip)
+	cargo build $(CARGO_BUILD_ARGS)
+endif
+
+# always re-create this dir
+# but also copy the local target/ and PMG/ dirs as a build-cache
+.PHONY: build
+build:
+	rm -rf build
+	cargo build --release
+	rsync -a debian Makefile Cargo.toml Cargo.lock src target PMG build/
+
+.PHONY: install
+install: target/release/libpmg_rs.so
+	install -d -m755 $(DESTDIR)$(PERL_INSTALLVENDORARCH)/auto
+	install -m644 target/release/libpmg_rs.so $(DESTDIR)$(PERL_INSTALLVENDORARCH)/auto/libpmg_rs.so
+	install -d -m755 $(DESTDIR)$(PERL_INSTALLVENDORLIB)/PMG/RS
+	for i in $(PM_DIRS); do \
+	  install -d -m755 $(DESTDIR)$(PERL_INSTALLVENDORLIB)/$$i; \
+	done
+	for i in $(PM_FILES); do \
+	  install -m644 $$i $(DESTDIR)$(PERL_INSTALLVENDORLIB)/$$i; \
+	done
+
+.PHONY: deb
+deb: $(MAIN_DEB)
+$(MAIN_DEB): build
+	cd build; dpkg-buildpackage -b -us -uc --no-pre-clean
+	lintian $(DEBS)
+
+distclean: clean
+
+clean:
+	cargo clean
+	rm -rf *.deb *.dsc *.tar.gz *.buildinfo *.changes Cargo.lock build
+	find . -name '*~' -exec rm {} ';'
+
+.PHONY: dinstall
+dinstall: ${DEBS}
+	dpkg -i ${DEBS}
+
+.PHONY: upload
+upload: ${DEBS}
+	# check if working directory is clean
+	git diff --exit-code --stat && git diff --exit-code --stat --staged
+	tar cf - ${DEBS} | ssh -X repoman@repo.proxmox.com upload --product pmg --dist bullseye
diff --git a/pmg-rs/debian/changelog b/pmg-rs/debian/changelog
new file mode 100644
index 0000000..afc3e60
--- /dev/null
+++ b/pmg-rs/debian/changelog
@@ -0,0 +1,50 @@
+libpmg-rs-perl (0.3.2) bullseye; urgency=medium
+
+  * acme: add proxy support
+
+ -- Proxmox Support Team <support@proxmox.com>  Thu, 18 Nov 2021 11:18:01 +0100
+
+libpmg-rs-perl (0.3.1) bullseye; urgency=medium
+
+  * update to proxmox-acme-rs 0.3
+
+ -- Proxmox Support Team <support@proxmox.com>  Thu, 21 Oct 2021 13:13:46 +0200
+
+libpmg-rs-perl (0.3.0) bullseye; urgency=medium
+
+  * update proxmox-apt to 0.6.0
+
+ -- Proxmox Support Team <support@proxmox.com>  Fri, 30 Jul 2021 10:56:35 +0200
+
+libpmg-rs-perl (0.2.0-1) bullseye; urgency=medium
+
+  * add bindings for proxmox-apt
+
+ -- Proxmox Support Team <support@proxmox.com>  Tue, 13 Jul 2021 12:48:04 +0200
+
+libpmg-rs-perl (0.1.3-1) bullseye; urgency=medium
+
+  * re-build for Proxmox Mail Gateway 7 / Debian 11 Bullseye
+
+ -- Proxmox Support Team <support@proxmox.com>  Thu, 27 May 2021 19:58:08 +0200
+
+libpmg-rs-perl (0.1.2-1) buster; urgency=medium
+
+  * update proxmox-acme-rs to 0.1.4 to store the 'created' account field if it
+    is available
+
+  * set account file permission to 0700
+
+ -- Proxmox Support Team <support@proxmox.com>  Mon, 29 Mar 2021 11:22:54 +0200
+
+libpmg-rs-perl (0.1.1-1) unstable; urgency=medium
+
+  * update proxmox-acme-rs to 0.1.3 to fix ecsda signature padding
+
+ -- Proxmox Support Team <support@proxmox.com>  Wed, 17 Mar 2021 13:43:12 +0100
+
+libpmg-rs-perl (0.1-1) unstable; urgency=medium
+
+  * initial release
+
+ -- Proxmox Support Team <support@proxmox.com>  Mon, 22 Feb 2021 13:40:10 +0100
diff --git a/pmg-rs/debian/compat b/pmg-rs/debian/compat
new file mode 100644
index 0000000..48082f7
--- /dev/null
+++ b/pmg-rs/debian/compat
@@ -0,0 +1 @@
+12
diff --git a/pmg-rs/debian/control b/pmg-rs/debian/control
new file mode 100644
index 0000000..be632ba
--- /dev/null
+++ b/pmg-rs/debian/control
@@ -0,0 +1,27 @@
+Source: libpmg-rs-perl
+Section: perl
+Priority: optional
+Maintainer: Proxmox Support Team <support@proxmox.com>
+Build-Depends:
+ debhelper (>= 12),
+ librust-anyhow-1+default-dev,
+ librust-hex-0.4+default-dev,
+ librust-openssl-0.10+default-dev (>= 0.10.32-~~),
+ librust-perlmod-0.8+default-dev,
+ librust-perlmod-0.8+exporter-dev,
+ librust-proxmox-acme-rs-0.3+client-dev (>= 0.3.1-~~),
+ librust-proxmox-acme-rs-0.3+default-dev (>= 0.3.1-~~),
+ librust-proxmox-apt-0.8+default-dev,
+ librust-serde-1+default-dev,
+ librust-serde-bytes-0.11+default-dev (>= 0.11.3-~~),
+ librust-serde-json-1+default-dev,
+Standards-Version: 4.3.0
+Homepage: https://www.proxmox.com
+
+Package: libpmg-rs-perl
+Architecture: any
+Depends: ${perl:Depends},
+         ${shlibs:Depends},
+Description: Components of Proxmox Mail Gateway which have been ported to Rust.
+ Contains parts of Proxmox Mail Gateway which have been ported to, or newly
+ implemented in the Rust programming language.
diff --git a/pmg-rs/debian/copyright b/pmg-rs/debian/copyright
new file mode 100644
index 0000000..477c305
--- /dev/null
+++ b/pmg-rs/debian/copyright
@@ -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/pmg-rs/debian/debcargo.toml b/pmg-rs/debian/debcargo.toml
new file mode 100644
index 0000000..8aa085f
--- /dev/null
+++ b/pmg-rs/debian/debcargo.toml
@@ -0,0 +1,10 @@
+overlay = "."
+crate_src_path = ".."
+maintainer = "Proxmox Support Team <support@proxmox.com>"
+
+[source]
+section = "perl"
+vcs_git = "git://git.proxmox.com/git/proxmox.git"
+vcs_browser = "https://git.proxmox.com/?p=proxmox.git"
+
+[packages.libpmg-rs-perl]
diff --git a/pmg-rs/debian/rules b/pmg-rs/debian/rules
new file mode 100755
index 0000000..0f5be05
--- /dev/null
+++ b/pmg-rs/debian/rules
@@ -0,0 +1,7 @@
+#!/usr/bin/make -f
+
+#export DH_VERBOSE=1
+export BUILD_MODE=release
+
+%:
+	dh $@
diff --git a/pmg-rs/debian/source/format b/pmg-rs/debian/source/format
new file mode 100644
index 0000000..89ae9db
--- /dev/null
+++ b/pmg-rs/debian/source/format
@@ -0,0 +1 @@
+3.0 (native)
diff --git a/pmg-rs/debian/triggers b/pmg-rs/debian/triggers
new file mode 100644
index 0000000..59dd688
--- /dev/null
+++ b/pmg-rs/debian/triggers
@@ -0,0 +1 @@
+activate-noawait pve-api-updates
diff --git a/pmg-rs/src/acme.rs b/pmg-rs/src/acme.rs
new file mode 100644
index 0000000..0429a0d
--- /dev/null
+++ b/pmg-rs/src/acme.rs
@@ -0,0 +1,430 @@
+//! `PMG::RS::Acme` perl module.
+//!
+//! The functions in here are perl bindings.
+
+use std::fs::OpenOptions;
+use std::io::{self, Write};
+use std::os::unix::fs::OpenOptionsExt;
+
+use anyhow::{format_err, Error};
+use serde::{Deserialize, Serialize};
+
+use proxmox_acme_rs::account::AccountData as AcmeAccountData;
+use proxmox_acme_rs::{Account, Client};
+
+/// Our on-disk format inherited from PVE's proxmox-acme code.
+#[derive(Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct AccountData {
+    /// The account's location URL.
+    location: String,
+
+    /// The account dat.
+    account: AcmeAccountData,
+
+    /// The private key as PEM formatted string.
+    key: String,
+
+    /// ToS URL the user agreed to.
+    #[serde(skip_serializing_if = "Option::is_none")]
+    tos: Option<String>,
+
+    #[serde(skip_serializing_if = "is_false", default)]
+    debug: bool,
+
+    /// The directory's URL.
+    directory_url: String,
+}
+
+#[inline]
+fn is_false(b: &bool) -> bool {
+    !*b
+}
+
+struct Inner {
+    client: Client,
+    account_path: Option<String>,
+    tos: Option<String>,
+    debug: bool,
+}
+
+impl Inner {
+    pub fn new(api_directory: String) -> Result<Self, Error> {
+        Ok(Self {
+            client: Client::new(api_directory),
+            account_path: None,
+            tos: None,
+            debug: false,
+        })
+    }
+
+    pub fn load(account_path: String) -> Result<Self, Error> {
+        let data = std::fs::read(&account_path)?;
+        let data: AccountData = serde_json::from_slice(&data)?;
+
+        let mut client = Client::new(data.directory_url);
+        client.set_account(Account::from_parts(data.location, data.key, data.account));
+
+        Ok(Self {
+            client,
+            account_path: Some(account_path),
+            tos: data.tos,
+            debug: data.debug,
+        })
+    }
+
+    pub fn new_account(
+        &mut self,
+        account_path: String,
+        tos_agreed: bool,
+        contact: Vec<String>,
+        rsa_bits: Option<u32>,
+    ) -> Result<(), Error> {
+        self.tos = if tos_agreed {
+            self.client.terms_of_service_url()?.map(str::to_owned)
+        } else {
+            None
+        };
+
+        let _account = self.client.new_account(contact, tos_agreed, rsa_bits)?;
+        let file = OpenOptions::new()
+            .write(true)
+            .create(true)
+            .mode(0o600)
+            .open(&account_path)
+            .map_err(|err| format_err!("failed to open {:?} for writing: {}", account_path, err))?;
+        self.write_to(file).map_err(|err| {
+            format_err!(
+                "failed to write acme account to {:?}: {}",
+                account_path,
+                err
+            )
+        })?;
+        self.account_path = Some(account_path);
+
+        Ok(())
+    }
+
+    /// Convenience helper around `.client.account().ok_or_else(||...)`
+    fn account(&self) -> Result<&Account, Error> {
+        self.client
+            .account()
+            .ok_or_else(|| format_err!("missing account"))
+    }
+
+    fn to_account_data(&self) -> Result<AccountData, Error> {
+        let account = self.account()?;
+
+        Ok(AccountData {
+            location: account.location.clone(),
+            key: account.private_key.clone(),
+            account: AcmeAccountData {
+                only_return_existing: false, // don't actually write this out in case it's set
+                ..account.data.clone()
+            },
+            tos: self.tos.clone(),
+            debug: self.debug,
+            directory_url: self.client.directory_url().to_owned(),
+        })
+    }
+
+    fn write_to<T: io::Write>(&mut self, out: T) -> Result<(), Error> {
+        let data = self.to_account_data()?;
+
+        Ok(serde_json::to_writer_pretty(out, &data)?)
+    }
+
+    pub fn update_account<T: Serialize>(&mut self, data: &T) -> Result<(), Error> {
+        let account_path = self
+            .account_path
+            .as_deref()
+            .ok_or_else(|| format_err!("missing account path"))?;
+        self.client.update_account(data)?;
+
+        let tmp_path = format!("{}.tmp", account_path);
+        // FIXME: move proxmox::tools::replace_file & make_temp out into a nice *little* crate...
+        let mut file = OpenOptions::new()
+            .write(true)
+            .create(true)
+            .mode(0o600)
+            .open(&tmp_path)
+            .map_err(|err| format_err!("failed to open {:?} for writing: {}", tmp_path, err))?;
+        self.write_to(&mut file).map_err(|err| {
+            format_err!("failed to write acme account to {:?}: {}", tmp_path, err)
+        })?;
+        file.flush().map_err(|err| {
+            format_err!("failed to flush acme account file {:?}: {}", tmp_path, err)
+        })?;
+
+        // re-borrow since we needed `self` as mut earlier
+        let account_path = self.account_path.as_deref().unwrap();
+        std::fs::rename(&tmp_path, account_path).map_err(|err| {
+            format_err!(
+                "failed to rotate temp file into place ({:?} -> {:?}): {}",
+                &tmp_path,
+                account_path,
+                err
+            )
+        })?;
+        drop(file);
+        Ok(())
+    }
+
+    pub fn revoke_certificate(&mut self, data: &[u8], reason: Option<u32>) -> Result<(), Error> {
+        Ok(self.client.revoke_certificate(data, reason)?)
+    }
+
+    pub fn set_proxy(&mut self, proxy: String) {
+        self.client.set_proxy(proxy)
+    }
+}
+
+#[perlmod::package(name = "PMG::RS::Acme", lib = "pmg_rs")]
+pub mod export {
+    use std::collections::HashMap;
+    use std::convert::TryFrom;
+    use std::sync::Mutex;
+
+    use anyhow::Error;
+    use serde_bytes::{ByteBuf, Bytes};
+
+    use perlmod::Value;
+    use proxmox_acme_rs::directory::Meta;
+    use proxmox_acme_rs::order::OrderData;
+    use proxmox_acme_rs::{Authorization, Challenge, Order};
+
+    use super::{AccountData, Inner};
+
+    const CLASSNAME: &str = "PMG::RS::Acme";
+
+    /// An Acme client instance.
+    pub struct Acme {
+        inner: Mutex<Inner>,
+    }
+
+    impl<'a> TryFrom<&'a Value> for &'a Acme {
+        type Error = Error;
+
+        fn try_from(value: &'a Value) -> Result<&'a Acme, Error> {
+            Ok(unsafe { value.from_blessed_box(CLASSNAME)? })
+        }
+    }
+
+    fn bless(class: Value, mut ptr: Box<Acme>) -> Result<Value, Error> {
+        let value = Value::new_pointer::<Acme>(&mut *ptr);
+        let value = Value::new_ref(&value);
+        let this = value.bless_sv(&class)?;
+        let _perl = Box::leak(ptr);
+        Ok(this)
+    }
+
+    /// Create a new ACME client instance given an account path and an API directory URL.
+    #[export(raw_return)]
+    pub fn new(#[raw] class: Value, api_directory: String) -> Result<Value, Error> {
+        bless(
+            class,
+            Box::new(Acme {
+                inner: Mutex::new(Inner::new(api_directory)?),
+            }),
+        )
+    }
+
+    /// Load an existing account.
+    #[export(raw_return)]
+    pub fn load(#[raw] class: Value, account_path: String) -> Result<Value, Error> {
+        bless(
+            class,
+            Box::new(Acme {
+                inner: Mutex::new(Inner::load(account_path)?),
+            }),
+        )
+    }
+
+    #[export(name = "DESTROY")]
+    fn destroy(#[raw] this: Value) {
+        perlmod::destructor!(this, Acme: CLASSNAME);
+    }
+
+    /// Create a new account.
+    ///
+    /// `tos_agreed` is usually not optional, but may be set later via an update.
+    /// The `contact` list should be a list of `mailto:` strings (or others, if the directory
+    /// allows the).
+    ///
+    /// In case an RSA key should be generated, an `rsa_bits` parameter should be provided.
+    /// Otherwise a P-256 EC key will be generated.
+    #[export]
+    pub fn new_account(
+        #[try_from_ref] this: &Acme,
+        account_path: String,
+        tos_agreed: bool,
+        contact: Vec<String>,
+        rsa_bits: Option<u32>,
+    ) -> Result<(), Error> {
+        this.inner
+            .lock()
+            .unwrap()
+            .new_account(account_path, tos_agreed, contact, rsa_bits)
+    }
+
+    /// Get the directory's meta information.
+    #[export]
+    pub fn get_meta(#[try_from_ref] this: &Acme) -> Result<Option<Meta>, Error> {
+        match this.inner.lock().unwrap().client.directory()?.meta() {
+            Some(meta) => Ok(Some(meta.clone())),
+            None => Ok(None),
+        }
+    }
+
+    /// Get the account's directory URL.
+    #[export]
+    pub fn directory(#[try_from_ref] this: &Acme) -> Result<String, Error> {
+        Ok(this.inner.lock().unwrap().client.directory()?.url.clone())
+    }
+
+    /// Serialize the account data.
+    #[export]
+    pub fn account(#[try_from_ref] this: &Acme) -> Result<AccountData, Error> {
+        this.inner.lock().unwrap().to_account_data()
+    }
+
+    /// Get the account's location URL.
+    #[export]
+    pub fn location(#[try_from_ref] this: &Acme) -> Result<String, Error> {
+        Ok(this.inner.lock().unwrap().account()?.location.clone())
+    }
+
+    /// Get the account's agreed-to ToS URL.
+    #[export]
+    pub fn tos_url(#[try_from_ref] this: &Acme) -> Option<String> {
+        this.inner.lock().unwrap().tos.clone()
+    }
+
+    /// Get the debug flag.
+    #[export]
+    pub fn debug(#[try_from_ref] this: &Acme) -> bool {
+        this.inner.lock().unwrap().debug
+    }
+
+    /// Get the debug flag.
+    #[export]
+    pub fn set_debug(#[try_from_ref] this: &Acme, on: bool) {
+        this.inner.lock().unwrap().debug = on;
+    }
+
+    /// Place a new order.
+    #[export]
+    pub fn new_order(
+        #[try_from_ref] this: &Acme,
+        domains: Vec<String>,
+    ) -> Result<(String, OrderData), Error> {
+        let order: Order = this.inner.lock().unwrap().client.new_order(domains)?;
+        Ok((order.location, order.data))
+    }
+
+    /// Get the authorization info given an authorization URL.
+    ///
+    /// This should be an URL found in the `authorizations` array in the `OrderData` returned from
+    /// `new_order`.
+    #[export]
+    pub fn get_authorization(
+        #[try_from_ref] this: &Acme,
+        url: &str,
+    ) -> Result<Authorization, Error> {
+        Ok(this.inner.lock().unwrap().client.get_authorization(url)?)
+    }
+
+    /// Query an order given its URL.
+    ///
+    /// The corresponding URL is returned as first value from the `new_order` call.
+    #[export]
+    pub fn get_order(#[try_from_ref] this: &Acme, url: &str) -> Result<OrderData, Error> {
+        Ok(this.inner.lock().unwrap().client.get_order(url)?)
+    }
+
+    /// Get the key authorization string for a challenge given a token.
+    #[export]
+    pub fn key_authorization(#[try_from_ref] this: &Acme, token: &str) -> Result<String, Error> {
+        Ok(this.inner.lock().unwrap().client.key_authorization(token)?)
+    }
+
+    /// Get the key dns-01 TXT challenge value for a token.
+    #[export]
+    pub fn dns_01_txt_value(#[try_from_ref] this: &Acme, token: &str) -> Result<String, Error> {
+        Ok(this.inner.lock().unwrap().client.dns_01_txt_value(token)?)
+    }
+
+    /// Request validation of a challenge by URL.
+    ///
+    /// Given an `Authorization`, it'll contain `challenges`. These contain `url`s pointing to a
+    /// method used to request challenge authorization. This is the URL used for this method,
+    /// *after* performing the necessary steps to satisfy the challenge. (Eg. after setting up a
+    /// DNS TXT entry using the `dns-01` type challenge's key authorization.
+    #[export]
+    pub fn request_challenge_validation(
+        #[try_from_ref] this: &Acme,
+        url: &str,
+    ) -> Result<Challenge, Error> {
+        Ok(this
+            .inner
+            .lock()
+            .unwrap()
+            .client
+            .request_challenge_validation(url)?)
+    }
+
+    /// Request finalization of an order.
+    ///
+    /// The `url` should be the 'finalize' URL of the order.
+    #[export]
+    pub fn finalize_order(
+        #[try_from_ref] this: &Acme,
+        url: &str,
+        csr: &Bytes,
+    ) -> Result<(), Error> {
+        Ok(this.inner.lock().unwrap().client.finalize(url, csr)?)
+    }
+
+    /// Download the certificate for an order.
+    ///
+    /// The `url` should be the 'certificate' URL of the order.
+    #[export]
+    pub fn get_certificate(#[try_from_ref] this: &Acme, url: &str) -> Result<ByteBuf, Error> {
+        Ok(ByteBuf::from(
+            this.inner.lock().unwrap().client.get_certificate(url)?,
+        ))
+    }
+
+    /// Update account data.
+    ///
+    /// This can be used for example to deactivate an account or agree to ToS later on.
+    #[export]
+    pub fn update_account(
+        #[try_from_ref] this: &Acme,
+        data: HashMap<String, serde_json::Value>,
+    ) -> Result<(), Error> {
+        this.inner.lock().unwrap().update_account(&data)?;
+        Ok(())
+    }
+
+    /// Revoke an existing certificate using the certificate in PEM or DER form.
+    #[export]
+    pub fn revoke_certificate(
+        #[try_from_ref] this: &Acme,
+        data: &[u8],
+        reason: Option<u32>,
+    ) -> Result<(), Error> {
+        this.inner
+            .lock()
+            .unwrap()
+            .revoke_certificate(&data, reason)?;
+        Ok(())
+    }
+
+    /// Set a proxy
+    #[export]
+    pub fn set_proxy(#[try_from_ref] this: &Acme, proxy: String) {
+        this.inner.lock().unwrap().set_proxy(proxy)
+    }
+
+}
diff --git a/pmg-rs/src/apt/mod.rs b/pmg-rs/src/apt/mod.rs
new file mode 100644
index 0000000..574c1a7
--- /dev/null
+++ b/pmg-rs/src/apt/mod.rs
@@ -0,0 +1 @@
+mod repositories;
diff --git a/pmg-rs/src/apt/repositories.rs b/pmg-rs/src/apt/repositories.rs
new file mode 100644
index 0000000..75207c7
--- /dev/null
+++ b/pmg-rs/src/apt/repositories.rs
@@ -0,0 +1,162 @@
+#[perlmod::package(name = "PMG::RS::APT::Repositories", lib = "pmg_rs")]
+mod export {
+    use std::convert::TryInto;
+
+    use anyhow::{bail, Error};
+    use serde::{Deserialize, Serialize};
+
+    use proxmox_apt::repositories::{
+        APTRepositoryFile, APTRepositoryFileError, APTRepositoryHandle, APTRepositoryInfo,
+        APTStandardRepository,
+    };
+
+    #[derive(Deserialize, Serialize)]
+    #[serde(rename_all = "kebab-case")]
+    /// Result for the repositories() function
+    pub struct RepositoriesResult {
+        /// Successfully parsed files.
+        pub files: Vec<APTRepositoryFile>,
+
+        /// Errors for files that could not be parsed or read.
+        pub errors: Vec<APTRepositoryFileError>,
+
+        /// Common digest for successfully parsed files.
+        pub digest: String,
+
+        /// Additional information/warnings about repositories.
+        pub infos: Vec<APTRepositoryInfo>,
+
+        /// Standard repositories and their configuration status.
+        pub standard_repos: Vec<APTStandardRepository>,
+    }
+
+    #[derive(Deserialize, Serialize)]
+    #[serde(rename_all = "kebab-case")]
+    /// For changing an existing repository.
+    pub struct ChangeProperties {
+        /// Whether the repository should be enabled or not.
+        pub enabled: Option<bool>,
+    }
+
+    /// Get information about configured and standard repositories.
+    #[export]
+    pub fn repositories() -> Result<RepositoriesResult, Error> {
+        let (files, errors, digest) = proxmox_apt::repositories::repositories()?;
+        let digest = hex::encode(&digest);
+
+        let suite = proxmox_apt::repositories::get_current_release_codename()?;
+
+        let infos = proxmox_apt::repositories::check_repositories(&files, suite);
+        let standard_repos = proxmox_apt::repositories::standard_repositories(&files, "pmg", suite);
+
+        Ok(RepositoriesResult {
+            files,
+            errors,
+            digest,
+            infos,
+            standard_repos,
+        })
+    }
+
+    /// Add the repository identified by the `handle`.
+    /// If the repository is already configured, it will be set to enabled.
+    ///
+    /// The `digest` parameter asserts that the configuration has not been modified.
+    #[export]
+    pub fn add_repository(handle: &str, digest: Option<&str>) -> Result<(), Error> {
+        let (mut files, errors, current_digest) = proxmox_apt::repositories::repositories()?;
+
+        let handle: APTRepositoryHandle = handle.try_into()?;
+        let suite = proxmox_apt::repositories::get_current_release_codename()?;
+
+        if let Some(digest) = digest {
+            let expected_digest = hex::decode(digest)?;
+            if expected_digest != current_digest {
+                bail!("detected modified configuration - file changed by other user? Try again.");
+            }
+        }
+
+        // check if it's already configured first
+        for file in files.iter_mut() {
+            for repo in file.repositories.iter_mut() {
+                if repo.is_referenced_repository(handle, "pmg", &suite.to_string()) {
+                    if repo.enabled {
+                        return Ok(());
+                    }
+
+                    repo.set_enabled(true);
+                    file.write()?;
+
+                    return Ok(());
+                }
+            }
+        }
+
+        let (repo, path) = proxmox_apt::repositories::get_standard_repository(handle, "pmg", suite);
+
+        if let Some(error) = errors.iter().find(|error| error.path == path) {
+            bail!(
+                "unable to parse existing file {} - {}",
+                error.path,
+                error.error,
+            );
+        }
+
+        if let Some(file) = files.iter_mut().find(|file| file.path == path) {
+            file.repositories.push(repo);
+
+            file.write()?;
+        } else {
+            let mut file = match APTRepositoryFile::new(&path)? {
+                Some(file) => file,
+                None => bail!("invalid path - {}", path),
+            };
+
+            file.repositories.push(repo);
+
+            file.write()?;
+        }
+
+        Ok(())
+    }
+
+    /// Change the properties of the specified repository.
+    ///
+    /// The `digest` parameter asserts that the configuration has not been modified.
+    #[export]
+    pub fn change_repository(
+        path: &str,
+        index: usize,
+        options: ChangeProperties,
+        digest: Option<&str>,
+    ) -> Result<(), Error> {
+        let (mut files, errors, current_digest) = proxmox_apt::repositories::repositories()?;
+
+        if let Some(digest) = digest {
+            let expected_digest = hex::decode(digest)?;
+            if expected_digest != current_digest {
+                bail!("detected modified configuration - file changed by other user? Try again.");
+            }
+        }
+
+        if let Some(error) = errors.iter().find(|error| error.path == path) {
+            bail!("unable to parse file {} - {}", error.path, error.error);
+        }
+
+        if let Some(file) = files.iter_mut().find(|file| file.path == path) {
+            if let Some(repo) = file.repositories.get_mut(index) {
+                if let Some(enabled) = options.enabled {
+                    repo.set_enabled(enabled);
+                }
+
+                file.write()?;
+            } else {
+                bail!("invalid index - {}", index);
+            }
+        } else {
+            bail!("invalid path - {}", path);
+        }
+
+        Ok(())
+    }
+}
diff --git a/pmg-rs/src/csr.rs b/pmg-rs/src/csr.rs
new file mode 100644
index 0000000..961a2cf
--- /dev/null
+++ b/pmg-rs/src/csr.rs
@@ -0,0 +1,24 @@
+#[perlmod::package(name = "PMG::RS::CSR", lib = "pmg_rs")]
+pub mod export {
+    use std::collections::HashMap;
+
+    use anyhow::Error;
+    use serde_bytes::ByteBuf;
+
+    use proxmox_acme_rs::util::Csr;
+
+    /// Generates a CSR and its accompanying private key.
+    ///
+    /// The CSR is DER formatted, the private key is a PEM formatted pkcs8 private key.
+    #[export]
+    pub fn generate_csr(
+        identifiers: Vec<&str>,
+        attributes: HashMap<String, &str>,
+    ) -> Result<(ByteBuf, ByteBuf), Error> {
+        let csr = Csr::generate(&identifiers, &attributes)?;
+        Ok((
+            ByteBuf::from(csr.data),
+            ByteBuf::from(csr.private_key_pem),
+        ))
+    }
+}
diff --git a/pmg-rs/src/lib.rs b/pmg-rs/src/lib.rs
new file mode 100644
index 0000000..47e61b5
--- /dev/null
+++ b/pmg-rs/src/lib.rs
@@ -0,0 +1,3 @@
+pub mod acme;
+pub mod apt;
+pub mod csr;
diff --git a/pmg-rs/test.pl b/pmg-rs/test.pl
new file mode 100644
index 0000000..3d2a6df
--- /dev/null
+++ b/pmg-rs/test.pl
@@ -0,0 +1,172 @@
+#!/usr/bin/env perl
+
+use v5.28.0;
+use Data::Dumper;
+
+use lib '.';
+use PMG::RS::Acme;
+use PMG::RS::CSR;
+
+# "Config:" The Acme server URL:
+my $DIR = 'https://acme-staging-v02.api.letsencrypt.org/directory';
+
+# Useage:
+#
+# * Create a new account:
+#     | ~/ $ ./test.pl ./account.json new 'somebody@example.invalid"
+#
+#   The `./account.json` will be created using an EC P-256 key.
+#   Optionally an RSA key size can be passed as additional parameter to generate
+#   an account with an RSA key instead.
+#
+# From here on out the `./account.json` file must already exist:
+#
+# * Place a new order:
+#     | ~/ $ ./test.pl ./account.json new-order my.domain.com
+#     | $VAR1 = {
+#     |     ... order data ...
+#     |     'authorizations' => [
+#     |         'https://acme.example/auths/1244',
+#     |         ... possibly more ...
+#     |     ]
+#     | }
+#     | Order URL: https://acme.example/order/1793
+#
+#   Note: This   ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
+#     URL will be used later for finalization and certifiate download.
+#   The `$VAR1` dump contains the order JSON data.
+#   The 'authorizations' URLs are going to be used next.
+#
+# * Get authorization info
+#     | ~/ $ ./test.pl ./account.json get-auth 'https://acme.example/auths/1244'
+#     | $VAR1 = {
+#     |     ... auth data ...
+#     |     'challenges' => [
+#     |         {
+#     |             'type' => 'dns-01',
+#     |             'url' => 'https://acme.example/challenge/8188/dns1'
+#     |         }
+#     |         ... likely more ...
+#     |     ]
+#     | }
+#     | Key Authorization = SuperVeryMegaLongValue
+#     | dns-01 TXT value = ShorterValue
+#
+#   Now perform the things you need to for the challenge, eg. setup the DNS
+#   entry using the provided TXT value.
+#   Then use the correct challenge's URL with req-auth
+#
+# * Request challenge validation
+#     | ~/ $ ./test.pl ./account.json \
+#     |      req-challenge 'https://acme.example/challenge/8188/dns1
+#
+# * Repeat the above 2 steps for all authorizations.
+# * Wait for the order to be valid via `get-order`
+#     | ~/ $ ./test.pl ./account.json get-order 'https://acme.example/order/1793'
+#     | $VAR1 = {
+#     |     'status' => 'valid',
+#     |     'finalize' => 'some URL',
+#     |     ... order data ...
+#     | }
+#     | Order URL: https://acme.example/order/1793
+#
+# * Finalize the order via the *Order URL* and a private key to sign the
+#   request with (eg. generated via `openssl genrsa` or `openssl ecparam`).
+#     | ~/ $ ./test.pl ./account.json \
+#     |      finalize my.domain.com ./my-private-key.pem \
+#     |      'https://acme.example/order/1793'
+#
+# * Wait for a 'certificate' property to pop up in the order
+#   (check via 'get-order')
+#
+# * Grab the certificate with the Order URL and a destination file name:
+#     | ~/ $ ./test.pl ./account.json get-cert \
+#     |      'https://acme.example/order/1793' \
+#     |      ./my-cert.pem
+
+
+my $account = shift // die "missing account file\n";
+my $cmd = shift // die "missing account file\n";
+
+sub load : prototype($) {
+    my ($file) = @_;
+    open(my $fh, '<', $file) or die "open($file): $!\n";
+    my $data = do {
+        local $/ = undef;
+        <$fh>
+    };
+    close($fh);
+    return $data;
+}
+
+sub store : prototype($$) {
+    my ($file, $data) = @_;
+    open(my $fh, '>', $file) or die "open($file): $!\n";
+    syswrite($fh, $data) == length($data)
+        or die "failed to write data to $file: $!\n";
+    close($fh);
+}
+
+if ($cmd eq 'new') {
+    my $mail = shift // die "missing mail address\n";
+    my $rsa_bits = shift;
+    if (defined($rsa_bits)) {
+        $rsa_bits = int($rsa_bits);
+    }
+    my $acme = PMG::RS::Acme->new($DIR);
+    $acme->new_account($account, 1, ["mailto:$mail"], undef);
+} elsif ($cmd eq 'get-meta') {
+    #my $acme = PMG::RS::Acme->new($DIR);
+    my $acme = PMG::RS::Acme->new('https%3A%2F%2Facme-v02.api.letsencrypt.org%2Fdirectory');
+    my $data = $acme->get_meta();
+    say Dumper($data);
+} elsif ($cmd eq 'new-order') {
+    my $domain = shift // die "missing domain\n";
+    my $acme = PMG::RS::Acme->load($account);
+    my ($url, $order) = $acme->new_order([$domain]);
+    say Dumper($order);
+    say "Order URL: $url\n";
+} elsif ($cmd eq 'get-auth') {
+    my $url = shift // die "missing url\n";
+    my $acme = PMG::RS::Acme->load($account);
+    my $auth = $acme->get_authorization($url);
+    say Dumper($auth);
+    for my $challenge ($auth->{challenges}->@*) {
+        next if $challenge->{type} ne 'dns-01';
+        say "Key Authorization = ".$acme->key_authorization($challenge->{token});
+        say "dns-01 TXT value = ".$acme->dns_01_txt_value($challenge->{token});
+    }
+} elsif ($cmd eq 'req-challenge') {
+    my $url = shift // die "missing url\n";
+    my $acme = PMG::RS::Acme->load($account);
+    my $challenge = $acme->request_challenge_validation($url);
+    say Dumper($challenge);
+} elsif ($cmd eq 'finalize') {
+    my $domain = shift // die 'missing domain\n';
+    my $pkfile = shift // die "missing private key file\n";
+    my $order_url = shift // die "missing order URL\n";
+    my ($csr_der, $pkey_pem) = PMG::RS::CSR::generate_csr([$domain], {});
+    store($pkfile, $pkey_pem);
+    my $acme = PMG::RS::Acme->load($account);
+    my $order = $acme->get_order($order_url);
+    say Dumper($order);
+    die "order not ready\n" if $order->{status} ne 'ready';
+    $acme->finalize_order($order->{finalize}, $csr_der);
+} elsif ($cmd eq 'get-order') {
+    my $order_url = shift // die "missing order URL\n";
+    my $acme = PMG::RS::Acme->load($account);
+    my $order = $acme->get_order($order_url);
+    say Dumper($order);
+} elsif ($cmd eq 'get-cert') {
+    my $order_url = shift // die "missing order URL\n";
+    my $file_name = shift // die "missing destination file name\n";
+    my $acme = PMG::RS::Acme->load($account);
+    my $order = $acme->get_order($order_url);
+    my $cert_url = $order->{certificate};
+    die "certificate not ready\n" if !$cert_url;
+    say Dumper($order);
+    my $cert = $acme->get_certificate($cert_url);
+    store($file_name, $cert);
+} else {
+    die "unknown command '$cmd'\n";
+}
-- 
2.30.2





  parent reply	other threads:[~2021-11-26 13:56 UTC|newest]

Thread overview: 24+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2021-11-26 13:55 [pmg-devel] [PATCH multiple 0/7] PMG TFA support Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH api 1/6] add tfa.json and its lock methods Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH api 2/6] add PMG::TFAConfig module Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH api 3/6] add TFA API Wolfgang Bumiller
2021-11-26 17:29   ` Stoiko Ivanov
2021-11-26 13:55 ` [pmg-devel] [PATCH api 4/6] add tfa config api Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH api 5/6] implement tfa authentication Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH api 6/6] provide qrcode.min.js from libjs-qrcodejs Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH gui] add TFA components Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH perl-rs 1/7] pve: bump perlmod to 0.9 Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH perl-rs 2/7] pve: update to proxmox-tfa 2.0 Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH perl-rs 3/7] pve: bump d/control Wolfgang Bumiller
2021-11-26 13:55 ` Wolfgang Bumiller [this message]
2021-11-26 13:55 ` [pmg-devel] [PATCH perl-rs 5/7] pmg: bump perlmod to 0.9 Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH perl-rs 6/7] pmg: add tfa module Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH perl-rs 7/7] pmg: bump d/control Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH proxmox 1/6] tfa: fix typo in docs Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH proxmox 2/6] tfa: add WebauthnConfig::digest method Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH proxmox 3/6] tfa: let OriginUrl deref to its inner Url, add FromStr impl Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH proxmox 4/6] tfa: make configured webauthn origin optional Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH proxmox 5/6] tfa: clippy fixes Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH proxmox 6/6] bump proxmox-tfa to 2.0.0-1 Wolfgang Bumiller
2021-11-26 17:34 ` [pmg-devel] [PATCH multiple 0/7] PMG TFA support Stoiko Ivanov
2021-11-28 21:17 ` [pmg-devel] applied-series: " Thomas Lamprecht

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20211126135524.117846-12-w.bumiller@proxmox.com \
    --to=w.bumiller@proxmox.com \
    --cc=pmg-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal