Commits vergleichen

...

7 Commits

20 geänderte Dateien mit 508 neuen und 319 gelöschten Zeilen

Datei anzeigen

@ -55,8 +55,12 @@ repos:
- id: tomlq
name: toml Format
description: Formats Toml files
entry: echo tomlq . -ti
entry: tomlq -S -ti .
language: system
types:
types_or:
- toml
- cargo
exclude: '\.lock$'
stages:
- manual
- pre-commit

11
Cargo.lock generiert
Datei anzeigen

@ -1543,6 +1543,7 @@ dependencies = [
"schemars",
"serde",
"serde_json",
"systemd-journal-logger",
"tokio",
"tokio-stream",
"toml",
@ -2016,6 +2017,16 @@ dependencies = [
"syn",
]
[[package]]
name = "systemd-journal-logger"
version = "2.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7266304d24ca5a4b230545fc558c80e18bd3e1d2eb1be149b6bcd04398d3e79c"
dependencies = [
"log",
"rustix",
]
[[package]]
name = "tempfile"
version = "3.20.0"

Datei anzeigen

@ -1,97 +1,102 @@
[package]
name = "racme"
version = "0.1.0"
edition = "2024"
resolver = "3"
[features]
capabilities = ["dep:caps"]
unstable = ["capabilities"]
[dependencies]
derive-new = "0.7.0"
env_logger = "0.11"
lazy_static = "1.5"
toml = "0.8"
[dependencies.schemars]
version = "0.9.0"
[dependencies.acme2-eab]
default-features = false
features = ["derive", "std", "preserve_order"]
[dependencies.serde_json]
version = "1.0.140"
default-features = false
features = ["std"]
version = "0"
[dependencies.caps]
version = "0.5.5"
default-features = false
optional = true
[dependencies.libc]
version = "0.2"
default-features = false
features = ["const-extern-fn", "std"]
[dependencies.data-encoding]
version = "2.9"
default-features = false
features = ["alloc", "std"]
[dependencies.macro_rules_attribute]
version = "0.2"
default-features = false
[dependencies.acme2-eab]
version = "0"
default-features = false
[dependencies.openssl]
version = "0.10"
default-features = false
[dependencies.pem]
version = "3.0"
default-features = false
features = ["serde", "std"]
[dependencies.tokio-stream]
version = "0.1"
default-features = false
features = ["fs"]
[dependencies.libsystemd]
version = "0.7"
default-features = false
[dependencies.zbus_systemd]
version = "0.25701"
default-features = false
features = ["systemd1"]
[dependencies.log]
version = "0.4"
features = ["std"]
[dependencies.serde]
version = "1.0"
features = ["derive", "std"]
default-features = false
version = "0.5.5"
[dependencies.clap]
version = "4.5"
features = ["derive"]
version = "4.5"
[dependencies.tokio]
version = "1.45"
[dependencies.data-encoding]
default-features = false
features = ["rt", "sync", "time", "net", "macros"]
features = ["alloc", "std"]
version = "2.9"
[dependencies.libc]
default-features = false
features = ["const-extern-fn", "std"]
version = "0.2"
[dependencies.libsystemd]
default-features = false
version = "0.7"
[dependencies.log]
features = ["std"]
version = "0.4"
[dependencies.macro_rules_attribute]
default-features = false
version = "0.2"
[dependencies.openssl]
default-features = false
version = "0.10"
[dependencies.pem]
default-features = false
features = ["serde", "std"]
version = "3.0"
[dependencies.reqwest]
version = "0.12"
default-features = false
features = ["rustls-tls-native-roots-no-provider", "default-tls"]
version = "0.12"
[dependencies.schemars]
default-features = false
features = ["derive", "std", "preserve_order"]
version = "0.9.0"
[dependencies.serde]
default-features = false
features = ["derive", "std"]
version = "1.0"
[dependencies.serde_json]
default-features = false
features = ["std"]
version = "1.0.140"
[dependencies.systemd-journal-logger]
default-features = false
version = "2.2.2"
[dependencies.tokio]
default-features = false
features = ["rt", "sync", "time", "net", "macros"]
version = "1.45"
[dependencies.tokio-stream]
default-features = false
features = ["fs"]
version = "0.1"
[dependencies.zbus_systemd]
default-features = false
features = ["systemd1"]
version = "0.25701"
[features]
capabilities = ["dep:caps"]
unstable = ["capabilities"]
[package]
edition = "2024"
name = "racme"
resolver = "3"
rust-version = "1.87"
version = "0.1.0"
[patch.crates-io.acme2-eab]
path = "../acme2-eab"

Datei anzeigen

@ -0,0 +1,8 @@
# Rust + ACME = Racme
I wrote this to scratch an itch i have with existing ACME Clients:
- no ownerchange, the acme process also generates certificates for my mailserver or database
- no DNS-Server integration
- native logging
- can do this without root

Datei anzeigen

@ -1,6 +1,6 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "GeneralConfig",
"title": "General",
"type": "object",
"properties": {
"accounts_path": {

Datei anzeigen

@ -1,6 +1,6 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "SiteConfig",
"title": "Site",
"type": "object",
"properties": {
"ca": {

Datei anzeigen

@ -1,4 +1,4 @@
use macro_rules_attribute::derive_alias;
use macro_rules_attribute::attribute_alias;
#[allow(unused_macros)]
macro_rules! match_error {
@ -24,9 +24,10 @@ macro_rules! attr_function {
}
}
derive_alias! {
#[derive(DefDer!)] = #[derive(Debug, Clone)];
#[derive(Hashable!)] = #[derive(Eq, Hash)];
attribute_alias! {
#[apply(ConfigFile!)] = #[derive(::serde::Deserialize, ::schemars::JsonSchema)];
#[apply(Hashable!)] = #[derive(Eq,Hash)];
#[apply(DefDer!)] = #[derive(Debug, Clone)];
}
#[allow(unused_imports)]

Datei anzeigen

@ -1,9 +1,15 @@
//! Acme client that supports multiple CAs and configs for sites that can be seperate from the mainconfig
#![allow(clippy::clone_on_copy)]
#![allow(clippy::identity_op)]
#![allow(refining_impl_trait)]
#![allow(clippy::collapsible_if)]
#![allow(clippy::identity_op)]
#![allow(dead_code)]
#![allow(refining_impl_trait)]
#![deny(clippy::format_push_string)]
#![deny(clippy::macro_use_imports)]
#![deny(clippy::module_name_repetitions)]
#![deny(clippy::single_component_path_imports)]
#![deny(clippy::unnecessary_debug_formatting)]
#![deny(clippy::unnecessary_self_imports)]
pub(crate) mod consts;
pub(crate) mod macros;
@ -13,12 +19,11 @@ pub(crate) mod types;
pub(crate) mod utils;
use crate::{
consts::*,
prelude::*,
types::{
config::{
GeneralConfig,
SiteConfig,
General,
Site,
},
dns::Manager,
structs::{
@ -28,30 +33,31 @@ use crate::{
SubCommand,
},
},
utils::check_permissions,
utils::{
check_permissions,
logging,
},
};
use acme2_eab::Directory;
use clap::Parser;
use env_logger::init as log_init;
use libsystemd::daemon;
use log::*;
use openssl::{
self,
pkey::{
PKey,
Private,
},
use reqwest::{
Client,
tls::Version,
};
use reqwest::tls::Version;
use schemars::{
SchemaGenerator,
consts::meta_schemas::DRAFT07,
generate::SchemaSettings,
};
use serde::Serialize;
use serde_json::ser::{
Formatter,
PrettyFormatter,
use serde_json::{
Serializer,
ser::{
Formatter,
PrettyFormatter,
},
};
use std::{
collections::{
@ -72,10 +78,8 @@ use tokio::{
create_dir_all,
read_dir,
},
io::{
AsyncReadExt,
AsyncWriteExt,
},
io::AsyncWriteExt as _,
runtime::Builder,
sync::Mutex,
};
use tokio_stream::{
@ -84,33 +88,21 @@ use tokio_stream::{
};
fn default_client() -> Result<reqwest::Client, Error> {
reqwest::Client::builder()
fn default_client() -> Result<Client, Error> {
trace!("Initialized new reqwest Client");
Client::builder()
.min_tls_version(Version::TLS_1_2)
.https_only(true)
.pool_max_idle_per_host(POOL_SIZE)
.connection_verbose(true)
.referer(true)
.build()
.map_err(Error::from_display)
}
async fn load_privkey(path: PathBuf) -> Result<PKey<Private>, Error> {
let mut file = match FILE_MODE.open(path).await {
Ok(file) => file,
Err(error) => return Error::err(format!("Failed to open Private Key: {error}")),
};
let mut data = String::new();
if let Err(error) = file.read_to_string(&mut data).await {
return Error::err(format!("Failed to read data for the key: {error}"));
}
match PKey::private_key_from_pem(data.as_bytes()) {
Ok(key) => Ok(key),
Err(error) => Error::err(format!("Failed to parse pem data: {error}")),
}
}
async fn racme(flags: Arguments) -> Result<(), Error> {
let client = default_client()?;
let mut dns_manager = Manager::new();
let mut dns_manager = Manager::new(client.clone());
let systemd_access = daemon::booted();
let mainconfig = {
let file = match FILE_MODE.open(flags.config).await {
@ -119,7 +111,7 @@ async fn racme(flags: Arguments) -> Result<(), Error> {
return Error::err(format!("error reading the config: {error}"));
},
};
GeneralConfig::from_file(file).await
General::from_file(file).await
};
for (zone, builder) in mainconfig.dns.iter() {
dns_manager.add_builder(zone.clone(), builder.clone()).await;
@ -143,7 +135,7 @@ async fn racme(flags: Arguments) -> Result<(), Error> {
continue;
},
};
let mut site = SiteConfig::from_file(file).await;
let mut site = Site::from_file(file).await;
site.name = filename.file_stem().unwrap().to_string_lossy().to_string();
siteconfigs.push(site);
}
@ -198,7 +190,7 @@ async fn racme(flags: Arguments) -> Result<(), Error> {
fn serialize_with_formatter<T: Serialize, F: Formatter>(value: &T, formatter: F) -> Result<String, Error> {
let mut store = Vec::with_capacity(2 ^ 10);
let mut serializer = serde_json::ser::Serializer::with_formatter(&mut store, formatter);
let mut serializer = Serializer::with_formatter(&mut store, formatter);
match value.serialize(&mut serializer) {
Ok(_) => {},
Err(error) => return Error::err(format!("Failed to Serialize the schema: {error}")),
@ -212,7 +204,7 @@ async fn schema_generator() -> Result<(), Error> {
let mut schema_settings = SchemaSettings::default();
schema_settings.meta_schema = Some(DRAFT07.into());
let mut generator = SchemaGenerator::new(schema_settings);
let general_schema = serialize_with_formatter(&generator.root_schema_for::<GeneralConfig>(), formatter.clone())?;
let general_schema = serialize_with_formatter(&generator.root_schema_for::<General>(), formatter.clone())?;
match FILE_MODE_WRITE.clone().create_new(false).open("schema-general.json").await {
Ok(mut file) => {
match file.write(general_schema.as_bytes()).await {
@ -223,7 +215,7 @@ async fn schema_generator() -> Result<(), Error> {
Err(error) => return Err(Error::from_display(error)),
};
let site_schema = serialize_with_formatter(&generator.root_schema_for::<SiteConfig>(), formatter.clone())?;
let site_schema = serialize_with_formatter(&generator.root_schema_for::<Site>(), formatter.clone())?;
match FILE_MODE_WRITE.clone().create_new(false).open("schema-site.json").await {
Ok(mut file) => {
match file.write(site_schema.as_bytes()).await {
@ -237,7 +229,7 @@ async fn schema_generator() -> Result<(), Error> {
}
fn main() {
log_init();
logging();
let args = Arguments::parse();
if args.subcommands.is_none() && !check_permissions() {
error!(
@ -245,7 +237,7 @@ fn main() {
);
exit(4)
}
let runtime = match tokio::runtime::Builder::new_current_thread().enable_all().build() {
let runtime = match Builder::new_current_thread().enable_all().build() {
Ok(runtime) => runtime,
Err(error) => {
error!("Could not initialize Tokio runtime: {error}");

Datei anzeigen

@ -1,4 +1,5 @@
pub(crate) use crate::{
consts::*,
macros::*,
types::traits::{
FromFile as _,

Datei anzeigen

@ -5,7 +5,10 @@ use std::{
},
fs::Permissions,
os::{
fd::AsFd,
fd::{
AsFd as _,
AsRawFd as _,
},
unix::fs::{
PermissionsExt as _,
fchown,
@ -16,23 +19,17 @@ use std::{
};
use crate::{
consts::{
ATTEMPTS,
FILE_MODE,
FILE_MODE_OVERWRITE,
MODE_PRIVATE,
MODE_PUBLIC,
MODE_SECRETS,
WAIT_TIME,
},
load_privkey,
prelude::*,
types::{
self,
config::CA,
cryptography::Algorithm,
cryptography::{
Algorithm,
Strength,
},
dns::Manager,
structs::{
Certificate,
Error,
ProcessorArgs,
San,
@ -57,9 +54,14 @@ use acme2_eab::{
OrderBuilder,
OrderStatus,
};
use libc::fchmod;
use log::*;
use openssl::{
hash::MessageDigest,
pkey::{
PKey,
Private,
},
stack::Stack,
x509::{
X509,
@ -74,6 +76,7 @@ use openssl::{
},
},
};
use pem::parse as pem_parse;
use reqwest::Client;
use tokio::{
fs::{
@ -81,12 +84,15 @@ use tokio::{
remove_file,
},
io::{
AsyncReadExt,
AsyncWriteExt,
AsyncReadExt as _,
AsyncWriteExt as _,
},
join,
};
use zbus_systemd::systemd1;
use zbus_systemd::{
systemd1,
zbus::Connection,
};
fn gen_stack(args: &ProcessorArgs, context: X509v3Context) -> Stack<X509Extension> {
let mut stack = Stack::new().unwrap();
@ -109,7 +115,7 @@ fn gen_stack(args: &ProcessorArgs, context: X509v3Context) -> Stack<X509Extensio
}
pub async fn accounts(
name: &String,
name: &str,
ca: &CA,
directories: &mut HashMap<String, Arc<Directory>>,
accounts: &mut HashMap<String, Arc<Account>>,
@ -125,7 +131,12 @@ pub async fn accounts(
dir
},
Err(error) => {
error!("Failed to initialize directory for ca {name}: {error}");
match error {
acme2_eab::Error::Server(server_error) => error!("Failed to get the directory(Server Error): {server_error}"),
acme2_eab::Error::Transport(error) => error!("Failed to connect to the CA(Transport error): {error}"),
acme2_eab::Error::Other(error) => error!("unexpected error(other): {error}"),
x => error!("Unexpected Error: {x}"),
}
return;
},
}
@ -141,14 +152,14 @@ pub async fn accounts(
debug!("No Email address given")
},
}
let accountkey = accountpath.join("file.pem").with_file_name(name.clone());
let accountkey = accountpath.join("file.pem").with_file_name(name);
let mut accountkeyfile = None;
if accountkey.exists() {
if let Ok(key) = load_privkey(accountkey).await {
ac.private_key(key);
}
} else {
info!("creating new key for the account {}", name.clone());
info!("creating new key for the account {}", name.to_owned());
accountkeyfile = match FILE_MODE_OVERWRITE.clone().mode(MODE_SECRETS).open(accountkey).await {
Ok(file) => Some(file),
Err(error) => {
@ -193,7 +204,7 @@ pub async fn accounts(
}
let account = match ac.build().await {
Ok(account) => {
accounts.insert(name.clone(), Arc::clone(&account));
accounts.insert(name.to_owned(), Arc::clone(&account));
account
},
Err(error) => {
@ -209,6 +220,120 @@ pub async fn accounts(
}
}
async fn load_privkey(path: PathBuf) -> Result<PKey<Private>, Error> {
let mut file = match FILE_MODE.open(path).await {
Ok(file) => file,
Err(error) => return Error::err(format!("Failed to open Private Key: {error}")),
};
let mut data = String::with_capacity(file.metadata().await.map(|metadata| metadata.len()).unwrap_or_default() as usize);
if let Err(error) = file.read_to_string(&mut data).await {
return Error::err(format!("Failed to read data for the key: {error}"));
}
match PKey::private_key_from_pem(data.as_bytes()) {
Ok(key) => Ok(key),
Err(error) => Error::err(format!("Failed to parse pem data: {error}")),
}
}
async fn load_or_create_privkey(
path: PathBuf,
owner: Option<u32>,
group: Option<u32>,
mode: Option<u32>,
algorithm: Algorithm,
strength: Strength,
) -> types::Result<(PKey<Private>, bool)> {
let mut key_changed = false;
let privkey;
if path.exists() {
match load_privkey(path.clone()).await {
Ok(key) => {
if !key.matches(algorithm, strength) {
info!("Regenerating Key: Parameters changed");
privkey = gen_key(algorithm, strength)?;
key_changed = true;
} else {
privkey = key;
}
},
Err(error) => {
error!("Failed to load key: {error}");
key_changed = true;
privkey = gen_key(algorithm, strength)?;
},
}
} else {
key_changed = true;
privkey = gen_key(algorithm, strength)?;
}
if key_changed {
let mut keyfile = match FILE_MODE_OVERWRITE.clone().open(path.clone()).await {
Ok(file) => file,
Err(error) => return Error::err(format!("Failed to write key: {error}")),
};
if let Err(error) = fchown(keyfile.as_fd(), owner, group) {
error!("Failed to change owner of the private key: {error}")
}
if let Some(mode) = mode {
match unsafe { fchmod(keyfile.as_raw_fd(), mode) } {
0 => {},
libc::EROFS => error!("Not enough Permissions to change the mode of the private key"),
libc::EINVAL => error!("Invalid Mode for the private key"),
libc::EINTR => warn!("chmod was interrupted by an signal"),
err => error!("unkown return code from fchmod: {err}"),
}
}
let pkey = match privkey.private_key_to_pem_pkcs8() {
Ok(pkey) => pkey,
Err(error) => return Err(Error::from_display(error)),
};
if let Err(error) = keyfile.write_all(pkey.as_slice()).await {
return Err(Error::from_display(error));
}
}
Ok((privkey, key_changed))
}
async fn load_public_key(path: PathBuf) -> types::Result<Certificate> {
let mut file = match FILE_MODE.open(path).await {
Ok(file) => file,
Err(error) => return Err(Error::from_display(error)),
};
let mut data = String::with_capacity(file.metadata().await.map(|metadata| metadata.len()).unwrap_or_default() as usize);
if let Err(error) = file.read_to_string(&mut data).await {
return Error::err(format!("Failed to read the public key: {error}"));
}
let pem = match pem_parse(data.as_bytes()) {
Ok(pem) => pem,
Err(error) => return Error::err(format!("Failed to parse PEM file: {error}")),
};
let cert = match X509::from_der(pem.contents()) {
Ok(cert) => cert,
Err(error) => {
return Error::err(format!(
"Failed to parse the certificate:\n- {}",
error
.errors()
.iter()
.map(|err| err.to_string())
.reduce(|mut errorlist, item| {
errorlist.push_str("\n- ");
errorlist.push_str(item.as_str());
errorlist
})
.unwrap_or_default()
));
},
};
Ok(Certificate {
account_id: pem.headers().get("account_id").map(|d| d.to_string()),
cert,
})
}
pub async fn site(args: ProcessorArgs<'_>) {
let mut cert_renew = false;
@ -222,62 +347,43 @@ pub async fn site(args: ProcessorArgs<'_>) {
cert_renew = true;
}
let (uid, gid) = get_uid_gid(args.owner(), args.group());
let mut private_key;
// Private key block
{
let private_key_file = directory.join("privkey.pem");
let mut write_pkey = false;
if !private_key_file.exists() {
cert_renew = true;
write_pkey = true;
private_key = match_error!(gen_key(args.algorithm(), args.strength())=>Err(error)->"Aborting processing the site due to problem with the certificate generation: {error}");
} else if let Ok(key) = load_privkey(private_key_file.clone()).await {
private_key = key;
if !private_key.matches(args.algorithm(), args.strength()) {
info!("Algorithm for the private key has changed, updating the key");
cert_renew = true;
write_pkey = true;
private_key = match_error!(gen_key(args.algorithm(), args.strength())=>Err(error)->"Aborting processing the site due to problem with the certificate generation: {error}");
}
} else {
error!("Failed to parse the private key. Renewing the private key.");
write_pkey = true;
cert_renew = true;
private_key = match_error!(gen_key(args.algorithm(), args.strength())=>Err(error)->"Aborting processing the site due to problem with the certificate generation: {error}");
}
if write_pkey {
let pkey = private_key.private_key_to_pem_pkcs8().unwrap();
let mut file = match_error!(FILE_MODE_OVERWRITE.clone().mode(MODE_PRIVATE).open(private_key_file.clone()).await=>Err(error)->"Failed to write new private key: {error}");
#[cfg(feature = "capabilities")]
if let Err(error) = fchown(file.as_fd(), uid, gid) {
error!("Failed to change owner of the new privatekey: {error}");
return;
}
match_error!(file.write_all(&pkey).await=>Err(error)->"Failed to write new private key: {error}");
}
}
let private_key = match load_or_create_privkey(directory.join("privkey.pem"), uid, gid, Some(MODE_SECRETS), args.algorithm(), args.strength()).await {
Ok((key, changes)) => {
cert_renew |= changes;
key
},
Err(error) => {
error!("Failed to load or to create the key: {error}");
return;
},
};
let pubkey_filename = directory.join("pubkey.pem");
if pubkey_filename.exists() {
let mut file = match_error!(FILE_MODE.open(pubkey_filename.clone()).await=>Err(error)->"Failed to open publickey. Aborting processing: {error}");
let mut data = String::new();
if let Err(error) = file.read_to_string(&mut data).await {
cert_renew = true;
error!("Failed to read public key: {error}")
} else {
let pubkey = match X509::from_pem(data.as_bytes()) {
Ok(key) => key,
Err(_) => todo!(),
};
if !pubkey.days_left(args.refresh_time()) {
let exists = pubkey_filename.exists();
let publickey = load_public_key(pubkey_filename.clone()).await;
if exists {
if let Ok(pubkey) = publickey.clone() {
if let Some(account) = pubkey.account_id {
if account != args.account().id {
info!("Account changed");
cert_renew = true;
}
}
if !pubkey.cert.days_left(args.refresh_time()) {
info!("Certificate is running out of time");
cert_renew = true
} else if !pubkey.match_san(args.san()) {
info!("Subject Alternative Names differ from Certifcate");
}
if !pubkey.cert.match_san(args.san()) {
info!("Subject Alternative Names differ from Certificate");
cert_renew = true;
};
}
} else {
}
if !exists || publickey.is_err() {
cert_renew = true;
if let Err(error) = publickey {
error!("Failed to parse the public key: {error}")
}
}
if !cert_renew {
info!("Site {} doesn't need an update for the certificate.", args.name());
@ -450,7 +556,7 @@ pub async fn auth(auth: Authorization, challenge_dir: Option<PathBuf>, manager:
}
pub async fn services(restart_services: HashSet<String>, reload_services: HashSet<String>) {
let conn = match_error!(zbus_systemd::zbus::Connection::system().await=>Err(error)-> "Failed to connect with the systemd manager: {error}");
let conn = match_error!(Connection::system().await=>Err(error)-> "Failed to connect with the systemd manager: {error}");
let systemd_manager = systemd1::ManagerProxy::new(&conn).await.unwrap();

Datei anzeigen

@ -1,52 +1,56 @@
use crate::{
prelude::*,
types::{
self,
VString,
cryptography::{
Algorithm,
Strength,
},
dns::DnsBuilder,
dns::Builder,
structs::Error,
},
utils::schema::{
email_transform,
uri_transform,
},
};
use macro_rules_attribute::macro_rules_derive;
use macro_rules_attribute::apply;
use openssl::pkey::{
PKey,
Private,
};
use schemars::JsonSchema;
use serde::Deserialize;
use std::{
collections::HashMap,
net::IpAddr,
};
#[macro_rules_derive(DefDer)]
#[derive(Deserialize, JsonSchema)]
#[apply(DefDer)]
#[apply(ConfigFile)]
#[serde(deny_unknown_fields)]
pub struct GeneralConfig {
#[serde(default = "GeneralConfig::default_accounts")]
pub struct General {
#[serde(default = "General::default_accounts")]
pub accounts_path: String,
#[serde(default = "GeneralConfig::default_sites")]
#[serde(default = "General::default_sites")]
pub sites_path: String,
#[serde(default = "GeneralConfig::default_challenge")]
#[serde(default = "General::default_challenge")]
pub http_challenge_path: Option<String>,
/// This contains the domains(Keys) and the DNS-Servers(values) that are responsible for it.
#[serde(default = "GeneralConfig::default_dns")]
pub dns: HashMap<String, DnsBuilder>,
#[serde(default = "GeneralConfig::default_certificates")]
#[serde(default = "General::default_dns")]
pub dns: HashMap<String, Builder>,
#[serde(default = "General::default_certificates")]
pub certificates_path: String,
/// The Key of this table describe an nickname for an CA.
/// Letsencrypt Prod and Staging are builtin configured, so they doesn't have to be configured.
#[serde(default = "GeneralConfig::default_cas")]
#[serde(default = "General::default_cas")]
pub ca: HashMap<String, CA>,
}
impl GeneralConfig {
impl General {
#[inline]
pub(super) fn default_accounts() -> String {
"accounts".into()
@ -63,7 +67,7 @@ impl GeneralConfig {
}
#[inline]
pub(super) fn default_dns() -> HashMap<String, DnsBuilder> {
pub(super) fn default_dns() -> HashMap<String, Builder> {
HashMap::new()
}
@ -79,8 +83,8 @@ impl GeneralConfig {
}
#[macro_rules_derive(DefDer)]
#[derive(Deserialize, JsonSchema)]
#[apply(DefDer)]
#[apply(ConfigFile)]
#[serde(deny_unknown_fields)]
pub struct Eab {
#[serde(rename = "eab_token", alias = "id")]
@ -90,22 +94,22 @@ pub struct Eab {
}
impl Eab {
pub fn key(&self) -> super::Result<PKey<Private>> {
pub fn key(&self) -> types::Result<PKey<Private>> {
let decoded = &match_error!(data_encoding::BASE64URL_NOPAD.decode(self.key.as_bytes())=>Err(error)-> "Failed to decode the HMAC key for the eab_key: {error}", Error::err("Failed to decode eab_key"));
PKey::hmac(decoded).map_err(|error| Error::new(format!("Failed to parse the private key: {error}")))
}
}
#[macro_rules_derive(DefDer)]
#[derive(Deserialize, JsonSchema)]
#[apply(DefDer)]
#[apply(ConfigFile)]
#[serde(deny_unknown_fields)]
pub struct CA {
/// Url for the directory
#[schemars(transform=crate::utils::schema::uri_transform)]
#[schemars(transform=uri_transform)]
pub directory: String,
/// Email addresses for the CA to contact the user
#[schemars(transform=crate::utils::schema::email_transform)]
#[schemars(transform=email_transform)]
pub email_addresses: Option<VString>,
#[serde(flatten, default)]
@ -127,10 +131,11 @@ impl CA {
}
}
#[macro_rules_derive(DefDer)]
#[derive(Deserialize, Default, JsonSchema)]
#[apply(DefDer)]
#[apply(ConfigFile)]
#[derive(Default)]
#[serde(deny_unknown_fields)]
pub struct SiteConfig {
pub struct Site {
/// The Configured Certificate Authority
pub ca: String,
@ -143,7 +148,7 @@ pub struct SiteConfig {
/// EmailAdresses that this Certificate is valid for
#[serde(default)]
#[schemars(transform=crate::utils::schema::email_transform)]
#[schemars(transform=email_transform)]
pub emails: VString,
/// The systemd services are reloaded

Datei anzeigen

@ -1,14 +1,10 @@
use crate::{
consts::RsaStrength,
prelude::*,
};
use macro_rules_attribute::macro_rules_derive;
use schemars::JsonSchema;
use serde::Deserialize;
use crate::prelude::*;
use macro_rules_attribute::apply;
#[macro_rules_derive(DefDer)]
#[derive(Copy, Deserialize, Default, JsonSchema)]
#[apply(DefDer)]
#[apply(ConfigFile)]
#[derive(Copy, Default)]
pub enum Algorithm {
Rsa,
Brainpool,
@ -18,8 +14,9 @@ pub enum Algorithm {
}
#[macro_rules_derive(DefDer)]
#[derive(Copy, Deserialize, Default, JsonSchema)]
#[apply(DefDer)]
#[apply(ConfigFile)]
#[derive(Copy, Default)]
#[serde(deny_unknown_fields)]
pub enum Strength {
Weak,

Datei anzeigen

@ -1,18 +1,20 @@
use macro_rules_attribute::macro_rules_derive;
use schemars::JsonSchema;
use serde::Deserialize;
use macro_rules_attribute::apply;
use crate::{
macros::DefDer,
macros::{
ConfigFile,
DefDer,
},
types::{
self,
dns::Dns,
structs::DnsToken,
traits::DnsHandler,
},
};
#[macro_rules_derive(DefDer)]
#[derive(Deserialize, JsonSchema)]
#[apply(DefDer)]
#[apply(ConfigFile)]
#[serde(deny_unknown_fields)]
pub struct DNSUpdateClientOptions {}
@ -22,16 +24,16 @@ impl DNSUpdateClientOptions {
}
}
#[macro_rules_derive(DefDer)]
#[apply(DefDer)]
pub struct DnsUpdateHandler {}
impl DnsHandler for DnsUpdateHandler {
async fn set_record(&self, _domain: String, _content: String) -> crate::types::Result<DnsToken> {
async fn set_record(&self, _domain: String, _content: String) -> types::Result<DnsToken> {
Ok(DnsToken::new_dns_update())
}
}
#[macro_rules_derive(DefDer)]
#[apply(DefDer)]
pub struct DnsUpdateToken {}
impl DnsUpdateToken {

Datei anzeigen

@ -2,8 +2,12 @@ pub(super) mod dnsupdate;
pub(super) mod pdns;
use crate::{
macros::DefDer,
macros::{
ConfigFile,
DefDer,
},
types::{
self,
dns::{
dnsupdate::{
DNSUpdateClientOptions,
@ -18,27 +22,29 @@ use crate::{
DnsToken,
Error,
},
traits::DnsHandler,
traits::DnsHandler as _,
},
};
use log::*;
use macro_rules_attribute::macro_rules_derive;
use schemars::JsonSchema;
use serde::Deserialize;
use macro_rules_attribute::apply;
use reqwest::Client;
use std::{
collections::HashMap,
sync::Arc,
};
use tokio::sync::Mutex;
#[macro_rules_derive(DefDer)]
pub struct Manager(Arc<Mutex<InnerManager>>);
#[apply(DefDer)]
pub struct Manager(Arc<Mutex<InnerManager>>, Client);
impl Manager {
pub fn new() -> Self {
Self(Arc::new(Mutex::new(InnerManager {
servers: HashMap::new(),
})))
pub fn new(client: Client) -> Self {
Self(
Arc::new(Mutex::new(InnerManager {
servers: HashMap::new(),
})),
client,
)
}
pub async fn set_record(&self, domain: String, value: String) -> Option<DnsToken> {
@ -71,7 +77,7 @@ impl Manager {
}
}
pub async fn add_builder(&mut self, zone: String, builder: DnsBuilder) {
pub async fn add_builder(&mut self, zone: String, builder: Builder) {
let mut fixed_zone = zone.clone();
if !fixed_zone.ends_with('.') {
fixed_zone.push('.');
@ -79,9 +85,9 @@ impl Manager {
self.0.lock().await.servers.insert(
fixed_zone,
match builder {
DnsBuilder::PowerDNS(pdns_client_options) => pdns_client_options.build(zone),
DnsBuilder::DNSUpdate(dnsupdate_client_options) => dnsupdate_client_options.build(zone),
DnsBuilder::None => Dns::None,
Builder::PowerDNS(pdns_client_options) => pdns_client_options.build(zone, self.1.clone()),
Builder::DNSUpdate(dnsupdate_client_options) => dnsupdate_client_options.build(zone),
Builder::None => Dns::None,
},
);
}
@ -95,18 +101,19 @@ struct InnerManager {
}
#[macro_rules_derive(DefDer)]
#[derive(Deserialize, Default, JsonSchema)]
#[apply(DefDer)]
#[apply(ConfigFile)]
#[derive(Default)]
#[serde(deny_unknown_fields)]
#[serde(tag = "type", rename_all = "lowercase")]
pub enum DnsBuilder {
pub enum Builder {
PowerDNS(PdnsClientOptions),
DNSUpdate(DNSUpdateClientOptions),
#[default]
None,
}
#[macro_rules_derive(DefDer)]
#[apply(DefDer)]
pub enum Dns {
PowerDNS(PdnsHandler),
DNSUpdate(DnsUpdateHandler),
@ -114,7 +121,7 @@ pub enum Dns {
}
impl Dns {
pub async fn set_record(&self, domain: String, content: String) -> crate::types::Result<DnsToken> {
pub async fn set_record(&self, domain: String, content: String) -> types::Result<DnsToken> {
match self {
Dns::PowerDNS(pdns_handler) => pdns_handler.set_record(domain, content).await,
Dns::DNSUpdate(dns_update_handler) => dns_update_handler.set_record(domain, content).await,

Datei anzeigen

@ -5,22 +5,25 @@ use std::time::{
use derive_new::new;
use log::*;
use macro_rules_attribute::macro_rules_derive;
use macro_rules_attribute::apply;
use reqwest::{
Client,
RequestBuilder,
StatusCode,
Url,
};
use schemars::JsonSchema;
use serde::{
Deserialize,
Serialize,
};
use crate::{
default_client,
macros::DefDer,
macros::{
ConfigFile,
DefDer,
},
types::{
self,
dns::Dns,
structs::{
DnsToken,
@ -30,8 +33,8 @@ use crate::{
},
};
#[macro_rules_derive(DefDer)]
#[derive(Deserialize, JsonSchema)]
#[apply(DefDer)]
#[apply(ConfigFile)]
#[serde(deny_unknown_fields)]
pub struct PdnsClientOptions {
api_key: String,
@ -48,9 +51,9 @@ impl PdnsClientOptions {
}
impl PdnsClientOptions {
pub fn build(self, zone: String) -> Dns {
pub fn build(self, zone: String, client: Client) -> Dns {
Dns::PowerDNS(PdnsHandler {
client: default_client().unwrap(),
client,
server: self.server,
api_key: self.api_key,
server_id: self.server_id,
@ -59,9 +62,9 @@ impl PdnsClientOptions {
}
}
#[macro_rules_derive(DefDer)]
#[apply(DefDer)]
pub struct PdnsHandler {
client: reqwest::Client,
client: Client,
server: String,
api_key: String,
server_id: String,
@ -83,7 +86,7 @@ fn fix_url(baseurl: Url, path: String) -> Result<Url, ()> {
impl DnsHandler for PdnsHandler {
async fn set_record(&self, mut domain: String, content: String) -> crate::types::Result<DnsToken> {
async fn set_record(&self, mut domain: String, content: String) -> types::Result<DnsToken> {
trace!("Original URL: {}", self.server);
let baseurl = match reqwest::Url::parse(&self.server) {
Ok(url) => {
@ -178,7 +181,7 @@ impl PdnsToken {
unsafe impl Send for PdnsToken {}
#[macro_rules_derive(DefDer)]
#[apply(DefDer)]
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PdnsError {
@ -187,7 +190,7 @@ pub struct PdnsError {
pub errors: Vec<String>,
}
#[macro_rules_derive(DefDer)]
#[apply(DefDer)]
#[derive(Serialize, new)]
struct Comment {
#[new(into)]
@ -206,7 +209,7 @@ impl Comment {
}
}
#[macro_rules_derive(DefDer)]
#[apply(DefDer)]
#[derive(Serialize, new)]
struct Record {
content: String,
@ -215,7 +218,7 @@ struct Record {
}
#[macro_rules_derive(DefDer)]
#[apply(DefDer)]
#[derive(Serialize)]
#[serde(tag = "changetype", rename_all = "UPPERCASE")]
enum ChangeType {
@ -227,7 +230,7 @@ enum ChangeType {
Delete,
}
#[macro_rules_derive(DefDer)]
#[apply(DefDer)]
#[derive(Serialize, new)]
struct RRSet {
name: String,
@ -238,7 +241,7 @@ struct RRSet {
}
#[macro_rules_derive(DefDer)]
#[apply(DefDer)]
#[derive(Serialize, new)]
struct RecordUpdate {
rrsets: Vec<RRSet>,

Datei anzeigen

@ -1,5 +1,9 @@
use std::{
fmt::Display,
fmt::{
Display,
Formatter,
Result as fmtResult,
},
net::IpAddr,
};
@ -7,7 +11,7 @@ use acme2_eab::Identifier;
use openssl::x509::GeneralName;
use crate::types::{
config::GeneralConfig,
config::General,
dns::pdns::PdnsError,
structs::{
Error,
@ -16,7 +20,7 @@ use crate::types::{
};
impl Default for GeneralConfig {
impl Default for General {
#[inline]
fn default() -> Self {
Self {
@ -78,14 +82,14 @@ impl From<San> for Identifier {
}
impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fn fmt(&self, f: &mut Formatter<'_>) -> fmtResult {
f.write_str(&self.message)
}
}
impl Display for PdnsError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
fn fmt(&self, f: &mut Formatter<'_>) -> fmtResult {
f.write_str(&self.error)?;
if !self.errors.is_empty() {
f.write_str("(")?;

Datei anzeigen

@ -1,7 +1,9 @@
use std::collections::HashSet;
use std::{
collections::HashSet,
result::Result as stdResult,
};
use tokio::sync::Mutex;
pub mod config;
pub mod cryptography;
pub mod dns;
@ -9,10 +11,11 @@ mod foreign_impl;
pub mod structs;
pub mod traits;
/// Alias for Vec\<String\>
pub type VString = Vec<String>;
/// Alias for an Safe Hashset
pub type SafeSet<T> = Mutex<HashSet<T>>;
pub type Result<T> = std::result::Result<T, structs::Error>;
pub type Result<T> = stdResult<T, structs::Error>;

Datei anzeigen

@ -15,7 +15,8 @@ use clap::{
Subcommand,
};
use derive_new::new;
use macro_rules_attribute::macro_rules_derive;
use macro_rules_attribute::apply;
use openssl::x509::X509;
use reqwest::{
Client,
RequestBuilder,
@ -26,7 +27,7 @@ use crate::{
prelude::*,
types::{
SafeSet,
config::SiteConfig,
config::Site,
cryptography::{
Algorithm,
Strength,
@ -40,7 +41,7 @@ use crate::{
};
#[macro_rules_derive(DefDer)]
#[apply(DefDer)]
#[derive(Parser)]
pub struct Arguments {
pub config: String,
@ -48,17 +49,17 @@ pub struct Arguments {
pub subcommands: Option<SubCommand>,
}
#[macro_rules_derive(DefDer)]
#[apply(DefDer)]
#[derive(Subcommand)]
pub enum SubCommand {
Schema,
}
#[macro_rules_derive(DefDer)]
#[apply(DefDer)]
#[allow(clippy::too_many_arguments)]
#[derive(new)]
pub struct ProcessorArgs<'a> {
site: SiteConfig,
site: Site,
account: Arc<Account>,
reload_services: &'a SafeSet<String>,
restart_services: &'a SafeSet<String>,
@ -128,7 +129,8 @@ impl<'a: 'b, 'b> ProcessorArgs<'a> {
}
}
#[macro_rules_derive(DefDer, Hashable)]
#[apply(DefDer)]
#[apply(Hashable)]
#[derive(PartialEq)]
pub enum San {
Dns(String),
@ -136,7 +138,7 @@ pub enum San {
IPAddress(IpAddr),
}
#[macro_rules_derive(DefDer)]
#[apply(DefDer)]
#[derive(derive_new::new)]
pub struct Error {
pub(super) message: String,
@ -162,7 +164,7 @@ impl Error {
}
#[macro_rules_derive(DefDer)]
#[apply(DefDer)]
pub enum DnsToken {
None,
Pdns(Box<PdnsToken>),
@ -190,3 +192,9 @@ impl DnsToken {
}
}
}
#[apply(DefDer)]
pub struct Certificate {
pub cert: X509,
pub account_id: Option<String>,
}

Datei anzeigen

@ -2,16 +2,9 @@ use std::collections::HashSet;
use crate::{
consts::{
BRAINPOOL_MIDDLE,
BRAINPOOL_STRONG,
BRAINPOOL_WEAK,
SECP_MIDDLE,
SECP_STRONG,
SECP_WEAK,
},
types,
prelude::*,
types::{
self,
cryptography::{
Algorithm,
Strength,
@ -33,9 +26,10 @@ use openssl::{
x509::X509,
};
use serde::de::DeserializeOwned;
use std::fmt::Debug as fmtDebug;
use tokio::{
fs::File,
io::AsyncReadExt,
io::AsyncReadExt as _,
};
pub trait FromFile: Default + DeserializeOwned {
@ -122,6 +116,6 @@ impl MatchX509 for X509 {
}
}
pub trait DnsHandler: std::fmt::Debug + Send {
pub trait DnsHandler: fmtDebug + Send {
async fn set_record(&self, domain: String, content: String) -> types::Result<DnsToken>;
}

Datei anzeigen

@ -1,14 +1,13 @@
use std::ffi::CString;
use std::{
env::{
self,
VarError,
},
ffi::CString,
};
use crate::{
consts::{
BRAINPOOL_MIDDLE,
BRAINPOOL_STRONG,
BRAINPOOL_WEAK,
SECP_MIDDLE,
SECP_STRONG,
SECP_WEAK,
},
prelude::*,
types::{
cryptography::{
Algorithm,
@ -38,6 +37,7 @@ use openssl::{
X509NameBuilder,
},
};
use systemd_journal_logger::JournalLog;
#[cfg(feature = "capabilities")]
const CAPABILITY_SET: CapSet = CapSet::Permitted;
@ -249,3 +249,41 @@ pub mod schema {
type_transform(schema, "uri");
}
}
pub(crate) fn logging() {
let level = match env::var("RUST_LOG") {
Ok(levelname) => levelname.to_uppercase(),
Err(error) => {
match error {
VarError::NotPresent => {},
VarError::NotUnicode(text) => println!("Invalid Log Level {}", text.display()),
};
"INFO".to_string()
},
};
let journal = JournalLog::new().map(|logger| logger.with_extra_fields(vec![("PKG_VERSION", env!("CARGO_PKG_VERSION"))]));
match journal {
Ok(logger) => {
match logger.install() {
Ok(()) => {
log::set_max_level(match level.as_str() {
"OFF" => LevelFilter::Off,
"TRACE" => LevelFilter::Trace,
"DEBUG" => LevelFilter::Debug,
"WARN" => LevelFilter::Warn,
"ERROR" => LevelFilter::Error,
_ => LevelFilter::Info,
});
},
Err(error) => {
env_logger::init();
warn!("Failed to initialize the Journal Logger: {error}")
},
}
},
Err(error) => {
env_logger::init();
warn!("Failed to initialize the Journal Logger: {error}")
},
}
}