# 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 = [] }
-use anyhow::{bail, Error};
+use anyhow::{bail, format_err, Error};
use serde_json::Value;
// Generate canonical json
}
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())
+}
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};
--- /dev/null
+//! 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
+ });
+ }
+}
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};
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 {
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;
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;
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();
.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())?;
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;
_ => 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(),
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,
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) {
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;
) -> 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)?;
.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)
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());
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;
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)?
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;
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);
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;
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)?
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),
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;
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?;
-//! 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
- });
- }
-}