diff --git a/README.md b/README.md index ebb143e..cc2ece4 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ mail_settings.smtp_port | Any positive number | The port of the SMTP server mail_settings.smtp_tls | `Tls` or `Starttls` | The encryption method to use mail_settings.mail_from | String | The email address to be used mail_settings.mail_subject | String | The confirmation email's subject +policy.key_max_validity | Any positive number | The maximum allowed validity period of a key in seconds. (optional) ## Environment Variables diff --git a/backend/Cargo.toml b/backend/Cargo.toml index cf317ff..8228ad0 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -20,3 +20,4 @@ tokio = { version = "1.27.0", features = ["time"] } toml = "0.7.3" url = { version = "2.3.1", features = ["serde"] } zbase32 = "0.1.2" +sequoia-policy-config = { version = "0.6" } diff --git a/backend/src/confirmation.rs b/backend/src/confirmation.rs index 08d52e7..27f5010 100644 --- a/backend/src/confirmation.rs +++ b/backend/src/confirmation.rs @@ -5,8 +5,8 @@ use log::{debug, error, warn}; use crate::errors::SpecialErrors; use crate::management::{delete_key, Action, Pending}; use crate::settings::{MAILER, SETTINGS}; -use crate::utils::{get_email_from_cert, insert_key, parse_pem, read_file}; -use crate::{log_err, pending_path, validate_cert}; +use crate::utils::{get_email_from_cert, insert_key, parse_pem, read_file, validate_cert}; +use crate::{log_err, pending_path}; use anyhow::Result; use lettre::{AsyncTransport, Message}; @@ -25,7 +25,7 @@ pub fn confirm_action(token: &str) -> Result<(Action, String)> { let address = match key.action() { Action::Add => { let cert = parse_pem(key.data())?; - let validcert = validate_cert!(cert)?; + let validcert = validate_cert(&cert)?; let email = get_email_from_cert(&validcert)?; log_err!(insert_key(&validcert), warn)?; email diff --git a/backend/src/errors.rs b/backend/src/errors.rs index fde8436..fb1dcf8 100644 --- a/backend/src/errors.rs +++ b/backend/src/errors.rs @@ -44,7 +44,7 @@ pub enum SpecialErrors { ExpiredRequest, #[error("The key for the requested user does not exist!")] InexistingUser, - #[error("The key is either expired or uses an obsolete cipher!")] + #[error("The primary key is either expired or uses an obsolete cipher!")] InvalidCert, #[error("Error while sending email")] MailErr, @@ -56,6 +56,12 @@ pub enum SpecialErrors { MissingFile, #[error("User email rejected: domain not allowed")] UnallowedDomain, + #[error("The primary key or a subkey does not expire")] + KeyNonExpiring, + #[error("The primary keys or a subkeys validity is too long")] + KeyValidityTooLong, + #[error("A subkey is either expired or uses an obsolete cipher!")] + KeyPolicyViolation, } #[derive(Debug)] @@ -104,6 +110,9 @@ impl ResponseError for CompatErr { SpecialErrors::MalformedEmail => StatusCode::BAD_REQUEST, SpecialErrors::MissingFile => StatusCode::NOT_FOUND, SpecialErrors::UnallowedDomain => StatusCode::UNAUTHORIZED, + SpecialErrors::KeyNonExpiring => StatusCode::BAD_REQUEST, + SpecialErrors::KeyValidityTooLong => StatusCode::BAD_REQUEST, + SpecialErrors::KeyPolicyViolation => StatusCode::BAD_REQUEST, }, } } diff --git a/backend/src/main.rs b/backend/src/main.rs index 0d4b886..5aab3b7 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -11,7 +11,7 @@ use crate::management::{clean_stale, store_pending_addition, store_pending_delet use crate::settings::{ROOT_FOLDER, SETTINGS}; use crate::utils::{ gen_random_token, get_email_from_cert, is_email_allowed, key_exists, parse_pem, read_file, - return_outcome, + return_outcome, validate_cert }; use actix_files::Files; @@ -102,7 +102,7 @@ async fn index(req: HttpRequest) -> Result { #[post("/api/submit")] async fn submit(pem: web::Form) -> Result { let cert = parse_pem(&pem.key)?; - let validcert = validate_cert!(cert)?; + let validcert = validate_cert(&cert)?; if validcert.is_tsk() { Err(SpecialErrors::ContainsSecret)? } diff --git a/backend/src/settings.rs b/backend/src/settings.rs index db23705..8ec2473 100644 --- a/backend/src/settings.rs +++ b/backend/src/settings.rs @@ -2,6 +2,7 @@ use lettre::{transport::smtp::authentication::Credentials, AsyncSmtpTransport, T use log::{debug, error}; use once_cell::sync::Lazy; use sequoia_openpgp::policy::StandardPolicy; +use sequoia_policy_config::ConfiguredStandardPolicy; use serde::{Deserialize, Serialize}; use std::path::PathBuf; use url::Url; @@ -18,6 +19,7 @@ pub struct Settings { pub bind_host: String, pub external_url: Url, pub mail_settings: MailSettings, + pub policy: Option, } #[derive(Serialize, Deserialize, Debug)] @@ -31,6 +33,11 @@ pub struct MailSettings { pub mail_subject: String, } +#[derive(Serialize, Deserialize, Debug)] +pub struct Policy { + pub key_max_validity: Option, +} + #[derive(Serialize, Deserialize, Debug)] pub enum Variant { Advanced, @@ -87,8 +94,19 @@ fn get_mailer() -> AsyncSmtpTransport { .build() } +fn get_policy<'a>() -> StandardPolicy<'a> { + let mut p = ConfiguredStandardPolicy::new(); + + match p.parse_default_config() { + Ok(_) => {}, + Err(e) => error!("{e}"), + } + + p.build() +} + pub const ERROR_TEXT: &str = "An error occoured:"; -pub const POLICY: &StandardPolicy = &StandardPolicy::new(); +pub static POLICY: Lazy = Lazy::new(get_policy); pub const ROOT_FOLDER: &str = "data"; pub static SETTINGS: Lazy = Lazy::new(get_settings); pub static MAILER: Lazy> = Lazy::new(get_mailer); diff --git a/backend/src/utils.rs b/backend/src/utils.rs index 05d8fa6..a1afe2e 100644 --- a/backend/src/utils.rs +++ b/backend/src/utils.rs @@ -4,6 +4,7 @@ use crate::log_err; use crate::settings::Variant; use crate::settings::ROOT_FOLDER; use crate::settings::SETTINGS; +use crate::settings::POLICY; use actix_web::ResponseError; use actix_web::{ @@ -16,22 +17,56 @@ use log::debug; use log::error; use rand::{distributions::Alphanumeric, thread_rng, Rng}; use sequoia_openpgp::cert::ValidCert; +use sequoia_openpgp::cert::amalgamation::ValidateAmalgamation; use sequoia_openpgp::serialize::Marshal; use sequoia_openpgp::types::HashAlgorithm; use sequoia_openpgp::{parse::Parse, Cert}; use std::{ fs, path::{Path, PathBuf}, + time::Duration, }; -#[macro_export] -macro_rules! validate_cert { - ( $x:expr ) => { - match log_err!($x.with_policy($crate::settings::POLICY, None), debug) { - Ok(validcert) => Ok(validcert), - Err(_) => Err($crate::errors::SpecialErrors::InvalidCert), - } +pub fn validate_cert(cert: &Cert) -> Result { + let validcert = match log_err!(cert.with_policy(&*POLICY, None), debug) { + Ok(validcert) => validcert, + Err(e) => { + debug!("Certificate was rejected: The primary key violates the policy: {}", e.source().unwrap()); + Err(SpecialErrors::InvalidCert)? + } }; + + for key in cert.keys().subkeys() { + match log_err!(key.with_policy(&*POLICY, None), debug) { + Ok(_) => continue, + Err(e) => { + debug!("Certificate was rejected: A sub key violates the policy: {}", e.source().unwrap()); + Err(SpecialErrors::KeyPolicyViolation)? + } + } + } + + if let Some(policy_settings) = &SETTINGS.policy { + if let Some(max_validity_setting) = policy_settings.key_max_validity { + let max_validity = Duration::from_secs(max_validity_setting); + + if !max_validity.is_zero() { + for key in validcert.keys() { + let validity = key.key_validity_period(); + + if validity.is_none() { + debug!("Certificate was rejected: The primary key or a subkey has validity period of zero"); + return Err(SpecialErrors::KeyNonExpiring)? + } else if validity > Some(max_validity) { + debug!("Certificate was rejected: The primary key or a subkey has a validity period greater than {max_validity_setting} seconds"); + return Err(SpecialErrors::KeyValidityTooLong)? + } + } + } + } + } + + Ok(validcert) } pub fn encode_local(local: &str) -> String { @@ -106,7 +141,7 @@ pub fn parse_pem(pemfile: &str) -> Result { Ok(cert) => cert, Err(_) => Err(SpecialErrors::MalformedCert)?, }; - validate_cert!(cert)?; + validate_cert(&cert)?; Ok(cert) } diff --git a/example.config.toml b/example.config.toml index 67a9724..e21792a 100644 --- a/example.config.toml +++ b/example.config.toml @@ -13,4 +13,7 @@ smtp_password = "verysecurepassword" smtp_port = 465 smtp_tls = "Tls" mail_from = "key-submission@example.org" -mail_subject = "Please confirm to %a your public key" \ No newline at end of file +mail_subject = "Please confirm to %a your public key" + +[policy] +key_max_validity = 0 diff --git a/example.policy.toml b/example.policy.toml new file mode 100644 index 0000000..150bb9d --- /dev/null +++ b/example.policy.toml @@ -0,0 +1,51 @@ +[hash_algorithms] +md5.collision_resistance = "never" +md5.second_preimage_resistance = "never" +sha1.collision_resistance = "never" +sha1.second_preimage_resistance = "never" +ripemd160.collision_resistance = "never" +ripemd160.second_preimage_resistance = "never" +sha224.collision_resistance = "always" +sha224.second_preimage_resistance = "always" +sha256.collision_resistance = "always" +sha256.second_preimage_resistance = "always" +sha384.collision_resistance = "always" +sha384.second_preimage_resistance = "always" +sha512.collision_resistance = "always" +sha512.second_preimage_resistance = "always" +default_disposition = "never" + +[symmetric_algorithms] +idea = "never" +tripledes = "never" +cast5 = "never" +blowfish = "never" +aes128 = "always" +aes192 = "never" +aes256 = "always" +twofish = "never" +camellia128 = "never" +camellia192 = "never" +camellia256 = "never" +default_disposition = "never" + +[asymmetric_algorithms] +elgamal1024 = "never" +elgamal2048 = "never" +elgamal3072 = "never" +elgamal4096 = "never" +brainpoolp256 = "never" +brainpoolp512 = "never" +rsa1024 = "never" +rsa2048 = "always" +rsa3072 = "always" +rsa4096 = "always" +dsa1024 = "never" +dsa2048 = "never" +dsa3072 = "never" +dsa4096 = "never" +nistp256 = "always" +nistp384 = "always" +nistp521 = "always" +cv25519 = "always" +default_disposition = "never"