mirror of
				https://git.proxmox.com/git/proxmox-backup
				synced 2025-11-02 15:18:42 +00:00 
			
		
		
		
	move more helpers to pbs-tools
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
		
							parent
							
								
									b9c5cd8291
								
							
						
					
					
						commit
						9eb784076c
					
				@ -8,12 +8,15 @@ description = "common tools used throughout pbs"
 | 
			
		||||
# This must not depend on any subcrates more closely related to pbs itself.
 | 
			
		||||
[dependencies]
 | 
			
		||||
anyhow = "1.0"
 | 
			
		||||
base64 = "0.12"
 | 
			
		||||
libc = "0.2"
 | 
			
		||||
nix = "0.19.1"
 | 
			
		||||
nom = "5.1"
 | 
			
		||||
openssl = "0.10"
 | 
			
		||||
percent-encoding = "2.1"
 | 
			
		||||
regex = "1.2"
 | 
			
		||||
serde = "1.0"
 | 
			
		||||
serde_json = "1.0"
 | 
			
		||||
url = "2.1"
 | 
			
		||||
 | 
			
		||||
proxmox = { version = "0.11.5", default-features = false, features = [] }
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
use anyhow::{bail, Error};
 | 
			
		||||
use anyhow::{bail, format_err, Error};
 | 
			
		||||
use serde_json::Value;
 | 
			
		||||
 | 
			
		||||
// Generate canonical json
 | 
			
		||||
@ -47,3 +47,46 @@ pub fn write_canonical_json(value: &Value, output: &mut Vec<u8>) -> Result<(), E
 | 
			
		||||
    }
 | 
			
		||||
    Ok(())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn json_object_to_query(data: Value) -> Result<String, Error> {
 | 
			
		||||
    let mut query = url::form_urlencoded::Serializer::new(String::new());
 | 
			
		||||
 | 
			
		||||
    let object = data.as_object().ok_or_else(|| {
 | 
			
		||||
        format_err!("json_object_to_query: got wrong data type (expected object).")
 | 
			
		||||
    })?;
 | 
			
		||||
 | 
			
		||||
    for (key, value) in object {
 | 
			
		||||
        match value {
 | 
			
		||||
            Value::Bool(b) => {
 | 
			
		||||
                query.append_pair(key, &b.to_string());
 | 
			
		||||
            }
 | 
			
		||||
            Value::Number(n) => {
 | 
			
		||||
                query.append_pair(key, &n.to_string());
 | 
			
		||||
            }
 | 
			
		||||
            Value::String(s) => {
 | 
			
		||||
                query.append_pair(key, &s);
 | 
			
		||||
            }
 | 
			
		||||
            Value::Array(arr) => {
 | 
			
		||||
                for element in arr {
 | 
			
		||||
                    match element {
 | 
			
		||||
                        Value::Bool(b) => {
 | 
			
		||||
                            query.append_pair(key, &b.to_string());
 | 
			
		||||
                        }
 | 
			
		||||
                        Value::Number(n) => {
 | 
			
		||||
                            query.append_pair(key, &n.to_string());
 | 
			
		||||
                        }
 | 
			
		||||
                        Value::String(s) => {
 | 
			
		||||
                            query.append_pair(key, &s);
 | 
			
		||||
                        }
 | 
			
		||||
                        _ => bail!(
 | 
			
		||||
                            "json_object_to_query: unable to handle complex array data types."
 | 
			
		||||
                        ),
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            _ => bail!("json_object_to_query: unable to handle complex data types."),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(query.finish())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -4,8 +4,9 @@ pub mod fs;
 | 
			
		||||
pub mod json;
 | 
			
		||||
pub mod nom;
 | 
			
		||||
pub mod process_locker;
 | 
			
		||||
pub mod str;
 | 
			
		||||
pub mod sha;
 | 
			
		||||
pub mod str;
 | 
			
		||||
pub mod ticket;
 | 
			
		||||
 | 
			
		||||
mod command;
 | 
			
		||||
pub use command::{command_output, command_output_as_string, run_command};
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										332
									
								
								pbs-tools/src/ticket.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										332
									
								
								pbs-tools/src/ticket.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,332 @@
 | 
			
		||||
//! Generate and verify Authentication tickets
 | 
			
		||||
 | 
			
		||||
use std::borrow::Cow;
 | 
			
		||||
use std::io;
 | 
			
		||||
use std::marker::PhantomData;
 | 
			
		||||
 | 
			
		||||
use anyhow::{bail, format_err, Error};
 | 
			
		||||
use openssl::hash::MessageDigest;
 | 
			
		||||
use openssl::pkey::{HasPublic, PKey, Private};
 | 
			
		||||
use openssl::sign::{Signer, Verifier};
 | 
			
		||||
use percent_encoding::{percent_decode_str, percent_encode, AsciiSet};
 | 
			
		||||
 | 
			
		||||
pub const TICKET_LIFETIME: i64 = 3600 * 2; // 2 hours
 | 
			
		||||
 | 
			
		||||
pub const TERM_PREFIX: &str = "PBSTERM";
 | 
			
		||||
 | 
			
		||||
/// Stringified ticket data must not contain colons...
 | 
			
		||||
const TICKET_ASCIISET: &AsciiSet = &percent_encoding::CONTROLS.add(b':');
 | 
			
		||||
 | 
			
		||||
/// An empty type implementing [`ToString`] and [`FromStr`](std::str::FromStr), used for tickets
 | 
			
		||||
/// with no data.
 | 
			
		||||
pub struct Empty;
 | 
			
		||||
 | 
			
		||||
impl ToString for Empty {
 | 
			
		||||
    fn to_string(&self) -> String {
 | 
			
		||||
        String::new()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl std::str::FromStr for Empty {
 | 
			
		||||
    type Err = Error;
 | 
			
		||||
 | 
			
		||||
    fn from_str(s: &str) -> Result<Self, Error> {
 | 
			
		||||
        if !s.is_empty() {
 | 
			
		||||
            bail!("unexpected ticket data, should be empty");
 | 
			
		||||
        }
 | 
			
		||||
        Ok(Empty)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// An API ticket consists of a ticket type (prefix), type-dependent data, optional additional
 | 
			
		||||
/// authenticaztion data, a timestamp and a signature. We store these values in the form
 | 
			
		||||
/// `<prefix>:<stringified data>:<timestamp>::<signature>`.
 | 
			
		||||
///
 | 
			
		||||
/// The signature is made over the string consisting of prefix, data, timestamp and aad joined
 | 
			
		||||
/// together by colons. If there is no additional authentication data it will be skipped together
 | 
			
		||||
/// with the colon separating it from the timestamp.
 | 
			
		||||
pub struct Ticket<T>
 | 
			
		||||
where
 | 
			
		||||
    T: ToString + std::str::FromStr,
 | 
			
		||||
{
 | 
			
		||||
    prefix: Cow<'static, str>,
 | 
			
		||||
    data: String,
 | 
			
		||||
    time: i64,
 | 
			
		||||
    signature: Option<Vec<u8>>,
 | 
			
		||||
    _type_marker: PhantomData<T>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<T> Ticket<T>
 | 
			
		||||
where
 | 
			
		||||
    T: ToString + std::str::FromStr,
 | 
			
		||||
    <T as std::str::FromStr>::Err: std::fmt::Debug,
 | 
			
		||||
{
 | 
			
		||||
    /// Prepare a new ticket for signing.
 | 
			
		||||
    pub fn new(prefix: &'static str, data: &T) -> Result<Self, Error> {
 | 
			
		||||
        Ok(Self {
 | 
			
		||||
            prefix: Cow::Borrowed(prefix),
 | 
			
		||||
            data: data.to_string(),
 | 
			
		||||
            time: proxmox::tools::time::epoch_i64(),
 | 
			
		||||
            signature: None,
 | 
			
		||||
            _type_marker: PhantomData,
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Get the ticket prefix.
 | 
			
		||||
    pub fn prefix(&self) -> &str {
 | 
			
		||||
        &self.prefix
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Get the ticket's time stamp in seconds since the unix epoch.
 | 
			
		||||
    pub fn time(&self) -> i64 {
 | 
			
		||||
        self.time
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Get the raw string data contained in the ticket. The `verify` method will call `parse()`
 | 
			
		||||
    /// this in the end, so using this method directly is discouraged as it does not verify the
 | 
			
		||||
    /// signature.
 | 
			
		||||
    pub fn raw_data(&self) -> &str {
 | 
			
		||||
        &self.data
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Serialize the ticket into a writer.
 | 
			
		||||
    ///
 | 
			
		||||
    /// This only writes a string. We use `io::write` instead of `fmt::Write` so we can reuse the
 | 
			
		||||
    /// same function for openssl's `Verify`, which only implements `io::Write`.
 | 
			
		||||
    fn write_data(&self, f: &mut dyn io::Write) -> Result<(), Error> {
 | 
			
		||||
        write!(
 | 
			
		||||
            f,
 | 
			
		||||
            "{}:{}:{:08X}",
 | 
			
		||||
            percent_encode(self.prefix.as_bytes(), &TICKET_ASCIISET),
 | 
			
		||||
            percent_encode(self.data.as_bytes(), &TICKET_ASCIISET),
 | 
			
		||||
            self.time,
 | 
			
		||||
        )
 | 
			
		||||
        .map_err(Error::from)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Write additional authentication data to the verifier.
 | 
			
		||||
    fn write_aad(f: &mut dyn io::Write, aad: Option<&str>) -> Result<(), Error> {
 | 
			
		||||
        if let Some(aad) = aad {
 | 
			
		||||
            write!(f, ":{}", percent_encode(aad.as_bytes(), &TICKET_ASCIISET))?;
 | 
			
		||||
        }
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Change the ticket's time, used mostly for testing.
 | 
			
		||||
    #[cfg(test)]
 | 
			
		||||
    fn change_time(&mut self, time: i64) -> &mut Self {
 | 
			
		||||
        self.time = time;
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Sign the ticket.
 | 
			
		||||
    pub fn sign(&mut self, keypair: &PKey<Private>, aad: Option<&str>) -> Result<String, Error> {
 | 
			
		||||
        let mut output = Vec::<u8>::new();
 | 
			
		||||
        let mut signer = Signer::new(MessageDigest::sha256(), &keypair)
 | 
			
		||||
            .map_err(|err| format_err!("openssl error creating signer for ticket: {}", err))?;
 | 
			
		||||
 | 
			
		||||
        self.write_data(&mut output)
 | 
			
		||||
            .map_err(|err| format_err!("error creating ticket: {}", err))?;
 | 
			
		||||
 | 
			
		||||
        signer
 | 
			
		||||
            .update(&output)
 | 
			
		||||
            .map_err(Error::from)
 | 
			
		||||
            .and_then(|()| Self::write_aad(&mut signer, aad))
 | 
			
		||||
            .map_err(|err| format_err!("error signing ticket: {}", err))?;
 | 
			
		||||
 | 
			
		||||
        // See `Self::write_data` for why this is safe
 | 
			
		||||
        let mut output = unsafe { String::from_utf8_unchecked(output) };
 | 
			
		||||
 | 
			
		||||
        let signature = signer
 | 
			
		||||
            .sign_to_vec()
 | 
			
		||||
            .map_err(|err| format_err!("error finishing ticket signature: {}", err))?;
 | 
			
		||||
 | 
			
		||||
        use std::fmt::Write;
 | 
			
		||||
        write!(
 | 
			
		||||
            &mut output,
 | 
			
		||||
            "::{}",
 | 
			
		||||
            base64::encode_config(&signature, base64::STANDARD_NO_PAD),
 | 
			
		||||
        )?;
 | 
			
		||||
 | 
			
		||||
        self.signature = Some(signature);
 | 
			
		||||
 | 
			
		||||
        Ok(output)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// `verify` with an additional time frame parameter, not usually required since we always use
 | 
			
		||||
    /// the same time frame.
 | 
			
		||||
    pub fn verify_with_time_frame<P: HasPublic>(
 | 
			
		||||
        &self,
 | 
			
		||||
        keypair: &PKey<P>,
 | 
			
		||||
        prefix: &str,
 | 
			
		||||
        aad: Option<&str>,
 | 
			
		||||
        time_frame: std::ops::Range<i64>,
 | 
			
		||||
    ) -> Result<T, Error> {
 | 
			
		||||
        if self.prefix != prefix {
 | 
			
		||||
            bail!("ticket with invalid prefix");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let signature = match self.signature.as_ref() {
 | 
			
		||||
            Some(sig) => sig,
 | 
			
		||||
            None => bail!("invalid ticket without signature"),
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let age = proxmox::tools::time::epoch_i64() - self.time;
 | 
			
		||||
        if age < time_frame.start {
 | 
			
		||||
            bail!("invalid ticket - timestamp newer than expected");
 | 
			
		||||
        }
 | 
			
		||||
        if age > time_frame.end {
 | 
			
		||||
            bail!("invalid ticket - expired");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let mut verifier = Verifier::new(MessageDigest::sha256(), &keypair)?;
 | 
			
		||||
 | 
			
		||||
        self.write_data(&mut verifier)
 | 
			
		||||
            .and_then(|()| Self::write_aad(&mut verifier, aad))
 | 
			
		||||
            .map_err(|err| format_err!("error verifying ticket: {}", err))?;
 | 
			
		||||
 | 
			
		||||
        let is_valid: bool = verifier
 | 
			
		||||
            .verify(&signature)
 | 
			
		||||
            .map_err(|err| format_err!("openssl error verifying ticket: {}", err))?;
 | 
			
		||||
 | 
			
		||||
        if !is_valid {
 | 
			
		||||
            bail!("ticket with invalid signature");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        self.data
 | 
			
		||||
            .parse()
 | 
			
		||||
            .map_err(|err| format_err!("failed to parse contained ticket data: {:?}", err))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Verify the ticket with the provided key pair. The additional authentication data needs to
 | 
			
		||||
    /// match the one used when generating the ticket, and the ticket's age must fall into the time
 | 
			
		||||
    /// frame.
 | 
			
		||||
    pub fn verify<P: HasPublic>(
 | 
			
		||||
        &self,
 | 
			
		||||
        keypair: &PKey<P>,
 | 
			
		||||
        prefix: &str,
 | 
			
		||||
        aad: Option<&str>,
 | 
			
		||||
    ) -> Result<T, Error> {
 | 
			
		||||
        self.verify_with_time_frame(keypair, prefix, aad, -300..TICKET_LIFETIME)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Parse a ticket string.
 | 
			
		||||
    pub fn parse(ticket: &str) -> Result<Self, Error> {
 | 
			
		||||
        let mut parts = ticket.splitn(4, ':');
 | 
			
		||||
 | 
			
		||||
        let prefix = percent_decode_str(
 | 
			
		||||
            parts
 | 
			
		||||
                .next()
 | 
			
		||||
                .ok_or_else(|| format_err!("ticket without prefix"))?,
 | 
			
		||||
        )
 | 
			
		||||
        .decode_utf8()
 | 
			
		||||
        .map_err(|err| format_err!("invalid ticket, error decoding prefix: {}", err))?;
 | 
			
		||||
 | 
			
		||||
        let data = percent_decode_str(
 | 
			
		||||
            parts
 | 
			
		||||
                .next()
 | 
			
		||||
                .ok_or_else(|| format_err!("ticket without data"))?,
 | 
			
		||||
        )
 | 
			
		||||
        .decode_utf8()
 | 
			
		||||
        .map_err(|err| format_err!("invalid ticket, error decoding data: {}", err))?;
 | 
			
		||||
 | 
			
		||||
        let time = i64::from_str_radix(
 | 
			
		||||
            parts
 | 
			
		||||
                .next()
 | 
			
		||||
                .ok_or_else(|| format_err!("ticket without timestamp"))?,
 | 
			
		||||
            16,
 | 
			
		||||
        )
 | 
			
		||||
        .map_err(|err| format_err!("ticket with bad timestamp: {}", err))?;
 | 
			
		||||
 | 
			
		||||
        let remainder = parts
 | 
			
		||||
            .next()
 | 
			
		||||
            .ok_or_else(|| format_err!("ticket without signature"))?;
 | 
			
		||||
        // <prefix>:<data>:<time>::signature - the 4th `.next()` swallows the first colon in the
 | 
			
		||||
        // double-colon!
 | 
			
		||||
        if !remainder.starts_with(':') {
 | 
			
		||||
            bail!("ticket without signature separator");
 | 
			
		||||
        }
 | 
			
		||||
        let signature = base64::decode_config(&remainder[1..], base64::STANDARD_NO_PAD)
 | 
			
		||||
            .map_err(|err| format_err!("ticket with bad signature: {}", err))?;
 | 
			
		||||
 | 
			
		||||
        Ok(Self {
 | 
			
		||||
            prefix: Cow::Owned(prefix.into_owned()),
 | 
			
		||||
            data: data.into_owned(),
 | 
			
		||||
            time,
 | 
			
		||||
            signature: Some(signature),
 | 
			
		||||
            _type_marker: PhantomData,
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod test {
 | 
			
		||||
    use std::convert::Infallible;
 | 
			
		||||
    use std::fmt;
 | 
			
		||||
 | 
			
		||||
    use openssl::pkey::{PKey, Private};
 | 
			
		||||
 | 
			
		||||
    use super::Ticket;
 | 
			
		||||
 | 
			
		||||
    #[derive(Debug, Eq, PartialEq)]
 | 
			
		||||
    struct Testid(String);
 | 
			
		||||
 | 
			
		||||
    impl std::str::FromStr for Testid {
 | 
			
		||||
        type Err = Infallible;
 | 
			
		||||
 | 
			
		||||
        fn from_str(s: &str) -> Result<Self, Infallible> {
 | 
			
		||||
            Ok(Self(s.to_string()))
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    impl fmt::Display for Testid {
 | 
			
		||||
        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
 | 
			
		||||
            write!(f, "{}", self.0)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn simple_test<F>(key: &PKey<Private>, aad: Option<&str>, modify: F)
 | 
			
		||||
    where
 | 
			
		||||
        F: FnOnce(&mut Ticket<Testid>) -> bool,
 | 
			
		||||
    {
 | 
			
		||||
        let userid = Testid("root".to_string());
 | 
			
		||||
 | 
			
		||||
        let mut ticket = Ticket::new("PREFIX", &userid).expect("failed to create Ticket struct");
 | 
			
		||||
        let should_work = modify(&mut ticket);
 | 
			
		||||
        let ticket = ticket.sign(key, aad).expect("failed to sign test ticket");
 | 
			
		||||
 | 
			
		||||
        let parsed =
 | 
			
		||||
            Ticket::<Testid>::parse(&ticket).expect("failed to parse generated test ticket");
 | 
			
		||||
        if should_work {
 | 
			
		||||
            let check: Testid = parsed
 | 
			
		||||
                .verify(key, "PREFIX", aad)
 | 
			
		||||
                .expect("failed to verify test ticket");
 | 
			
		||||
 | 
			
		||||
            assert_eq!(userid, check);
 | 
			
		||||
        } else {
 | 
			
		||||
            parsed
 | 
			
		||||
                .verify(key, "PREFIX", aad)
 | 
			
		||||
                .expect_err("failed to verify test ticket");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_tickets() {
 | 
			
		||||
        // first we need keys, for testing we use small keys for speed...
 | 
			
		||||
        let rsa =
 | 
			
		||||
            openssl::rsa::Rsa::generate(1024).expect("failed to generate RSA key for testing");
 | 
			
		||||
        let key = openssl::pkey::PKey::<openssl::pkey::Private>::from_rsa(rsa)
 | 
			
		||||
            .expect("failed to create PKey for RSA key");
 | 
			
		||||
 | 
			
		||||
        simple_test(&key, Some("secret aad data"), |_| true);
 | 
			
		||||
        simple_test(&key, None, |_| true);
 | 
			
		||||
        simple_test(&key, None, |t| {
 | 
			
		||||
            t.change_time(0);
 | 
			
		||||
            false
 | 
			
		||||
        });
 | 
			
		||||
        simple_test(&key, None, |t| {
 | 
			
		||||
            t.change_time(proxmox::tools::time::epoch_i64() + 0x1000_0000);
 | 
			
		||||
            false
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@ -11,10 +11,11 @@ use proxmox::api::{api, Permission, RpcEnvironment};
 | 
			
		||||
use proxmox::{http_err, list_subdirs_api_method};
 | 
			
		||||
use proxmox::{identity, sortable};
 | 
			
		||||
 | 
			
		||||
use pbs_tools::ticket::{self, Empty, Ticket};
 | 
			
		||||
 | 
			
		||||
use crate::api2::types::*;
 | 
			
		||||
use crate::auth_helpers::*;
 | 
			
		||||
use crate::server::ticket::ApiTicket;
 | 
			
		||||
use crate::tools::ticket::{self, Empty, Ticket};
 | 
			
		||||
 | 
			
		||||
use crate::config::acl as acl_config;
 | 
			
		||||
use crate::config::acl::{PRIVILEGES, PRIV_PERMISSIONS_MODIFY, PRIV_SYS_AUDIT};
 | 
			
		||||
@ -84,7 +85,7 @@ fn authenticate_user(
 | 
			
		||||
            ticket.verify(
 | 
			
		||||
                public_auth_key(),
 | 
			
		||||
                ticket::TERM_PREFIX,
 | 
			
		||||
                Some(&ticket::term_aad(userid, &path, port)),
 | 
			
		||||
                Some(&crate::tools::ticket::term_aad(userid, &path, port)),
 | 
			
		||||
            )
 | 
			
		||||
        }) {
 | 
			
		||||
            for (name, privilege) in PRIVILEGES {
 | 
			
		||||
 | 
			
		||||
@ -14,9 +14,9 @@ use proxmox::tools::fs::open_file_locked;
 | 
			
		||||
use proxmox_openid::{OpenIdAuthenticator,  OpenIdConfig};
 | 
			
		||||
 | 
			
		||||
use pbs_buildcfg::PROXMOX_BACKUP_RUN_DIR_M;
 | 
			
		||||
use pbs_tools::ticket::Ticket;
 | 
			
		||||
 | 
			
		||||
use crate::server::ticket::ApiTicket;
 | 
			
		||||
use crate::tools::ticket::Ticket;
 | 
			
		||||
 | 
			
		||||
use crate::config::domains::{OpenIdUserAttribute, OpenIdRealmConfig};
 | 
			
		||||
use crate::config::cached_user_info::CachedUserInfo;
 | 
			
		||||
 | 
			
		||||
@ -20,11 +20,12 @@ use proxmox::list_subdirs_api_method;
 | 
			
		||||
use proxmox_http::websocket::WebSocket;
 | 
			
		||||
use proxmox::{identity, sortable};
 | 
			
		||||
 | 
			
		||||
use pbs_tools::ticket::{self, Empty, Ticket};
 | 
			
		||||
 | 
			
		||||
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 apt;
 | 
			
		||||
pub mod certificates;
 | 
			
		||||
@ -121,7 +122,7 @@ async fn termproxy(
 | 
			
		||||
    let ticket = Ticket::new(ticket::TERM_PREFIX, &Empty)?
 | 
			
		||||
        .sign(
 | 
			
		||||
            crate::auth_helpers::private_auth_key(),
 | 
			
		||||
            Some(&ticket::term_aad(&userid, &path, port)),
 | 
			
		||||
            Some(&tools::ticket::term_aad(&userid, &path, port)),
 | 
			
		||||
        )?;
 | 
			
		||||
 | 
			
		||||
    let mut command = Vec::new();
 | 
			
		||||
@ -294,7 +295,7 @@ fn upgrade_to_websocket(
 | 
			
		||||
            .verify(
 | 
			
		||||
                crate::auth_helpers::public_auth_key(),
 | 
			
		||||
                ticket::TERM_PREFIX,
 | 
			
		||||
                Some(&ticket::term_aad(&userid, "/system", port)),
 | 
			
		||||
                Some(&tools::ticket::term_aad(&userid, "/system", port)),
 | 
			
		||||
            )?;
 | 
			
		||||
 | 
			
		||||
        let (ws, response) = WebSocket::new(parts.headers.clone())?;
 | 
			
		||||
 | 
			
		||||
@ -12,9 +12,10 @@ use proxmox::{
 | 
			
		||||
 | 
			
		||||
use pbs_api_types::{BACKUP_REPO_URL, Authid};
 | 
			
		||||
use pbs_buildcfg;
 | 
			
		||||
use pbs_datastore::BackupDir;
 | 
			
		||||
use pbs_tools::json::json_object_to_query;
 | 
			
		||||
 | 
			
		||||
use proxmox_backup::api2::access::user::UserWithTokens;
 | 
			
		||||
use proxmox_backup::backup::BackupDir;
 | 
			
		||||
use proxmox_backup::client::{BackupRepository, HttpClient, HttpClientOptions};
 | 
			
		||||
use proxmox_backup::tools;
 | 
			
		||||
 | 
			
		||||
@ -210,7 +211,7 @@ pub async fn complete_server_file_name_do(param: &HashMap<String, String>) -> Ve
 | 
			
		||||
        _ => return result,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    let query = tools::json_object_to_query(json!({
 | 
			
		||||
    let query = json_object_to_query(json!({
 | 
			
		||||
        "backup-type": snapshot.group().backup_type(),
 | 
			
		||||
        "backup-id": snapshot.group().backup_id(),
 | 
			
		||||
        "backup-time": snapshot.backup_time(),
 | 
			
		||||
 | 
			
		||||
@ -24,10 +24,11 @@ use proxmox_http::client::HttpsConnector;
 | 
			
		||||
use proxmox_http::uri::build_authority;
 | 
			
		||||
 | 
			
		||||
use pbs_api_types::{Authid, Userid};
 | 
			
		||||
use pbs_tools::json::json_object_to_query;
 | 
			
		||||
use pbs_tools::ticket;
 | 
			
		||||
 | 
			
		||||
use super::pipe_to_stream::PipeToSendStream;
 | 
			
		||||
use crate::tools::{
 | 
			
		||||
    self,
 | 
			
		||||
    BroadcastFuture,
 | 
			
		||||
    DEFAULT_ENCODE_SET,
 | 
			
		||||
    PROXMOX_BACKUP_TCP_KEEPALIVE_TIME,
 | 
			
		||||
@ -237,7 +238,7 @@ fn store_ticket_info(prefix: &str, server: &str, username: &str, ticket: &str, t
 | 
			
		||||
 | 
			
		||||
    let mut new_data = json!({});
 | 
			
		||||
 | 
			
		||||
    let ticket_lifetime = tools::ticket::TICKET_LIFETIME - 60;
 | 
			
		||||
    let ticket_lifetime = ticket::TICKET_LIFETIME - 60;
 | 
			
		||||
 | 
			
		||||
    let empty = serde_json::map::Map::new();
 | 
			
		||||
    for (server, info) in data.as_object().unwrap_or(&empty) {
 | 
			
		||||
@ -263,7 +264,7 @@ fn load_ticket_info(prefix: &str, server: &str, userid: &Userid) -> Option<(Stri
 | 
			
		||||
    let path = base.place_runtime_file("tickets").ok()?;
 | 
			
		||||
    let data = file_get_json(&path, None).ok()?;
 | 
			
		||||
    let now = proxmox::tools::time::epoch_i64();
 | 
			
		||||
    let ticket_lifetime = tools::ticket::TICKET_LIFETIME - 60;
 | 
			
		||||
    let ticket_lifetime = ticket::TICKET_LIFETIME - 60;
 | 
			
		||||
    let uinfo = data[server][userid.as_str()].as_object()?;
 | 
			
		||||
    let timestamp = uinfo["timestamp"].as_i64()?;
 | 
			
		||||
    let age = now - timestamp;
 | 
			
		||||
@ -641,7 +642,7 @@ impl HttpClient {
 | 
			
		||||
    ) -> Result<Value, Error> {
 | 
			
		||||
 | 
			
		||||
        let query = match data {
 | 
			
		||||
            Some(data) => Some(tools::json_object_to_query(data)?),
 | 
			
		||||
            Some(data) => Some(json_object_to_query(data)?),
 | 
			
		||||
            None => None,
 | 
			
		||||
        };
 | 
			
		||||
        let url = build_uri(&self.server, self.port, path, query)?;
 | 
			
		||||
@ -789,7 +790,7 @@ impl HttpClient {
 | 
			
		||||
                    .body(Body::from(data.to_string()))?;
 | 
			
		||||
                Ok(request)
 | 
			
		||||
            } else {
 | 
			
		||||
                let query = tools::json_object_to_query(data)?;
 | 
			
		||||
                let query = json_object_to_query(data)?;
 | 
			
		||||
                let url = build_uri(server, port, path, Some(query))?;
 | 
			
		||||
                let request = Request::builder()
 | 
			
		||||
                    .method(method)
 | 
			
		||||
@ -992,7 +993,7 @@ impl H2Client {
 | 
			
		||||
        let content_type = content_type.unwrap_or("application/x-www-form-urlencoded");
 | 
			
		||||
        let query = match param {
 | 
			
		||||
            Some(param) => {
 | 
			
		||||
                let query = tools::json_object_to_query(param)?;
 | 
			
		||||
                let query = json_object_to_query(param)?;
 | 
			
		||||
                // We detected problem with hyper around 6000 characters - so we try to keep on the safe side
 | 
			
		||||
                if query.len() > 4096 {
 | 
			
		||||
                    bail!("h2 query data too large ({} bytes) - please encode data inside body", query.len());
 | 
			
		||||
 | 
			
		||||
@ -5,14 +5,14 @@
 | 
			
		||||
 | 
			
		||||
use anyhow::Error;
 | 
			
		||||
 | 
			
		||||
use pbs_api_types::{Authid, Userid};
 | 
			
		||||
use pbs_tools::ticket::Ticket;
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
    api2::types::{Userid, Authid},
 | 
			
		||||
    tools::ticket::Ticket,
 | 
			
		||||
    tools::cert::CertInfo,
 | 
			
		||||
    auth_helpers::private_auth_key,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
mod merge_known_chunks;
 | 
			
		||||
pub mod pipe_to_stream;
 | 
			
		||||
 | 
			
		||||
@ -53,7 +53,7 @@ pub fn connect_to_localhost() -> Result<HttpClient, Error> {
 | 
			
		||||
    let client = if uid.is_root()  {
 | 
			
		||||
        let ticket = Ticket::new("PBS", Userid::root_userid())?
 | 
			
		||||
            .sign(private_auth_key(), None)?;
 | 
			
		||||
        let fingerprint = crate::tools::cert::CertInfo::new()?.fingerprint()?;
 | 
			
		||||
        let fingerprint = CertInfo::new()?.fingerprint()?;
 | 
			
		||||
        let options = HttpClientOptions::new_non_interactive(ticket, Some(fingerprint));
 | 
			
		||||
 | 
			
		||||
        HttpClient::new("localhost", 8007, Authid::root_auth_id(), options)?
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,6 @@ use serde_json::Value;
 | 
			
		||||
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, ReadBuf};
 | 
			
		||||
use tokio::net::UnixStream;
 | 
			
		||||
 | 
			
		||||
use crate::tools;
 | 
			
		||||
use proxmox::api::error::HttpError;
 | 
			
		||||
 | 
			
		||||
pub const DEFAULT_VSOCK_PORT: u16 = 807;
 | 
			
		||||
@ -242,7 +241,7 @@ impl VsockClient {
 | 
			
		||||
                let request = builder.body(Body::from(data.to_string()))?;
 | 
			
		||||
                return Ok(request);
 | 
			
		||||
            } else {
 | 
			
		||||
                let query = tools::json_object_to_query(data)?;
 | 
			
		||||
                let query = pbs_tools::json::json_object_to_query(data)?;
 | 
			
		||||
                let url: Uri =
 | 
			
		||||
                    format!("vsock://{}:{}/{}?{}", self.cid, self.port, path, query).parse()?;
 | 
			
		||||
                let builder = make_builder("application/x-www-form-urlencoded", &url);
 | 
			
		||||
 | 
			
		||||
@ -3,11 +3,12 @@ use anyhow::{format_err, Error};
 | 
			
		||||
 | 
			
		||||
use std::sync::Arc;
 | 
			
		||||
 | 
			
		||||
use pbs_tools::ticket::{self, Ticket};
 | 
			
		||||
 | 
			
		||||
use crate::api2::types::{Authid, Userid};
 | 
			
		||||
use crate::auth_helpers::*;
 | 
			
		||||
use crate::config::cached_user_info::CachedUserInfo;
 | 
			
		||||
use crate::tools;
 | 
			
		||||
use crate::tools::ticket::Ticket;
 | 
			
		||||
 | 
			
		||||
use hyper::header;
 | 
			
		||||
use percent_encoding::percent_decode_str;
 | 
			
		||||
@ -85,7 +86,7 @@ impl ApiAuth for UserApiAuth {
 | 
			
		||||
        match auth_data {
 | 
			
		||||
            Some(AuthData::User(user_auth_data)) => {
 | 
			
		||||
                let ticket = user_auth_data.ticket.clone();
 | 
			
		||||
                let ticket_lifetime = tools::ticket::TICKET_LIFETIME;
 | 
			
		||||
                let ticket_lifetime = ticket::TICKET_LIFETIME;
 | 
			
		||||
 | 
			
		||||
                let userid: Userid = Ticket::<super::ticket::ApiTicket>::parse(&ticket)?
 | 
			
		||||
                    .verify_with_time_frame(public_auth_key(), "PBS", None, -300..ticket_lifetime)?
 | 
			
		||||
 | 
			
		||||
@ -89,49 +89,6 @@ pub trait BufferedRead {
 | 
			
		||||
    fn buffered_read(&mut self, offset: u64) -> Result<&[u8], Error>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn json_object_to_query(data: Value) -> Result<String, Error> {
 | 
			
		||||
    let mut query = url::form_urlencoded::Serializer::new(String::new());
 | 
			
		||||
 | 
			
		||||
    let object = data.as_object().ok_or_else(|| {
 | 
			
		||||
        format_err!("json_object_to_query: got wrong data type (expected object).")
 | 
			
		||||
    })?;
 | 
			
		||||
 | 
			
		||||
    for (key, value) in object {
 | 
			
		||||
        match value {
 | 
			
		||||
            Value::Bool(b) => {
 | 
			
		||||
                query.append_pair(key, &b.to_string());
 | 
			
		||||
            }
 | 
			
		||||
            Value::Number(n) => {
 | 
			
		||||
                query.append_pair(key, &n.to_string());
 | 
			
		||||
            }
 | 
			
		||||
            Value::String(s) => {
 | 
			
		||||
                query.append_pair(key, &s);
 | 
			
		||||
            }
 | 
			
		||||
            Value::Array(arr) => {
 | 
			
		||||
                for element in arr {
 | 
			
		||||
                    match element {
 | 
			
		||||
                        Value::Bool(b) => {
 | 
			
		||||
                            query.append_pair(key, &b.to_string());
 | 
			
		||||
                        }
 | 
			
		||||
                        Value::Number(n) => {
 | 
			
		||||
                            query.append_pair(key, &n.to_string());
 | 
			
		||||
                        }
 | 
			
		||||
                        Value::String(s) => {
 | 
			
		||||
                            query.append_pair(key, &s);
 | 
			
		||||
                        }
 | 
			
		||||
                        _ => bail!(
 | 
			
		||||
                            "json_object_to_query: unable to handle complex array data types."
 | 
			
		||||
                        ),
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            _ => bail!("json_object_to_query: unable to handle complex data types."),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Ok(query.finish())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub fn required_string_param<'a>(param: &'a Value, name: &str) -> Result<&'a str, Error> {
 | 
			
		||||
    match param[name].as_str() {
 | 
			
		||||
        Some(s) => Ok(s),
 | 
			
		||||
 | 
			
		||||
@ -1,18 +1,21 @@
 | 
			
		||||
use anyhow::{Error, format_err, bail};
 | 
			
		||||
use lazy_static::lazy_static;
 | 
			
		||||
use serde_json::json;
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use regex::Regex;
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
use serde_json::json;
 | 
			
		||||
 | 
			
		||||
use proxmox::api::api;
 | 
			
		||||
 | 
			
		||||
use proxmox::tools::fs::{replace_file, CreateOptions};
 | 
			
		||||
use proxmox_http::client::SimpleHttp;
 | 
			
		||||
 | 
			
		||||
use pbs_tools::json::json_object_to_query;
 | 
			
		||||
 | 
			
		||||
use crate::config::node;
 | 
			
		||||
use crate::tools::{
 | 
			
		||||
    self,
 | 
			
		||||
    pbs_simple_http,
 | 
			
		||||
};
 | 
			
		||||
use proxmox::tools::fs::{replace_file, CreateOptions};
 | 
			
		||||
use proxmox_http::client::SimpleHttp;
 | 
			
		||||
 | 
			
		||||
/// How long the local key is valid for in between remote checks
 | 
			
		||||
pub const MAX_LOCAL_KEY_AGE: i64 = 15 * 24 * 3600;
 | 
			
		||||
@ -116,7 +119,7 @@ async fn register_subscription(
 | 
			
		||||
    let mut client = pbs_simple_http(proxy_config);
 | 
			
		||||
 | 
			
		||||
    let uri = "https://shop.maurer-it.com/modules/servers/licensing/verify.php";
 | 
			
		||||
    let query = tools::json_object_to_query(params)?;
 | 
			
		||||
    let query = json_object_to_query(params)?;
 | 
			
		||||
    let response = client.post(uri, Some(query), Some("application/x-www-form-urlencoded")).await?;
 | 
			
		||||
    let body = SimpleHttp::response_body_string(response).await?;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,319 +1,5 @@
 | 
			
		||||
//! Generate and verify Authentication tickets
 | 
			
		||||
 | 
			
		||||
use std::borrow::Cow;
 | 
			
		||||
use std::io;
 | 
			
		||||
use std::marker::PhantomData;
 | 
			
		||||
 | 
			
		||||
use anyhow::{bail, format_err, Error};
 | 
			
		||||
use openssl::hash::MessageDigest;
 | 
			
		||||
use openssl::pkey::{HasPublic, PKey, Private};
 | 
			
		||||
use openssl::sign::{Signer, Verifier};
 | 
			
		||||
use percent_encoding::{percent_decode_str, percent_encode, AsciiSet};
 | 
			
		||||
 | 
			
		||||
use crate::api2::types::Userid;
 | 
			
		||||
 | 
			
		||||
pub const TICKET_LIFETIME: i64 = 3600 * 2; // 2 hours
 | 
			
		||||
 | 
			
		||||
pub const TERM_PREFIX: &str = "PBSTERM";
 | 
			
		||||
 | 
			
		||||
/// Stringified ticket data must not contain colons...
 | 
			
		||||
const TICKET_ASCIISET: &AsciiSet = &percent_encoding::CONTROLS.add(b':');
 | 
			
		||||
 | 
			
		||||
/// An empty type implementing [`ToString`] and [`FromStr`](std::str::FromStr), used for tickets
 | 
			
		||||
/// with no data.
 | 
			
		||||
pub struct Empty;
 | 
			
		||||
 | 
			
		||||
impl ToString for Empty {
 | 
			
		||||
    fn to_string(&self) -> String {
 | 
			
		||||
        String::new()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl std::str::FromStr for Empty {
 | 
			
		||||
    type Err = Error;
 | 
			
		||||
 | 
			
		||||
    fn from_str(s: &str) -> Result<Self, Error> {
 | 
			
		||||
        if !s.is_empty() {
 | 
			
		||||
            bail!("unexpected ticket data, should be empty");
 | 
			
		||||
        }
 | 
			
		||||
        Ok(Empty)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// An API ticket consists of a ticket type (prefix), type-dependent data, optional additional
 | 
			
		||||
/// authenticaztion data, a timestamp and a signature. We store these values in the form
 | 
			
		||||
/// `<prefix>:<stringified data>:<timestamp>::<signature>`.
 | 
			
		||||
///
 | 
			
		||||
/// The signature is made over the string consisting of prefix, data, timestamp and aad joined
 | 
			
		||||
/// together by colons. If there is no additional authentication data it will be skipped together
 | 
			
		||||
/// with the colon separating it from the timestamp.
 | 
			
		||||
pub struct Ticket<T>
 | 
			
		||||
where
 | 
			
		||||
    T: ToString + std::str::FromStr,
 | 
			
		||||
{
 | 
			
		||||
    prefix: Cow<'static, str>,
 | 
			
		||||
    data: String,
 | 
			
		||||
    time: i64,
 | 
			
		||||
    signature: Option<Vec<u8>>,
 | 
			
		||||
    _type_marker: PhantomData<T>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<T> Ticket<T>
 | 
			
		||||
where
 | 
			
		||||
    T: ToString + std::str::FromStr,
 | 
			
		||||
    <T as std::str::FromStr>::Err: std::fmt::Debug,
 | 
			
		||||
{
 | 
			
		||||
    /// Prepare a new ticket for signing.
 | 
			
		||||
    pub fn new(prefix: &'static str, data: &T) -> Result<Self, Error> {
 | 
			
		||||
        Ok(Self {
 | 
			
		||||
            prefix: Cow::Borrowed(prefix),
 | 
			
		||||
            data: data.to_string(),
 | 
			
		||||
            time: proxmox::tools::time::epoch_i64(),
 | 
			
		||||
            signature: None,
 | 
			
		||||
            _type_marker: PhantomData,
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Get the ticket prefix.
 | 
			
		||||
    pub fn prefix(&self) -> &str {
 | 
			
		||||
        &self.prefix
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Get the ticket's time stamp in seconds since the unix epoch.
 | 
			
		||||
    pub fn time(&self) -> i64 {
 | 
			
		||||
        self.time
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Get the raw string data contained in the ticket. The `verify` method will call `parse()`
 | 
			
		||||
    /// this in the end, so using this method directly is discouraged as it does not verify the
 | 
			
		||||
    /// signature.
 | 
			
		||||
    pub fn raw_data(&self) -> &str {
 | 
			
		||||
        &self.data
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Serialize the ticket into a writer.
 | 
			
		||||
    ///
 | 
			
		||||
    /// This only writes a string. We use `io::write` instead of `fmt::Write` so we can reuse the
 | 
			
		||||
    /// same function for openssl's `Verify`, which only implements `io::Write`.
 | 
			
		||||
    fn write_data(&self, f: &mut dyn io::Write) -> Result<(), Error> {
 | 
			
		||||
        write!(
 | 
			
		||||
            f,
 | 
			
		||||
            "{}:{}:{:08X}",
 | 
			
		||||
            percent_encode(self.prefix.as_bytes(), &TICKET_ASCIISET),
 | 
			
		||||
            percent_encode(self.data.as_bytes(), &TICKET_ASCIISET),
 | 
			
		||||
            self.time,
 | 
			
		||||
        )
 | 
			
		||||
        .map_err(Error::from)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Write additional authentication data to the verifier.
 | 
			
		||||
    fn write_aad(f: &mut dyn io::Write, aad: Option<&str>) -> Result<(), Error> {
 | 
			
		||||
        if let Some(aad) = aad {
 | 
			
		||||
            write!(f, ":{}", percent_encode(aad.as_bytes(), &TICKET_ASCIISET))?;
 | 
			
		||||
        }
 | 
			
		||||
        Ok(())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Change the ticket's time, used mostly for testing.
 | 
			
		||||
    #[cfg(test)]
 | 
			
		||||
    fn change_time(&mut self, time: i64) -> &mut Self {
 | 
			
		||||
        self.time = time;
 | 
			
		||||
        self
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Sign the ticket.
 | 
			
		||||
    pub fn sign(&mut self, keypair: &PKey<Private>, aad: Option<&str>) -> Result<String, Error> {
 | 
			
		||||
        let mut output = Vec::<u8>::new();
 | 
			
		||||
        let mut signer = Signer::new(MessageDigest::sha256(), &keypair)
 | 
			
		||||
            .map_err(|err| format_err!("openssl error creating signer for ticket: {}", err))?;
 | 
			
		||||
 | 
			
		||||
        self.write_data(&mut output)
 | 
			
		||||
            .map_err(|err| format_err!("error creating ticket: {}", err))?;
 | 
			
		||||
 | 
			
		||||
        signer
 | 
			
		||||
            .update(&output)
 | 
			
		||||
            .map_err(Error::from)
 | 
			
		||||
            .and_then(|()| Self::write_aad(&mut signer, aad))
 | 
			
		||||
            .map_err(|err| format_err!("error signing ticket: {}", err))?;
 | 
			
		||||
 | 
			
		||||
        // See `Self::write_data` for why this is safe
 | 
			
		||||
        let mut output = unsafe { String::from_utf8_unchecked(output) };
 | 
			
		||||
 | 
			
		||||
        let signature = signer
 | 
			
		||||
            .sign_to_vec()
 | 
			
		||||
            .map_err(|err| format_err!("error finishing ticket signature: {}", err))?;
 | 
			
		||||
 | 
			
		||||
        use std::fmt::Write;
 | 
			
		||||
        write!(
 | 
			
		||||
            &mut output,
 | 
			
		||||
            "::{}",
 | 
			
		||||
            base64::encode_config(&signature, base64::STANDARD_NO_PAD),
 | 
			
		||||
        )?;
 | 
			
		||||
 | 
			
		||||
        self.signature = Some(signature);
 | 
			
		||||
 | 
			
		||||
        Ok(output)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// `verify` with an additional time frame parameter, not usually required since we always use
 | 
			
		||||
    /// the same time frame.
 | 
			
		||||
    pub fn verify_with_time_frame<P: HasPublic>(
 | 
			
		||||
        &self,
 | 
			
		||||
        keypair: &PKey<P>,
 | 
			
		||||
        prefix: &str,
 | 
			
		||||
        aad: Option<&str>,
 | 
			
		||||
        time_frame: std::ops::Range<i64>,
 | 
			
		||||
    ) -> Result<T, Error> {
 | 
			
		||||
        if self.prefix != prefix {
 | 
			
		||||
            bail!("ticket with invalid prefix");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let signature = match self.signature.as_ref() {
 | 
			
		||||
            Some(sig) => sig,
 | 
			
		||||
            None => bail!("invalid ticket without signature"),
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let age = proxmox::tools::time::epoch_i64() - self.time;
 | 
			
		||||
        if age < time_frame.start {
 | 
			
		||||
            bail!("invalid ticket - timestamp newer than expected");
 | 
			
		||||
        }
 | 
			
		||||
        if age > time_frame.end {
 | 
			
		||||
            bail!("invalid ticket - expired");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        let mut verifier = Verifier::new(MessageDigest::sha256(), &keypair)?;
 | 
			
		||||
 | 
			
		||||
        self.write_data(&mut verifier)
 | 
			
		||||
            .and_then(|()| Self::write_aad(&mut verifier, aad))
 | 
			
		||||
            .map_err(|err| format_err!("error verifying ticket: {}", err))?;
 | 
			
		||||
 | 
			
		||||
        let is_valid: bool = verifier
 | 
			
		||||
            .verify(&signature)
 | 
			
		||||
            .map_err(|err| format_err!("openssl error verifying ticket: {}", err))?;
 | 
			
		||||
 | 
			
		||||
        if !is_valid {
 | 
			
		||||
            bail!("ticket with invalid signature");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        self.data
 | 
			
		||||
            .parse()
 | 
			
		||||
            .map_err(|err| format_err!("failed to parse contained ticket data: {:?}", err))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Verify the ticket with the provided key pair. The additional authentication data needs to
 | 
			
		||||
    /// match the one used when generating the ticket, and the ticket's age must fall into the time
 | 
			
		||||
    /// frame.
 | 
			
		||||
    pub fn verify<P: HasPublic>(
 | 
			
		||||
        &self,
 | 
			
		||||
        keypair: &PKey<P>,
 | 
			
		||||
        prefix: &str,
 | 
			
		||||
        aad: Option<&str>,
 | 
			
		||||
    ) -> Result<T, Error> {
 | 
			
		||||
        self.verify_with_time_frame(keypair, prefix, aad, -300..TICKET_LIFETIME)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// Parse a ticket string.
 | 
			
		||||
    pub fn parse(ticket: &str) -> Result<Self, Error> {
 | 
			
		||||
        let mut parts = ticket.splitn(4, ':');
 | 
			
		||||
 | 
			
		||||
        let prefix = percent_decode_str(
 | 
			
		||||
            parts
 | 
			
		||||
                .next()
 | 
			
		||||
                .ok_or_else(|| format_err!("ticket without prefix"))?,
 | 
			
		||||
        )
 | 
			
		||||
        .decode_utf8()
 | 
			
		||||
        .map_err(|err| format_err!("invalid ticket, error decoding prefix: {}", err))?;
 | 
			
		||||
 | 
			
		||||
        let data = percent_decode_str(
 | 
			
		||||
            parts
 | 
			
		||||
                .next()
 | 
			
		||||
                .ok_or_else(|| format_err!("ticket without data"))?,
 | 
			
		||||
        )
 | 
			
		||||
        .decode_utf8()
 | 
			
		||||
        .map_err(|err| format_err!("invalid ticket, error decoding data: {}", err))?;
 | 
			
		||||
 | 
			
		||||
        let time = i64::from_str_radix(
 | 
			
		||||
            parts
 | 
			
		||||
                .next()
 | 
			
		||||
                .ok_or_else(|| format_err!("ticket without timestamp"))?,
 | 
			
		||||
            16,
 | 
			
		||||
        )
 | 
			
		||||
        .map_err(|err| format_err!("ticket with bad timestamp: {}", err))?;
 | 
			
		||||
 | 
			
		||||
        let remainder = parts
 | 
			
		||||
            .next()
 | 
			
		||||
            .ok_or_else(|| format_err!("ticket without signature"))?;
 | 
			
		||||
        // <prefix>:<data>:<time>::signature - the 4th `.next()` swallows the first colon in the
 | 
			
		||||
        // double-colon!
 | 
			
		||||
        if !remainder.starts_with(':') {
 | 
			
		||||
            bail!("ticket without signature separator");
 | 
			
		||||
        }
 | 
			
		||||
        let signature = base64::decode_config(&remainder[1..], base64::STANDARD_NO_PAD)
 | 
			
		||||
            .map_err(|err| format_err!("ticket with bad signature: {}", err))?;
 | 
			
		||||
 | 
			
		||||
        Ok(Self {
 | 
			
		||||
            prefix: Cow::Owned(prefix.into_owned()),
 | 
			
		||||
            data: data.into_owned(),
 | 
			
		||||
            time,
 | 
			
		||||
            signature: Some(signature),
 | 
			
		||||
            _type_marker: PhantomData,
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
use pbs_api_types::Userid;
 | 
			
		||||
 | 
			
		||||
pub fn term_aad(userid: &Userid, path: &str, port: u16) -> String {
 | 
			
		||||
    format!("{}{}{}", userid, path, port)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[cfg(test)]
 | 
			
		||||
mod test {
 | 
			
		||||
    use openssl::pkey::{PKey, Private};
 | 
			
		||||
 | 
			
		||||
    use super::Ticket;
 | 
			
		||||
    use crate::api2::types::Userid;
 | 
			
		||||
 | 
			
		||||
    fn simple_test<F>(key: &PKey<Private>, aad: Option<&str>, modify: F)
 | 
			
		||||
    where
 | 
			
		||||
        F: FnOnce(&mut Ticket<Userid>) -> bool,
 | 
			
		||||
    {
 | 
			
		||||
        let userid = Userid::root_userid();
 | 
			
		||||
 | 
			
		||||
        let mut ticket = Ticket::new("PREFIX", userid).expect("failed to create Ticket struct");
 | 
			
		||||
        let should_work = modify(&mut ticket);
 | 
			
		||||
        let ticket = ticket.sign(key, aad).expect("failed to sign test ticket");
 | 
			
		||||
 | 
			
		||||
        let parsed =
 | 
			
		||||
            Ticket::<Userid>::parse(&ticket).expect("failed to parse generated test ticket");
 | 
			
		||||
        if should_work {
 | 
			
		||||
            let check: Userid = parsed
 | 
			
		||||
                .verify(key, "PREFIX", aad)
 | 
			
		||||
                .expect("failed to verify test ticket");
 | 
			
		||||
 | 
			
		||||
            assert_eq!(*userid, check);
 | 
			
		||||
        } else {
 | 
			
		||||
            parsed
 | 
			
		||||
                .verify(key, "PREFIX", aad)
 | 
			
		||||
                .expect_err("failed to verify test ticket");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[test]
 | 
			
		||||
    fn test_tickets() {
 | 
			
		||||
        // first we need keys, for testing we use small keys for speed...
 | 
			
		||||
        let rsa =
 | 
			
		||||
            openssl::rsa::Rsa::generate(1024).expect("failed to generate RSA key for testing");
 | 
			
		||||
        let key = openssl::pkey::PKey::<openssl::pkey::Private>::from_rsa(rsa)
 | 
			
		||||
            .expect("failed to create PKey for RSA key");
 | 
			
		||||
 | 
			
		||||
        simple_test(&key, Some("secret aad data"), |_| true);
 | 
			
		||||
        simple_test(&key, None, |_| true);
 | 
			
		||||
        simple_test(&key, None, |t| {
 | 
			
		||||
            t.change_time(0);
 | 
			
		||||
            false
 | 
			
		||||
        });
 | 
			
		||||
        simple_test(&key, None, |t| {
 | 
			
		||||
            t.change_time(proxmox::tools::time::epoch_i64() + 0x1000_0000);
 | 
			
		||||
            false
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user