replace and remove old ticket functions

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
Wolfgang Bumiller 2020-08-12 12:05:52 +02:00
parent 593f917742
commit 72dc68323c
5 changed files with 39 additions and 199 deletions

View File

@ -7,8 +7,7 @@ use proxmox::api::router::{Router, SubdirMap};
use proxmox::{sortable, identity}; use proxmox::{sortable, identity};
use proxmox::{http_err, list_subdirs_api_method}; use proxmox::{http_err, list_subdirs_api_method};
use crate::tools; use crate::tools::ticket::{self, Empty, Ticket};
use crate::tools::ticket::*;
use crate::auth_helpers::*; use crate::auth_helpers::*;
use crate::api2::types::*; use crate::api2::types::*;
@ -35,27 +34,31 @@ fn authenticate_user(
bail!("user account disabled or expired."); bail!("user account disabled or expired.");
} }
let ticket_lifetime = tools::ticket::TICKET_LIFETIME;
if password.starts_with("PBS:") { 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 let Ok(ticket_userid) = Ticket::<Userid>::parse(password)
if *userid == ticket_username { .and_then(|ticket| ticket.verify(public_auth_key(), "PBS", None))
{
if *userid == ticket_userid {
return Ok(true); return Ok(true);
} else {
bail!("ticket login failed - wrong userid");
} }
bail!("ticket login failed - wrong userid");
} }
} else if password.starts_with("PBSTERM:") { } else if password.starts_with("PBSTERM:") {
if path.is_none() || privs.is_none() || port.is_none() { if path.is_none() || privs.is_none() || port.is_none() {
bail!("cannot check termnal ticket without path, priv and port"); bail!("cannot check termnal ticket without path, priv and port");
} }
let path = path.unwrap(); let path = path.ok_or_else(|| format_err!("missing path for termproxy ticket"))?;
let privilege_name = privs.unwrap(); let privilege_name = privs
let port = port.unwrap(); .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)) = if let Ok(Empty) = Ticket::parse(password)
tools::ticket::verify_term_ticket(public_auth_key(), &userid, &path, port, password) .and_then(|ticket| ticket.verify(
public_auth_key(),
ticket::TERM_PREFIX,
Some(&ticket::term_aad(userid, &path, port)),
))
{ {
for (name, privilege) in PRIVILEGES { for (name, privilege) in PRIVILEGES {
if *name == privilege_name { if *name == privilege_name {
@ -138,7 +141,7 @@ fn create_ticket(
) -> Result<Value, Error> { ) -> Result<Value, Error> {
match authenticate_user(&username, &password, path, privs, port) { match authenticate_user(&username, &password, path, privs, port) {
Ok(true) => { 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); let token = assemble_csrf_prevention_token(csrf_secret(), &username);

View File

@ -22,6 +22,7 @@ use crate::api2::types::*;
use crate::config::acl::PRIV_SYS_CONSOLE; use crate::config::acl::PRIV_SYS_CONSOLE;
use crate::server::WorkerTask; use crate::server::WorkerTask;
use crate::tools; use crate::tools;
use crate::tools::ticket::{self, Empty, Ticket};
pub mod disks; pub mod disks;
pub mod dns; pub mod dns;
@ -105,12 +106,11 @@ async fn termproxy(
let listener = TcpListener::bind("localhost:0")?; let listener = TcpListener::bind("localhost:0")?;
let port = listener.local_addr()?.port(); let port = listener.local_addr()?.port();
let ticket = tools::ticket::assemble_term_ticket( let ticket = Ticket::new(ticket::TERM_PREFIX, &Empty)?
crate::auth_helpers::private_auth_key(), .sign(
&userid, crate::auth_helpers::private_auth_key(),
&path, Some(&ticket::term_aad(&userid, &path, port)),
port, )?;
)?;
let mut command = Vec::new(); let mut command = Vec::new();
match cmd.as_ref().map(|x| x.as_str()) { match cmd.as_ref().map(|x| x.as_str()) {
@ -273,17 +273,16 @@ fn upgrade_to_websocket(
) -> ApiResponseFuture { ) -> ApiResponseFuture {
async move { async move {
let userid: Userid = rpcenv.get_user().unwrap().parse()?; let userid: Userid = rpcenv.get_user().unwrap().parse()?;
let ticket = tools::required_string_param(&param, "vncticket")?.to_owned(); let ticket = tools::required_string_param(&param, "vncticket")?;
let port: u16 = tools::required_integer_param(&param, "port")? as u16; let port: u16 = tools::required_integer_param(&param, "port")? as u16;
// will be checked again by termproxy // will be checked again by termproxy
tools::ticket::verify_term_ticket( Ticket::<Empty>::parse(ticket)?
crate::auth_helpers::public_auth_key(), .verify(
&userid, crate::auth_helpers::public_auth_key(),
&"/system", ticket::TERM_PREFIX,
port, Some(&ticket::term_aad(&userid, "/system", port)),
&ticket, )?;
)?;
let (ws, response) = WebSocket::new(parts.headers)?; let (ws, response) = WebSocket::new(parts.headers)?;

View File

@ -9,7 +9,7 @@ use proxmox_backup::tools;
use proxmox_backup::config; use proxmox_backup::config;
use proxmox_backup::api2::{self, types::* }; use proxmox_backup::api2::{self, types::* };
use proxmox_backup::client::*; use proxmox_backup::client::*;
use proxmox_backup::tools::ticket::*; use proxmox_backup::tools::ticket::Ticket;
use proxmox_backup::auth_helpers::*; use proxmox_backup::auth_helpers::*;
mod proxmox_backup_manager; mod proxmox_backup_manager;
@ -59,12 +59,8 @@ fn connect() -> Result<HttpClient, Error> {
.verify_cert(false); // not required for connection to localhost .verify_cert(false); // not required for connection to localhost
let client = if uid.is_root() { let client = if uid.is_root() {
let ticket = assemble_rsa_ticket( let ticket = Ticket::new("PBS", Userid::root_userid())?
private_auth_key(), .sign(private_auth_key(), None)?;
"PBS",
Some(Userid::root_userid()),
None,
)?;
options = options.password(Some(ticket)); options = options.password(Some(ticket));
HttpClient::new("localhost", Userid::root_userid(), options)? HttpClient::new("localhost", Userid::root_userid(), options)?
} else { } else {

View File

@ -29,6 +29,7 @@ use super::ApiConfig;
use crate::auth_helpers::*; use crate::auth_helpers::*;
use crate::api2::types::Userid; use crate::api2::types::Userid;
use crate::tools; use crate::tools;
use crate::tools::ticket::Ticket;
use crate::config::cached_user_info::CachedUserInfo; use crate::config::cached_user_info::CachedUserInfo;
extern "C" { fn tzset(); } extern "C" { fn tzset(); }
@ -463,17 +464,11 @@ fn check_auth(
token: &Option<String>, token: &Option<String>,
user_info: &CachedUserInfo, user_info: &CachedUserInfo,
) -> Result<Userid, Error> { ) -> Result<Userid, Error> {
let ticket_lifetime = tools::ticket::TICKET_LIFETIME; let ticket_lifetime = tools::ticket::TICKET_LIFETIME;
let userid = match ticket { let ticket = ticket.as_ref().map(String::as_str);
Some(ticket) => match tools::ticket::verify_rsa_ticket(public_auth_key(), "PBS", &ticket, None, -300, ticket_lifetime) { let userid: Userid = Ticket::parse(&ticket.ok_or_else(|| format_err!("missing ticket"))?)?
Ok((_age, Some(userid))) => userid, .verify_with_time_frame(public_auth_key(), "PBS", None, -300..ticket_lifetime)?;
Ok((_, None)) => bail!("ticket without username."),
Err(err) => return Err(err),
}
None => bail!("missing ticket"),
};
if !user_info.is_active_user(&userid) { if !user_info.is_active_user(&userid) {
bail!("user account disabled or expired."); bail!("user account disabled or expired.");

View File

@ -7,7 +7,7 @@ use std::marker::PhantomData;
use anyhow::{bail, format_err, Error}; use anyhow::{bail, format_err, Error};
use base64; use base64;
use openssl::pkey::{PKey, Public, Private, HasPublic}; use openssl::pkey::{PKey, Private, HasPublic};
use openssl::sign::{Signer, Verifier}; use openssl::sign::{Signer, Verifier};
use openssl::hash::MessageDigest; use openssl::hash::MessageDigest;
use percent_encoding::{AsciiSet, percent_decode_str, percent_encode}; 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 { mod test {
use openssl::pkey::{PKey, Private}; use openssl::pkey::{PKey, Private};
use super::{Ticket, TICKET_LIFETIME}; use super::Ticket;
use crate::api2::types::Userid; use crate::api2::types::Userid;
use crate::tools::epoch_now_u64; use crate::tools::epoch_now_u64;
@ -288,13 +288,6 @@ mod test {
.expect("failed to verify test ticket"); .expect("failed to verify test ticket");
assert_eq!(*userid, check); 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 { } else {
parsed parsed
.verify(key, "PREFIX", aad) .verify(key, "PREFIX", aad)
@ -320,151 +313,5 @@ mod test {
t.change_time(epoch_now_u64().unwrap() as i64 + 0x1000_0000); t.change_time(epoch_now_u64().unwrap() as i64 + 0x1000_0000);
false 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<Private>,
userid: &Userid,
path: &str,
port: u16,
) -> Result<String, Error> {
assemble_rsa_ticket(
keypair,
TERM_PREFIX,
None,
Some(&format!("{}{}{}", userid, path, port)),
)
}
pub fn verify_term_ticket(
keypair: &PKey<Public>,
userid: &Userid,
path: &str,
port: u16,
ticket: &str,
) -> Result<(i64, Option<Userid>), Error> {
verify_rsa_ticket(
keypair,
TERM_PREFIX,
ticket,
Some(&format!("{}{}{}", userid, path, port)),
-300,
TICKET_LIFETIME,
)
}
pub fn assemble_rsa_ticket(
keypair: &PKey<Private>,
prefix: &str,
data: Option<&Userid>,
secret_data: Option<&str>,
) -> Result<String, Error> {
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(&timestamp);
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<P: HasPublic>(
keypair: &PKey<P>,
prefix: &str,
ticket: &str,
secret_data: Option<&str>,
min_age: i64,
max_age: i64,
) -> Result<(i64, Option<Userid>), 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()?))
}