diff --git a/src/api2/access.rs b/src/api2/access.rs index 6ad70e6e..81666bda 100644 --- a/src/api2/access.rs +++ b/src/api2/access.rs @@ -7,8 +7,7 @@ use proxmox::api::router::{Router, SubdirMap}; use proxmox::{sortable, identity}; use proxmox::{http_err, list_subdirs_api_method}; -use crate::tools; -use crate::tools::ticket::*; +use crate::tools::ticket::{self, Empty, Ticket}; use crate::auth_helpers::*; use crate::api2::types::*; @@ -35,27 +34,31 @@ fn authenticate_user( bail!("user account disabled or expired."); } - let ticket_lifetime = tools::ticket::TICKET_LIFETIME; - if password.starts_with("PBS:") { - if let Ok((_age, Some(ticket_username))) = tools::ticket::verify_rsa_ticket(public_auth_key(), "PBS", password, None, -300, ticket_lifetime) { - if *userid == ticket_username { + if let Ok(ticket_userid) = Ticket::::parse(password) + .and_then(|ticket| ticket.verify(public_auth_key(), "PBS", None)) + { + if *userid == ticket_userid { return Ok(true); - } else { - bail!("ticket login failed - wrong userid"); } + bail!("ticket login failed - wrong userid"); } } else if password.starts_with("PBSTERM:") { if path.is_none() || privs.is_none() || port.is_none() { bail!("cannot check termnal ticket without path, priv and port"); } - let path = path.unwrap(); - let privilege_name = privs.unwrap(); - let port = port.unwrap(); + let path = path.ok_or_else(|| format_err!("missing path for termproxy ticket"))?; + let privilege_name = privs + .ok_or_else(|| format_err!("missing privilege name for termproxy ticket"))?; + let port = port.ok_or_else(|| format_err!("missing port for termproxy ticket"))?; - if let Ok((_age, _data)) = - tools::ticket::verify_term_ticket(public_auth_key(), &userid, &path, port, password) + if let Ok(Empty) = Ticket::parse(password) + .and_then(|ticket| ticket.verify( + public_auth_key(), + ticket::TERM_PREFIX, + Some(&ticket::term_aad(userid, &path, port)), + )) { for (name, privilege) in PRIVILEGES { if *name == privilege_name { @@ -138,7 +141,7 @@ fn create_ticket( ) -> Result { match authenticate_user(&username, &password, path, privs, port) { Ok(true) => { - let ticket = assemble_rsa_ticket(private_auth_key(), "PBS", Some(&username), None)?; + let ticket = Ticket::new("PBS", &username)?.sign(private_auth_key(), None)?; let token = assemble_csrf_prevention_token(csrf_secret(), &username); diff --git a/src/api2/node.rs b/src/api2/node.rs index e9af5193..4689c494 100644 --- a/src/api2/node.rs +++ b/src/api2/node.rs @@ -22,6 +22,7 @@ use crate::api2::types::*; use crate::config::acl::PRIV_SYS_CONSOLE; use crate::server::WorkerTask; use crate::tools; +use crate::tools::ticket::{self, Empty, Ticket}; pub mod disks; pub mod dns; @@ -105,12 +106,11 @@ async fn termproxy( let listener = TcpListener::bind("localhost:0")?; let port = listener.local_addr()?.port(); - let ticket = tools::ticket::assemble_term_ticket( - crate::auth_helpers::private_auth_key(), - &userid, - &path, - port, - )?; + let ticket = Ticket::new(ticket::TERM_PREFIX, &Empty)? + .sign( + crate::auth_helpers::private_auth_key(), + Some(&ticket::term_aad(&userid, &path, port)), + )?; let mut command = Vec::new(); match cmd.as_ref().map(|x| x.as_str()) { @@ -273,17 +273,16 @@ fn upgrade_to_websocket( ) -> ApiResponseFuture { async move { let userid: Userid = rpcenv.get_user().unwrap().parse()?; - let ticket = tools::required_string_param(¶m, "vncticket")?.to_owned(); + let ticket = tools::required_string_param(¶m, "vncticket")?; let port: u16 = tools::required_integer_param(¶m, "port")? as u16; // will be checked again by termproxy - tools::ticket::verify_term_ticket( - crate::auth_helpers::public_auth_key(), - &userid, - &"/system", - port, - &ticket, - )?; + Ticket::::parse(ticket)? + .verify( + crate::auth_helpers::public_auth_key(), + ticket::TERM_PREFIX, + Some(&ticket::term_aad(&userid, "/system", port)), + )?; let (ws, response) = WebSocket::new(parts.headers)?; diff --git a/src/bin/proxmox-backup-manager.rs b/src/bin/proxmox-backup-manager.rs index 617a0a0e..16b8d702 100644 --- a/src/bin/proxmox-backup-manager.rs +++ b/src/bin/proxmox-backup-manager.rs @@ -9,7 +9,7 @@ use proxmox_backup::tools; use proxmox_backup::config; use proxmox_backup::api2::{self, types::* }; use proxmox_backup::client::*; -use proxmox_backup::tools::ticket::*; +use proxmox_backup::tools::ticket::Ticket; use proxmox_backup::auth_helpers::*; mod proxmox_backup_manager; @@ -59,12 +59,8 @@ fn connect() -> Result { .verify_cert(false); // not required for connection to localhost let client = if uid.is_root() { - let ticket = assemble_rsa_ticket( - private_auth_key(), - "PBS", - Some(Userid::root_userid()), - None, - )?; + let ticket = Ticket::new("PBS", Userid::root_userid())? + .sign(private_auth_key(), None)?; options = options.password(Some(ticket)); HttpClient::new("localhost", Userid::root_userid(), options)? } else { diff --git a/src/server/rest.rs b/src/server/rest.rs index 1e551be5..5ee788d2 100644 --- a/src/server/rest.rs +++ b/src/server/rest.rs @@ -29,6 +29,7 @@ use super::ApiConfig; use crate::auth_helpers::*; use crate::api2::types::Userid; use crate::tools; +use crate::tools::ticket::Ticket; use crate::config::cached_user_info::CachedUserInfo; extern "C" { fn tzset(); } @@ -463,17 +464,11 @@ fn check_auth( token: &Option, user_info: &CachedUserInfo, ) -> Result { - let ticket_lifetime = tools::ticket::TICKET_LIFETIME; - let userid = match ticket { - Some(ticket) => match tools::ticket::verify_rsa_ticket(public_auth_key(), "PBS", &ticket, None, -300, ticket_lifetime) { - Ok((_age, Some(userid))) => userid, - Ok((_, None)) => bail!("ticket without username."), - Err(err) => return Err(err), - } - None => bail!("missing ticket"), - }; + let ticket = ticket.as_ref().map(String::as_str); + let userid: Userid = Ticket::parse(&ticket.ok_or_else(|| format_err!("missing ticket"))?)? + .verify_with_time_frame(public_auth_key(), "PBS", None, -300..ticket_lifetime)?; if !user_info.is_active_user(&userid) { bail!("user account disabled or expired."); diff --git a/src/tools/ticket.rs b/src/tools/ticket.rs index e6c6442e..625c2051 100644 --- a/src/tools/ticket.rs +++ b/src/tools/ticket.rs @@ -7,7 +7,7 @@ use std::marker::PhantomData; use anyhow::{bail, format_err, Error}; use base64; -use openssl::pkey::{PKey, Public, Private, HasPublic}; +use openssl::pkey::{PKey, Private, HasPublic}; use openssl::sign::{Signer, Verifier}; use openssl::hash::MessageDigest; use percent_encoding::{AsciiSet, percent_decode_str, percent_encode}; @@ -266,7 +266,7 @@ pub fn term_aad(userid: &Userid, path: &str, port: u16) -> String { mod test { use openssl::pkey::{PKey, Private}; - use super::{Ticket, TICKET_LIFETIME}; + use super::Ticket; use crate::api2::types::Userid; use crate::tools::epoch_now_u64; @@ -288,13 +288,6 @@ mod test { .expect("failed to verify test ticket"); assert_eq!(*userid, check); - - // Compat check: - let (_age, uid) = - super::verify_rsa_ticket(key, "PREFIX", &ticket, aad, -300, TICKET_LIFETIME) - .expect("failed compatibility verification"); - let uid = uid.expect("compat did not return a userid"); - assert_eq!(*userid, uid); } else { parsed .verify(key, "PREFIX", aad) @@ -320,151 +313,5 @@ mod test { t.change_time(epoch_now_u64().unwrap() as i64 + 0x1000_0000); false }); - - // compat check: - let ticket = - super::assemble_rsa_ticket(&key, "PREFIX", Some(Userid::root_userid()), Some("stuff")) - .expect("failed to assemble compatibility ticket"); - let parsed_uid: Userid = Ticket::parse(&ticket) - .expect("failed to parse compatibility ticket") - .verify(&key, Some("stuff"), -300..TICKET_LIFETIME) - .expect("failed to verify compatibility ticket"); - assert_eq!(parsed_uid, *Userid::root_userid()); } } - -pub fn assemble_term_ticket( - keypair: &PKey, - userid: &Userid, - path: &str, - port: u16, -) -> Result { - assemble_rsa_ticket( - keypair, - TERM_PREFIX, - None, - Some(&format!("{}{}{}", userid, path, port)), - ) -} - -pub fn verify_term_ticket( - keypair: &PKey, - userid: &Userid, - path: &str, - port: u16, - ticket: &str, -) -> Result<(i64, Option), Error> { - verify_rsa_ticket( - keypair, - TERM_PREFIX, - ticket, - Some(&format!("{}{}{}", userid, path, port)), - -300, - TICKET_LIFETIME, - ) -} - -pub fn assemble_rsa_ticket( - keypair: &PKey, - prefix: &str, - data: Option<&Userid>, - secret_data: Option<&str>, -) -> Result { - - let epoch = epoch_now_u64()?; - - let timestamp = format!("{:08X}", epoch); - - let mut plain = prefix.to_owned(); - plain.push(':'); - - if let Some(data) = data { - use std::fmt::Write; - write!(plain, "{}", data)?; - plain.push(':'); - } - - plain.push_str(×tamp); - - let mut full = plain.clone(); - if let Some(secret) = secret_data { - full.push(':'); - full.push_str(secret); - } - - let mut signer = Signer::new(MessageDigest::sha256(), &keypair)?; - signer.update(full.as_bytes())?; - let sign = signer.sign_to_vec()?; - - let sign_b64 = base64::encode_config(&sign, base64::STANDARD_NO_PAD); - - Ok(format!("{}::{}", plain, sign_b64)) -} - -pub fn verify_rsa_ticket( - keypair: &PKey

, - prefix: &str, - ticket: &str, - secret_data: Option<&str>, - min_age: i64, - max_age: i64, -) -> Result<(i64, Option), Error> { - - use std::collections::VecDeque; - - let mut parts: VecDeque<&str> = ticket.split(':').collect(); - - match parts.pop_front() { - Some(text) => if text != prefix { bail!("ticket with invalid prefix"); } - None => bail!("ticket without prefix"), - } - - let sign_b64 = match parts.pop_back() { - Some(v) => v, - None => bail!("ticket without signature"), - }; - - match parts.pop_back() { - Some(text) => if text != "" { bail!("ticket with invalid signature separator"); } - None => bail!("ticket without signature separator"), - } - - let mut data = None; - - let mut full = match parts.len() { - 2 => { - data = Some(parts[0].to_owned()); - format!("{}:{}:{}", prefix, parts[0], parts[1]) - } - 1 => format!("{}:{}", prefix, parts[0]), - _ => bail!("ticket with invalid number of components"), - }; - - if let Some(secret) = secret_data { - full.push(':'); - full.push_str(secret); - } - - let sign = base64::decode_config(sign_b64, base64::STANDARD_NO_PAD)?; - - let mut verifier = Verifier::new(MessageDigest::sha256(), &keypair)?; - verifier.update(full.as_bytes())?; - - if !verifier.verify(&sign)? { - bail!("ticket with invalid signature"); - } - - let timestamp = i64::from_str_radix(parts.pop_back().unwrap(), 16)?; - let now = epoch_now_u64()? as i64; - - let age = now - timestamp; - if age < min_age { - bail!("invalid ticket - timestamp newer than expected."); - } - - if age > max_age { - bail!("invalid ticket - timestamp too old."); - } - - Ok((age, data.map(|s| s.parse()).transpose()?)) -}