]> git.proxmox.com Git - proxmox-backup.git/commitdiff
move more helpers to pbs-tools
authorWolfgang Bumiller <w.bumiller@proxmox.com>
Mon, 12 Jul 2021 09:07:52 +0000 (11:07 +0200)
committerWolfgang Bumiller <w.bumiller@proxmox.com>
Mon, 19 Jul 2021 08:07:12 +0000 (10:07 +0200)
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
15 files changed:
pbs-tools/Cargo.toml
pbs-tools/src/json.rs
pbs-tools/src/lib.rs
pbs-tools/src/ticket.rs [new file with mode: 0644]
src/api2/access/mod.rs
src/api2/access/openid.rs
src/api2/node/mod.rs
src/bin/proxmox_client_tools/mod.rs
src/client/http_client.rs
src/client/mod.rs
src/client/vsock_client.rs
src/server/auth.rs
src/tools/mod.rs
src/tools/subscription.rs
src/tools/ticket.rs

index 17ef91122750770b3c76a64105073e898d9a5cd8..a3aa81c1fef95013e9e79f6a0d6bc7565f3c9cc1 100644 (file)
@@ -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 = [] }
index 7a43c700685a2c6f625778cbe2a2c5e087aba415..6d7d923b8cf0c1379b10b808d60761fc08b597d8 100644 (file)
@@ -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())
+}
index 533ec1f13e75d83f0bd9366d23317dbf1bd9ad53..72b0e9fdce22ae873c12d9cf1e3629bd207447be 100644 (file)
@@ -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};
diff --git a/pbs-tools/src/ticket.rs b/pbs-tools/src/ticket.rs
new file mode 100644 (file)
index 0000000..b4e4161
--- /dev/null
@@ -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
+        });
+    }
+}
index 1cd772d6692058114a246f7f0dd3510088e850f1..c6bfbb9e86bb9788e917ddba5b55af3ce655d7a5 100644 (file)
@@ -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 {
index 39b4c43e25d4f100dd90948fc3d033b664446aef..a778aa2a58fc4c82bacab5abe417bd4ddbffb775 100644 (file)
@@ -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;
index af9ecf695e065f7878bb928de18637c59a334c4d..208cbf98c79f45223388aac187375b4f6f25fd8d 100644 (file)
@@ -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())?;
index 77c33ff0b614a39c9eced346782e84797770438e..a54abe060a200e017b2736b30928050b70473a5f 100644 (file)
@@ -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(),
index 0f3ec729a98fc7c1c9e78ee13dc6e556129a74e3..d19fa5c25a370afb00a01c5bfd7a1886f0c4b809 100644 (file)
@@ -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());
index 831d7a5a0f6c25ee93b06ce8e14b56a35535cd9e..8a4e45567ce317a49d624991334ba02ff69e6335 100644 (file)
@@ -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)?
index 5dd9eb4bf22afd4ab89c888190af2fe30a9801bf..d735b6ead296f79758d169264e22904805f5d054 100644 (file)
@@ -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);
index 0a9a740cfdcfe662fcab7ae80372f02b23a5462f..9fb5a204733f3a1e2958042bbb3a98293a51545d 100644 (file)
@@ -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)?
index 16f21091c454d29ee5b56e54937042afe28e083b..900f33c03146b5633ffd9bf5b4e05a56fcc55fde 100644 (file)
@@ -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),
index 1c0e379c6f4906b3eb51d9eb51369821528ec492..230d3aeb12ea7fd6e9f2328076403c61934c397f 100644 (file)
@@ -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?;
 
index 4462ee3500ed347b222f31746f4265891a956eb6..30a360ad89abad57a17218dc32199e5b1a65b362 100644 (file)
@@ -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
-        });
-    }
-}