//! Generate and verify Authentication tickets use std::borrow::Cow; use std::marker::PhantomData; use anyhow::{bail, format_err, Error}; use openssl::hash::MessageDigest; use percent_encoding::{percent_decode_str, percent_encode, AsciiSet}; use crate::auth_key::Keyring; use crate::TICKET_LIFETIME; /// 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 { 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 /// `::::`. /// /// 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 where T: ToString + std::str::FromStr, { prefix: Cow<'static, str>, data: String, time: i64, signature: Option>, _type_marker: PhantomData T>, } impl Ticket where T: ToString + std::str::FromStr, ::Err: std::fmt::Debug, { /// Prepare a new ticket for signing. pub fn new(prefix: &'static str, data: &T) -> Result { Ok(Self { prefix: Cow::Borrowed(prefix), data: data.to_string(), time: crate::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 string. fn ticket_data(&self) -> String { format!( "{}:{}:{:08X}", percent_encode(self.prefix.as_bytes(), TICKET_ASCIISET), percent_encode(self.data.as_bytes(), TICKET_ASCIISET), self.time, ) } /// Serialize the verification data. fn verification_data(&self, aad: Option<&str>) -> Vec { let mut data = self.ticket_data().into_bytes(); if let Some(aad) = aad { data.push(b':'); data.extend(aad.as_bytes()); } data } /// 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, keyring: &Keyring, aad: Option<&str>) -> Result { let mut output = self.ticket_data(); let mut signer = keyring.signer(MessageDigest::sha256())?; signer .update(output.as_bytes()) .map_err(Error::from) .and_then(|()| { if let Some(aad) = aad { signer .update(b":") .and_then(|()| signer.update(aad.as_bytes())) .map_err(Error::from) } else { Ok::<_, Error>(()) } }) .map_err(|err| format_err!("error signing ticket: {}", err))?; 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( &self, keyring: &Keyring, prefix: &str, aad: Option<&str>, time_frame: std::ops::Range, ) -> Result { if self.prefix != prefix { bail!("ticket with invalid prefix"); } let signature = match self.signature.as_deref() { Some(sig) => sig, None => bail!("invalid ticket without signature"), }; let age = crate::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 is_valid = keyring.verify( MessageDigest::sha256(), &signature, &self.verification_data(aad), )?; 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(&self, keyring: &Keyring, prefix: &str, aad: Option<&str>) -> Result { self.verify_with_time_frame(keyring, prefix, aad, -300..TICKET_LIFETIME) } /// Parse a ticket string. pub fn parse(ticket: &str) -> Result { 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"))?; // ::