From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id 07FF19B66B for ; Wed, 18 Oct 2023 12:39:45 +0200 (CEST) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id E48B6D3FE for ; Wed, 18 Oct 2023 12:39:14 +0200 (CEST) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [94.136.29.106]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by firstgate.proxmox.com (Proxmox) with ESMTPS for ; Wed, 18 Oct 2023 12:39:13 +0200 (CEST) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 7C99A42A12 for ; Wed, 18 Oct 2023 12:39:13 +0200 (CEST) From: Dominik Csapak To: pbs-devel@lists.proxmox.com Date: Wed, 18 Oct 2023 12:39:10 +0200 Message-Id: <20231018103911.3798182-4-d.csapak@proxmox.com> X-Mailer: git-send-email 2.30.2 In-Reply-To: <20231018103911.3798182-1-d.csapak@proxmox.com> References: <20231018103911.3798182-1-d.csapak@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL 0.012 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: [pbs-devel] [RFC proxmox 3/3] new proxmox-cert-management crate X-BeenThere: pbs-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Backup Server development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Wed, 18 Oct 2023 10:39:45 -0000 refactored from proxmox-backup, but uses the new ServerConfig global settings for the users and directories. Signed-off-by: Dominik Csapak --- Cargo.toml | 2 + proxmox-cert-management/Cargo.toml | 23 +++ proxmox-cert-management/debian/changelog | 5 + proxmox-cert-management/debian/control | 53 ++++++ proxmox-cert-management/debian/copyright | 18 ++ proxmox-cert-management/debian/debcargo.toml | 7 + proxmox-cert-management/src/lib.rs | 182 +++++++++++++++++++ 7 files changed, 290 insertions(+) create mode 100644 proxmox-cert-management/Cargo.toml create mode 100644 proxmox-cert-management/debian/changelog create mode 100644 proxmox-cert-management/debian/control create mode 100644 proxmox-cert-management/debian/copyright create mode 100644 proxmox-cert-management/debian/debcargo.toml create mode 100644 proxmox-cert-management/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 4b4b787..0a4ad06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "proxmox-async", "proxmox-auth-api", "proxmox-borrow", + "proxmox-cert-management", "proxmox-client", "proxmox-compression", "proxmox-http", @@ -91,6 +92,7 @@ webauthn-rs = "0.3" zstd = { version = "0.12", features = [ "bindgen" ] } # workspace dependencies +proxmox-auth-api = { version = "0.3", path = "proxmox-auth-api" } proxmox-api-macro = { version = "1.0.6", path = "proxmox-api-macro" } proxmox-async = { version = "0.4.1", path = "proxmox-async" } proxmox-compression = { version = "0.2.0", path = "proxmox-compression" } diff --git a/proxmox-cert-management/Cargo.toml b/proxmox-cert-management/Cargo.toml new file mode 100644 index 0000000..816f4b6 --- /dev/null +++ b/proxmox-cert-management/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "proxmox-cert-management" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +description = "Certificate management and utilities" + +exclude.workspace = true + +[dependencies] +anyhow.workspace = true +base64.workspace = true +lazy_static.workspace = true +nix.workspace = true +openssl.workspace = true + +proxmox-auth-api = { workspace = true, features = ["api-types"] } +proxmox-lang.workspace = true +proxmox-sys.workspace = true +proxmox-server-config.workspace = true +proxmox-time.workspace = true diff --git a/proxmox-cert-management/debian/changelog b/proxmox-cert-management/debian/changelog new file mode 100644 index 0000000..ed5d4d6 --- /dev/null +++ b/proxmox-cert-management/debian/changelog @@ -0,0 +1,5 @@ +rust-proxmox-cert-management (0.1.0-1) stable; urgency=medium + + * initial version + + -- Proxmox Support Team Tue, 17 Oct 2023 13:56:35 +0200 diff --git a/proxmox-cert-management/debian/control b/proxmox-cert-management/debian/control new file mode 100644 index 0000000..593ea6c --- /dev/null +++ b/proxmox-cert-management/debian/control @@ -0,0 +1,53 @@ +Source: rust-proxmox-cert-management +Section: rust +Priority: optional +Build-Depends: debhelper (>= 12), + dh-cargo (>= 25), + cargo:native , + rustc:native , + libstd-rust-dev , + librust-anyhow-1+default-dev , + librust-base64-0.13+default-dev , + librust-lazy-static-1+default-dev (>= 1.4-~~) , + librust-nix-0.26+default-dev (>= 0.26.1-~~) , + librust-openssl-0.10+default-dev , + librust-proxmox-auth-api-0.3+api-types-dev , + librust-proxmox-auth-api-0.3+default-dev , + librust-proxmox-lang-1+default-dev (>= 1.1-~~) , + librust-proxmox-server-config-0.1+default-dev , + librust-proxmox-sys-0.5+default-dev , + librust-proxmox-time-1+default-dev (>= 1.1.4-~~) +Maintainer: Proxmox Support Team +Standards-Version: 4.6.1 +Vcs-Git: git://git.proxmox.com/git/proxmox.git +Vcs-Browser: https://git.proxmox.com/?p=proxmox.git +X-Cargo-Crate: proxmox-cert-management +Rules-Requires-Root: no + +Package: librust-proxmox-cert-management-dev +Architecture: any +Multi-Arch: same +Depends: + ${misc:Depends}, + librust-anyhow-1+default-dev, + librust-base64-0.13+default-dev, + librust-lazy-static-1+default-dev (>= 1.4-~~), + librust-nix-0.26+default-dev (>= 0.26.1-~~), + librust-openssl-0.10+default-dev, + librust-proxmox-auth-api-0.3+api-types-dev, + librust-proxmox-auth-api-0.3+default-dev, + librust-proxmox-lang-1+default-dev (>= 1.1-~~), + librust-proxmox-server-config-0.1+default-dev, + librust-proxmox-sys-0.5+default-dev, + librust-proxmox-time-1+default-dev (>= 1.1.4-~~) +Provides: + librust-proxmox-cert-management+default-dev (= ${binary:Version}), + librust-proxmox-cert-management-0-dev (= ${binary:Version}), + librust-proxmox-cert-management-0+default-dev (= ${binary:Version}), + librust-proxmox-cert-management-0.1-dev (= ${binary:Version}), + librust-proxmox-cert-management-0.1+default-dev (= ${binary:Version}), + librust-proxmox-cert-management-0.1.0-dev (= ${binary:Version}), + librust-proxmox-cert-management-0.1.0+default-dev (= ${binary:Version}) +Description: Certificate management and utilities - Rust source code + This package contains the source for the Rust proxmox-cert-management crate, + packaged by debcargo for use with cargo and dh-cargo. diff --git a/proxmox-cert-management/debian/copyright b/proxmox-cert-management/debian/copyright new file mode 100644 index 0000000..0d9eab3 --- /dev/null +++ b/proxmox-cert-management/debian/copyright @@ -0,0 +1,18 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ + +Files: + * +Copyright: 2019 - 2023 Proxmox Server Solutions GmbH +License: AGPL-3.0-or-later + This program is free software: you can redistribute it and/or modify it under + the terms of the GNU Affero General Public License as published by the Free + Software Foundation, either version 3 of the License, or (at your option) any + later version. + . + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more + details. + . + You should have received a copy of the GNU Affero General Public License along + with this program. If not, see . diff --git a/proxmox-cert-management/debian/debcargo.toml b/proxmox-cert-management/debian/debcargo.toml new file mode 100644 index 0000000..b7864cd --- /dev/null +++ b/proxmox-cert-management/debian/debcargo.toml @@ -0,0 +1,7 @@ +overlay = "." +crate_src_path = ".." +maintainer = "Proxmox Support Team " + +[source] +vcs_git = "git://git.proxmox.com/git/proxmox.git" +vcs_browser = "https://git.proxmox.com/?p=proxmox.git" diff --git a/proxmox-cert-management/src/lib.rs b/proxmox-cert-management/src/lib.rs new file mode 100644 index 0000000..dd327f7 --- /dev/null +++ b/proxmox-cert-management/src/lib.rs @@ -0,0 +1,182 @@ +use anyhow::{bail, format_err, Error}; +use lazy_static::lazy_static; +use openssl::pkey::{PKey, Private, Public}; +use openssl::rsa::Rsa; +use openssl::sha; + +use proxmox_auth_api::types::Userid; +use proxmox_lang::try_block; +use proxmox_server_config::get_server_config; +use proxmox_sys::fs::{file_get_contents, replace_file, CreateOptions}; + +fn compute_csrf_secret_digest(timestamp: i64, secret: &[u8], userid: &Userid) -> String { + let mut hasher = sha::Sha256::new(); + let data = format!("{:08X}:{}:", timestamp, userid); + hasher.update(data.as_bytes()); + hasher.update(secret); + + base64::encode_config(hasher.finish(), base64::STANDARD_NO_PAD) +} + +pub fn assemble_csrf_prevention_token(secret: &[u8], userid: &Userid) -> String { + let epoch = proxmox_time::epoch_i64(); + + let digest = compute_csrf_secret_digest(epoch, secret, userid); + + format!("{:08X}:{}", epoch, digest) +} + +pub fn verify_csrf_prevention_token( + secret: &[u8], + userid: &Userid, + token: &str, + min_age: i64, + max_age: i64, +) -> Result { + use std::collections::VecDeque; + + let mut parts: VecDeque<&str> = token.split(':').collect(); + + try_block!({ + if parts.len() != 2 { + bail!("format error - wrong number of parts."); + } + + let timestamp = parts.pop_front().unwrap(); + let sig = parts.pop_front().unwrap(); + + let ttime = i64::from_str_radix(timestamp, 16) + .map_err(|err| format_err!("timestamp format error - {}", err))?; + + let digest = compute_csrf_secret_digest(ttime, secret, userid); + + if digest != sig { + bail!("invalid signature."); + } + + let now = proxmox_time::epoch_i64(); + + let age = now - ttime; + if age < min_age { + bail!("timestamp newer than expected."); + } + + if age > max_age { + bail!("timestamp too old."); + } + + Ok(age) + }) + .map_err(|err| format_err!("invalid csrf token - {}", err)) +} + +pub fn generate_csrf_key() -> Result<(), Error> { + let server_config = get_server_config()?; + let path = server_config.config_dir().join("csrf.key"); + + if path.exists() { + return Ok(()); + } + + let rsa = Rsa::generate(2048).unwrap(); + + let pem = rsa.private_key_to_pem()?; + + use nix::sys::stat::Mode; + + replace_file( + &path, + &pem, + CreateOptions::new() + .perm(Mode::from_bits_truncate(0o0640)) + .owner(server_config.privileged_user().uid) + .group(server_config.user().gid), + true, + )?; + + Ok(()) +} + +pub fn generate_auth_key() -> Result<(), Error> { + let server_config = get_server_config()?; + let priv_path = server_config.config_dir().join("authkey.key"); + + let mut public_path = priv_path.clone(); + public_path.set_extension("pub"); + + if priv_path.exists() && public_path.exists() { + return Ok(()); + } + + let rsa = Rsa::generate(4096).unwrap(); + + let priv_pem = rsa.private_key_to_pem()?; + + use nix::sys::stat::Mode; + + replace_file( + &priv_path, + &priv_pem, + CreateOptions::new().perm(Mode::from_bits_truncate(0o0600)), + true, + )?; + + let public_pem = rsa.public_key_to_pem()?; + + replace_file( + &public_path, + &public_pem, + CreateOptions::new() + .perm(Mode::from_bits_truncate(0o0640)) + .owner(server_config.privileged_user().uid) + .group(server_config.user().gid), + true, + )?; + + Ok(()) +} + +pub fn csrf_secret() -> &'static [u8] { + lazy_static! { + static ref SECRET: Vec = { + let dir = get_server_config().unwrap().config_dir().join("csrf.key"); + file_get_contents(dir).unwrap() + }; + } + + &SECRET +} + +fn load_public_auth_key() -> Result, Error> { + let pem_path = get_server_config()?.config_dir().join("authkey.pub"); + let pem = file_get_contents(pem_path)?; + let rsa = Rsa::public_key_from_pem(&pem)?; + let key = PKey::from_rsa(rsa)?; + + Ok(key) +} + +pub fn public_auth_key() -> &'static PKey { + lazy_static! { + static ref KEY: PKey = load_public_auth_key().unwrap(); + } + + &KEY +} + +fn load_private_auth_key() -> Result, Error> { + let pem_path = get_server_config()?.config_dir().join("authkey.key"); + let pem = file_get_contents(pem_path)?; + let rsa = Rsa::private_key_from_pem(&pem)?; + let key = PKey::from_rsa(rsa)?; + + Ok(key) +} + +pub fn private_auth_key() -> &'static PKey { + lazy_static! { + static ref KEY: PKey = load_private_auth_key().unwrap(); + } + + &KEY +} -- 2.30.2