[pbs-devel] [PATCH proxmox-backup 09/12] auth: move to hmac keys for csrf tokens

Max Carrara m.carrara at proxmox.com
Mon Feb 19 19:55:20 CET 2024


On 2/15/24 16:19, Stefan Sterz wrote:
> previously we used a self-rolled implementation for csrf tokens. while
> it's unlikely to cause issues in reality, as csrf tokens are only
> valid for a given tickets lifetime, there are still theoretical
> attacks on our implementation. so move all of this code into the
> proxmox-auth-api crate and use hmac instead.
> 
> this change should not impact existing installations for now, as this
> falls back to the old implementation if a key is already present. hmac
> keys will only be used for new installations and if users manually
> remove the old key and
> 
> Signed-off-by: Stefan Sterz <s.sterz at proxmox.com>
> ---
> note that the fallbacks here and in `proxmox-auth-api` should be removed
> with the next (major) version if possible. [...]

As mentioned in my reply to patch 04, we should somehow ensure that this
removed with some kind of compile time check or similar, so we *really*
don't miss it.

> [...] simply removing the csrf key
> before restarting the services should be enough here. it is possible
> that this will break some sessions, possibly forcing a user to log back
> in. so only do this when we can assume that an admin would have a
> reasonable expectation that something like this will happen (i.e.:
> possibly document this in the upgrade notes etc.).
> 
>  src/auth.rs         | 10 ++---
>  src/auth_helpers.rs | 97 ++++++++++-----------------------------------
>  2 files changed, 26 insertions(+), 81 deletions(-)
> 
> diff --git a/src/auth.rs b/src/auth.rs
> index ec0bc41f..c89314f5 100644
> --- a/src/auth.rs
> +++ b/src/auth.rs
> @@ -15,7 +15,7 @@ use serde_json::json;
>  use proxmox_auth_api::api::{Authenticator, LockedTfaConfig};
>  use proxmox_auth_api::ticket::{Empty, Ticket};
>  use proxmox_auth_api::types::Authid;
> -use proxmox_auth_api::Keyring;
> +use proxmox_auth_api::{HMACKey, Keyring};
>  use proxmox_ldap::{Config, Connection, ConnectionMode};
>  use proxmox_tfa::api::{OpenUserChallengeData, TfaConfig};
> 
> @@ -267,7 +267,7 @@ pub fn setup_auth_context(use_private_key: bool) {
>      AUTH_CONTEXT
>          .set(PbsAuthContext {
>              keyring,
> -            csrf_secret: crate::auth_helpers::csrf_secret().to_vec(),
> +            csrf_secret: crate::auth_helpers::csrf_secret(),
>          })
>          .map_err(drop)
>          .expect("auth context setup twice");
> @@ -285,7 +285,7 @@ pub(crate) fn public_auth_keyring() -> &'static Keyring {
> 
>  struct PbsAuthContext {
>      keyring: &'static Keyring,
> -    csrf_secret: Vec<u8>,
> +    csrf_secret: &'static HMACKey,
>  }
> 
>  impl proxmox_auth_api::api::AuthContext for PbsAuthContext {
> @@ -327,8 +327,8 @@ impl proxmox_auth_api::api::AuthContext for PbsAuthContext {
>      }
> 
>      /// CSRF prevention token secret data.
> -    fn csrf_secret(&self) -> &[u8] {
> -        &self.csrf_secret
> +    fn csrf_secret(&self) -> &'static HMACKey {
> +        self.csrf_secret
>      }
> 
>      /// Verify a token secret.
> diff --git a/src/auth_helpers.rs b/src/auth_helpers.rs
> index c2eaaef1..1a483d84 100644
> --- a/src/auth_helpers.rs
> +++ b/src/auth_helpers.rs
> @@ -1,81 +1,20 @@
>  use std::path::PathBuf;
> +use std::sync::OnceLock;
> 
> -use anyhow::{bail, format_err, Error};
> +use anyhow::Error;
>  use lazy_static::lazy_static;
>  use openssl::pkey::{PKey, Private, Public};
>  use openssl::rsa::Rsa;
> -use openssl::sha;
> 
>  use pbs_config::BackupLockGuard;
> -use proxmox_lang::try_block;
> +use proxmox_auth_api::HMACKey;
>  use proxmox_sys::fs::{file_get_contents, replace_file, CreateOptions};
> 
> -use pbs_api_types::Userid;
>  use pbs_buildcfg::configdir;
>  use serde_json::json;
> 
>  pub use crate::auth::setup_auth_context;
> -
> -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<i64, Error> {
> -    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 use proxmox_auth_api::api::assemble_csrf_prevention_token;
> 
>  pub fn generate_csrf_key() -> Result<(), Error> {
>      let path = PathBuf::from(configdir!("/csrf.key"));
> @@ -84,17 +23,14 @@ pub fn generate_csrf_key() -> Result<(), Error> {
>          return Ok(());
>      }
> 
> -    let rsa = Rsa::generate(2048).unwrap();
> -
> -    let pem = rsa.private_key_to_pem()?;
> +    let key = HMACKey::generate()?.to_base64()?;
> 
>      use nix::sys::stat::Mode;
> -
>      let backup_user = pbs_config::backup_user()?;
> 
>      replace_file(
>          &path,
> -        &pem,
> +        key.as_bytes(),
>          CreateOptions::new()
>              .perm(Mode::from_bits_truncate(0o0640))
>              .owner(nix::unistd::ROOT)
> @@ -145,12 +81,21 @@ pub fn generate_auth_key() -> Result<(), Error> {
>      Ok(())
>  }
> 
> -pub fn csrf_secret() -> &'static [u8] {
> -    lazy_static! {
> -        static ref SECRET: Vec<u8> = file_get_contents(configdir!("/csrf.key")).unwrap();
> -    }
> -
> -    &SECRET
> +pub fn csrf_secret() -> &'static HMACKey {
> +    static SECRET: OnceLock<HMACKey> = OnceLock::new();

Nice that we can use a `OnceLock` here!

> +
> +    SECRET.get_or_init(|| {
> +        let bytes = file_get_contents(configdir!("/csrf.key")).unwrap();
> +        std::str::from_utf8(&bytes)
> +            .map_err(anyhow::Error::new)
> +            .and_then(HMACKey::from_base64)
> +            // legacy fall back to load legacy csrf secrets
> +            // TODO: remove once we move away from legacy token verification

See my comment above for the TODO up there.

> +            .unwrap_or_else(|_| {
> +                let key_as_b64 = base64::encode_config(bytes, base64::STANDARD_NO_PAD);
> +                HMACKey::from_base64(&key_as_b64).unwrap()
> +            })
> +    })
>  }
> 
>  fn load_public_auth_key() -> Result<PKey<Public>, Error> {
> --
> 2.39.2
> 
> 
> 
> _______________________________________________
> pbs-devel mailing list
> pbs-devel at lists.proxmox.com
> https://lists.proxmox.com/cgi-bin/mailman/listinfo/pbs-devel
> 
> 





More information about the pbs-devel mailing list