use std::borrow::{Borrow, BorrowMut}; use hmac::{Hmac, KeyInit, Mac}; use sha2::Sha256; use cookie::{Cookie, CookieJar, Key}; // Keep these in sync, and keep the key len synced with the `signed` docs as // well as the `KEYS_INFO` const in secure::Key. pub(crate) const BASE64_DIGEST_LEN: usize = 44; pub(crate) const KEY_LEN: usize = 32; use base64::{prelude::BASE64_STANDARD, DecodeError, Engine}; use tokio::task; /// Encode `input` as the standard base64 with padding. pub(crate) fn encode>(input: T) -> String { BASE64_STANDARD.encode(input) } /// Decode `input` as the standard base64 with padding. pub(crate) fn decode>(input: T) -> Result, DecodeError> { BASE64_STANDARD.decode(input) } pub trait CookiesAdditionJar { fn message_signed<'a>(&'a self, key: &Key, message: String) -> AdditionalSignedJar<&'a Self>; fn message_signed_mut<'a>( &'a mut self, key: &Key, message: String, ) -> AdditionalSignedJar<&'a mut Self>; } impl CookiesAdditionJar for CookieJar { fn message_signed<'a>(&'a self, key: &Key, message: String) -> AdditionalSignedJar<&'a Self> { AdditionalSignedJar::new(self, key, message) } fn message_signed_mut<'a>( &'a mut self, key: &Key, message: String, ) -> AdditionalSignedJar<&'a mut Self> { AdditionalSignedJar::new(self, key, message) } } /// A child cookie jar that authenticates its cookies and Adds Additional measures to ensure integrity. pub struct AdditionalSignedJar { parent: J, key: [u8; KEY_LEN], message: String, } impl AdditionalSignedJar { /// Creates a new child `AdditionalSignedJar` pub(crate) fn new(parent: J, key: &Key, message: String) -> AdditionalSignedJar { AdditionalSignedJar { parent, key: key.signing().try_into().expect("sign key len"), message, } } /// Signs the cookie's value and message providing integrity and authenticity. async fn sign_cookie(&self, cookie: &mut Cookie<'_>) { let key = self.key.to_owned(); let message = self.message.to_owned(); let value = cookie.value().to_owned(); let new_value = task::spawn_blocking(move || { // Compute HMAC-SHA256 of the cookie's value. let mut mac = match Hmac::::new_from_slice(&key) { Ok(v) => v, Err(err) => { tracing::error!(err = %err, "key is invalid." ); return None; } }; // Add the payload to the message first. let message = format!("{}{}", value, message); mac.update(message.as_bytes()); // Cookie's new value is [MAC | original-value]. let mut new_value = encode(mac.finalize().into_bytes()); new_value.push_str(&value); Some(new_value) }) .await; if let Ok(Some(value)) = new_value { cookie.set_value(value); } } /// Given a signed value `str` where the signature is prepended to `value`, /// verifies the signed value and returns it. If there's a problem, returns /// an `Err` with a string describing the issue. async fn _verify(&self, cookie_value: &str) -> Result { if !cookie_value.is_char_boundary(BASE64_DIGEST_LEN) { return Err("missing or invalid digest"); } // we clone it so we can pass them to the blocking thread for encryption verification needs. let key = self.key.to_owned(); let message = self.message.to_owned(); let cookie_value = cookie_value.to_owned(); task::spawn_blocking(move || { // Split [MAC | original-value] into its two parts. let (digest_str, value) = cookie_value.split_at(BASE64_DIGEST_LEN); let digest = decode(digest_str).map_err(|_| "bad base64 digest")?; // Perform the verification. let mut mac = Hmac::::new_from_slice(&key).map_err(|_| "key is invalid.")?; // Add message here so we can check if it matches. let message = format!("{}{}", value, message); mac.update(message.as_bytes()); mac.verify_slice(&digest) .map(|_| value.to_string()) .map_err(|_| "value did not verify") }) .await .map_err(|_| "thread join in _verify was unsuccessful")? } /// Verifies the authenticity and integrity of `cookie`, returning the /// plaintext version if verification succeeds or `None` otherwise. /// Verification _always_ succeeds if `cookie` was generated by a /// `AdditionalSignedJar` with the same key and message as `self`. /// /// # Example /// /// ```rust ignore /// use cookie::{CookieJar, Cookie, Key}; /// /// let key = Key::generate(); /// let mut jar = CookieJar::new(); /// assert!(jar.message_signed(&key, "".to_owned()).get("name").is_none()); /// /// jar.message_signed_mut(&key, "".to_owned()).add(("name", "value")); /// assert_eq!(jar.message_signed(&key, "".to_owned()).get("name").unwrap().value(), "value"); /// /// let plain = jar.get("name").cloned().unwrap(); /// assert_ne!(plain.value(), "value"); /// let verified = jar.message_signed(&key, "".to_owned()).verify(plain).unwrap(); /// assert_eq!(verified.value(), "value"); /// /// let plain = Cookie::new("plaintext", "hello"); /// assert!(jar.message_signed(&key, "".to_owned()).verify(plain).is_none()); /// ``` pub async fn verify(&self, mut cookie: Cookie<'static>) -> Option> { match self._verify(cookie.value()).await { Ok(value) => { cookie.set_value(value); Some(cookie) } Err(err) => { tracing::warn!( err = %err, "possibly suspicious activity: Verification failed for Cookie. cookie validation string was: {}", self.message ); None } } } } impl> AdditionalSignedJar { /// Returns a reference to the `Cookie` inside this jar with the name `name` /// and verifies the authenticity and integrity of the cookie's value, /// returning a `Cookie` with the authenticated value. If the cookie cannot /// be found, or the cookie fails to verify, `None` is returned. /// /// # Example /// /// ```rust ignore /// use cookie::{CookieJar, Cookie, Key}; /// /// let key = Key::generate(); /// let jar = CookieJar::new(); /// assert!(jar.message_signed(&key, "".to_owned()).get("name").is_none()); /// /// let mut jar = jar; /// let mut signed_jar = jar.message_signed_mut(&key, "".to_owned()); /// signed_jar.add(Cookie::new("name", "value")); /// assert_eq!(signed_jar.get("name").unwrap().value(), "value"); /// ``` pub async fn get(&self, name: &str) -> Option> { match self.parent.borrow().get(name) { Some(c) => self.verify(c.clone()).await, None => None, } } } impl> AdditionalSignedJar { /// Adds `cookie` to the parent jar. The cookie's value is signed assuring /// integrity and authenticity. /// /// # Example /// /// ```rust ignore /// use cookie::{CookieJar, Cookie, Key}; /// /// let key = Key::generate(); /// let mut jar = CookieJar::new(); /// jar.message_signed_mut(&key, "".to_owned()).add(("name", "value")).await; /// /// assert_ne!(jar.get("name").unwrap().value(), "value"); /// assert!(jar.get("name").unwrap().value().contains("value")); /// assert_eq!(jar.message_signed(&key, "".to_owned()).get("name").unwrap().value(), "value"); /// ``` pub async fn add>>(&mut self, cookie: C) { let mut cookie = cookie.into(); self.sign_cookie(&mut cookie).await; self.parent.borrow_mut().add(cookie); } /// Adds an "original" `cookie` to this jar. The cookie's value is signed /// assuring integrity and authenticity. Adding an original cookie does not /// affect the [`CookieJar::delta()`] computation. This method is intended /// to be used to seed the cookie jar with cookies received from a client's /// HTTP message. /// /// For accurate `delta` computations, this method should not be called /// after calling `remove`. /// /// # Example /// /// ```rust ignore /// use cookie::{CookieJar, Cookie, Key}; /// /// let key = Key::generate(); /// let mut jar = CookieJar::new(); /// jar.message_signed_mut(&key, "".to_owned()).add_original(("name", "value")).await; /// /// assert_eq!(jar.iter().count(), 1); /// assert_eq!(jar.delta().count(), 0); /// ``` pub async fn add_original>>(&mut self, cookie: C) { let mut cookie = cookie.into(); self.sign_cookie(&mut cookie).await; self.parent.borrow_mut().add_original(cookie); } /// Removes `cookie` from the parent jar. /// /// For correct removal, the passed in `cookie` must contain the same `path` /// and `domain` as the cookie that was initially set. /// /// This is identical to [`CookieJar::remove()`]. See the method's /// documentation for more details. /// /// # Example /// /// ```rust ignore /// use cookie::{CookieJar, Cookie, Key}; /// /// let key = Key::generate(); /// let mut jar = CookieJar::new(); /// let mut signed_jar = jar.message_signed_mut(&key, "".to_owned()); /// /// signed_jar.add(("name", "value")); /// assert!(signed_jar.get("name").is_some()); /// /// signed_jar.remove("name"); /// assert!(signed_jar.get("name").is_none()); /// ``` pub fn remove>>(&mut self, cookie: C) { self.parent.borrow_mut().remove(cookie.into()); } } pub(crate) async fn sign_header( value: &str, key: &Key, message: &str, ) -> Result { let value = value.to_owned(); let message = message.to_owned(); let key = key.to_owned(); task::spawn_blocking(move || { // Compute HMAC-SHA256 of the cookie's value. let mut mac = Hmac::::new_from_slice(key.signing()).map_err(|_| "Key was invalid.")?; // Add the payload to the message first. let message = format!("{value}{message}"); mac.update(message.as_bytes()); // Cookie's new value is [MAC | original-value]. let mut new_value = encode(mac.finalize().into_bytes()); new_value.push_str(&value); Ok(new_value) }) .await .map_err(|_| "thread join in _verify was unsuccessful")? } /// Given a signed value `str` where the signature is prepended to `value`, /// verifies the signed value and returns it. If there's a problem, returns /// an `Err` with a string describing the issue. pub(crate) async fn verify_header( header_value: &str, key: &Key, message: &str, ) -> Result { let header_value = header_value.to_owned(); let message = message.to_owned(); let key = key.to_owned(); task::spawn_blocking(move || { if !header_value.is_char_boundary(BASE64_DIGEST_LEN) { return Err("missing or invalid digest"); } // Split [MAC | original-value] into its two parts. let (digest_str, value) = header_value.split_at(BASE64_DIGEST_LEN); let digest = decode(digest_str).map_err(|_| "bad base64 digest")?; // Perform the verification. let mut mac = Hmac::::new_from_slice(key.signing()).map_err(|_| "Key was invalid.")?; // Add message here so we can check if it matches. let message = format!("{value}{message}"); mac.update(message.as_bytes()); mac.verify_slice(&digest) .map(|_| value.to_string()) .map_err(|_| "value did not verify") }) .await .map_err(|_| "thread join in _verify was unsuccessful")? } #[cfg(test)] mod test { use crate::sec::signed::CookiesAdditionJar; use cookie::{Cookie, CookieJar, Key}; #[tokio::test] async fn roundtrip() { // Secret is SHA-256 hash of 'Super secret!' passed through HKDF-SHA256. let key = Key::from(&[ 89, 202, 200, 125, 230, 90, 197, 245, 166, 249, 34, 169, 135, 31, 20, 197, 94, 154, 254, 79, 60, 26, 8, 143, 254, 24, 116, 138, 92, 225, 159, 60, 157, 41, 135, 129, 31, 226, 196, 16, 198, 168, 134, 4, 42, 1, 196, 24, 57, 103, 241, 147, 201, 185, 233, 10, 180, 170, 187, 89, 252, 137, 110, 107, ]); let mut jar = CookieJar::new(); jar.add(Cookie::new( "signed_with_ring014", "3tdHXEQ2kf6fxC7dWzBGmpSLMtJenXLKrZ9cHkSsl1w=Tamper-proof", )); jar.add(Cookie::new( "signed_with_ring016", "3tdHXEQ2kf6fxC7dWzBGmpSLMtJenXLKrZ9cHkSsl1w=Tamper-proof", )); let signed = jar.message_signed(&key, "".to_owned()); assert_eq!( signed.get("signed_with_ring014").await.unwrap().value(), "Tamper-proof" ); assert_eq!( signed.get("signed_with_ring016").await.unwrap().value(), "Tamper-proof" ); } }