import pmg-rs

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
Wolfgang Bumiller 2021-11-26 14:55:15 +01:00 committed by Thomas Lamprecht
parent 73a88784f1
commit b6274a49a5
18 changed files with 1018 additions and 3 deletions

View File

@ -2,6 +2,7 @@
exclude = [ "build", "perl-*" ]
members = [
"pve-rs",
"pmg-rs",
]
[patch.crates-io]

View File

@ -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; \

33
pmg-rs/Cargo.toml Normal file
View File

@ -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"

75
pmg-rs/Makefile Normal file
View File

@ -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

50
pmg-rs/debian/changelog Normal file
View File

@ -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

1
pmg-rs/debian/compat Normal file
View File

@ -0,0 +1 @@
12

27
pmg-rs/debian/control Normal file
View File

@ -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.

16
pmg-rs/debian/copyright Normal file
View File

@ -0,0 +1,16 @@
Copyright (C) 2020-2021 Proxmox Server Solutions GmbH
This software is written by Proxmox Server Solutions GmbH <support@proxmox.com>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.

View File

@ -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]

7
pmg-rs/debian/rules Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/make -f
#export DH_VERBOSE=1
export BUILD_MODE=release
%:
dh $@

View File

@ -0,0 +1 @@
3.0 (native)

1
pmg-rs/debian/triggers Normal file
View File

@ -0,0 +1 @@
activate-noawait pve-api-updates

430
pmg-rs/src/acme.rs Normal file
View File

@ -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)
}
}

1
pmg-rs/src/apt/mod.rs Normal file
View File

@ -0,0 +1 @@
mod repositories;

View File

@ -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(())
}
}

24
pmg-rs/src/csr.rs Normal file
View File

@ -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),
))
}
}

3
pmg-rs/src/lib.rs Normal file
View File

@ -0,0 +1,3 @@
pub mod acme;
pub mod apt;
pub mod csr;

172
pmg-rs/test.pl Normal file
View File

@ -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";
}