axum_session/sec/
signed.rs

1use std::borrow::{Borrow, BorrowMut};
2
3use hmac::{Hmac, Mac};
4use sha2::Sha256;
5
6use cookie::{Cookie, CookieJar, Key};
7
8// Keep these in sync, and keep the key len synced with the `signed` docs as
9// well as the `KEYS_INFO` const in secure::Key.
10pub(crate) const BASE64_DIGEST_LEN: usize = 44;
11pub(crate) const KEY_LEN: usize = 32;
12
13use base64::{prelude::BASE64_STANDARD, DecodeError, Engine};
14
15/// Encode `input` as the standard base64 with padding.
16pub(crate) fn encode<T: AsRef<[u8]>>(input: T) -> String {
17    BASE64_STANDARD.encode(input)
18}
19
20/// Decode `input` as the standard base64 with padding.
21pub(crate) fn decode<T: AsRef<[u8]>>(input: T) -> Result<Vec<u8>, DecodeError> {
22    BASE64_STANDARD.decode(input)
23}
24
25pub trait CookiesAdditionJar {
26    fn message_signed<'a>(&'a self, key: &Key, message: String) -> AdditionalSignedJar<&'a Self>;
27    fn message_signed_mut<'a>(
28        &'a mut self,
29        key: &Key,
30        message: String,
31    ) -> AdditionalSignedJar<&'a mut Self>;
32}
33
34impl CookiesAdditionJar for CookieJar {
35    fn message_signed<'a>(&'a self, key: &Key, message: String) -> AdditionalSignedJar<&'a Self> {
36        AdditionalSignedJar::new(self, key, message)
37    }
38
39    fn message_signed_mut<'a>(
40        &'a mut self,
41        key: &Key,
42        message: String,
43    ) -> AdditionalSignedJar<&'a mut Self> {
44        AdditionalSignedJar::new(self, key, message)
45    }
46}
47
48/// A child cookie jar that authenticates its cookies and Adds Additional measures to ensure integrity.
49pub struct AdditionalSignedJar<J> {
50    parent: J,
51    key: [u8; KEY_LEN],
52    message: String,
53}
54
55impl<J> AdditionalSignedJar<J> {
56    /// Creates a new child `AdditionalSignedJar`
57    pub(crate) fn new(parent: J, key: &Key, message: String) -> AdditionalSignedJar<J> {
58        AdditionalSignedJar {
59            parent,
60            key: key.signing().try_into().expect("sign key len"),
61            message,
62        }
63    }
64
65    /// Signs the cookie's value and message providing integrity and authenticity.
66    fn sign_cookie(&self, cookie: &mut Cookie) {
67        // Compute HMAC-SHA256 of the cookie's value.
68        let mut mac = match Hmac::<Sha256>::new_from_slice(&self.key) {
69            Ok(v) => v,
70            Err(err) => {
71                tracing::error!(err = %err,  "key is invalid." );
72                return;
73            }
74        };
75
76        // Add the payload to the message first.
77        let message = format!("{}{}", cookie.value(), &self.message);
78        mac.update(message.as_bytes());
79
80        // Cookie's new value is [MAC | original-value].
81        let mut new_value = encode(mac.finalize().into_bytes());
82        new_value.push_str(cookie.value());
83        cookie.set_value(new_value);
84    }
85
86    /// Given a signed value `str` where the signature is prepended to `value`,
87    /// verifies the signed value and returns it. If there's a problem, returns
88    /// an `Err` with a string describing the issue.
89    fn _verify(&self, cookie_value: &str) -> Result<String, &'static str> {
90        if !cookie_value.is_char_boundary(BASE64_DIGEST_LEN) {
91            return Err("missing or invalid digest");
92        }
93
94        // Split [MAC | original-value] into its two parts.
95        let (digest_str, value) = cookie_value.split_at(BASE64_DIGEST_LEN);
96        let digest = decode(digest_str).map_err(|_| "bad base64 digest")?;
97
98        // Perform the verification.
99        let mut mac = Hmac::<Sha256>::new_from_slice(&self.key).map_err(|_| "key is invalid.")?;
100        // Add message here so we can check if it matches.
101        let message = format!("{}{}", value, &self.message);
102        mac.update(message.as_bytes());
103        mac.verify_slice(&digest)
104            .map(|_| value.to_string())
105            .map_err(|_| "value did not verify")
106    }
107
108    /// Verifies the authenticity and integrity of `cookie`, returning the
109    /// plaintext version if verification succeeds or `None` otherwise.
110    /// Verification _always_ succeeds if `cookie` was generated by a
111    /// `AdditionalSignedJar` with the same key and message as `self`.
112    ///
113    /// # Example
114    ///
115    /// ```rust ignore
116    /// use cookie::{CookieJar, Cookie, Key};
117    ///
118    /// let key = Key::generate();
119    /// let mut jar = CookieJar::new();
120    /// assert!(jar.message_signed(&key, "".to_owned()).get("name").is_none());
121    ///
122    /// jar.message_signed_mut(&key, "".to_owned()).add(("name", "value"));
123    /// assert_eq!(jar.message_signed(&key, "".to_owned()).get("name").unwrap().value(), "value");
124    ///
125    /// let plain = jar.get("name").cloned().unwrap();
126    /// assert_ne!(plain.value(), "value");
127    /// let verified = jar.message_signed(&key, "".to_owned()).verify(plain).unwrap();
128    /// assert_eq!(verified.value(), "value");
129    ///
130    /// let plain = Cookie::new("plaintext", "hello");
131    /// assert!(jar.message_signed(&key, "".to_owned()).verify(plain).is_none());
132    /// ```
133    pub fn verify(&self, mut cookie: Cookie<'static>) -> Option<Cookie<'static>> {
134        match self._verify(cookie.value()) {
135            Ok(value) => {
136                cookie.set_value(value);
137                Some(cookie)
138            }
139            Err(err) => {
140                tracing::warn!(
141                    err = %err,
142                    "possibly suspicious activity: Verification failed for Cookie. cookie validation string was: {}",
143                    self.message
144                );
145                None
146            }
147        }
148    }
149}
150
151impl<J: Borrow<CookieJar>> AdditionalSignedJar<J> {
152    /// Returns a reference to the `Cookie` inside this jar with the name `name`
153    /// and verifies the authenticity and integrity of the cookie's value,
154    /// returning a `Cookie` with the authenticated value. If the cookie cannot
155    /// be found, or the cookie fails to verify, `None` is returned.
156    ///
157    /// # Example
158    ///
159    /// ```rust ignore
160    /// use cookie::{CookieJar, Cookie, Key};
161    ///
162    /// let key = Key::generate();
163    /// let jar = CookieJar::new();
164    /// assert!(jar.message_signed(&key, "".to_owned()).get("name").is_none());
165    ///
166    /// let mut jar = jar;
167    /// let mut signed_jar = jar.message_signed_mut(&key, "".to_owned());
168    /// signed_jar.add(Cookie::new("name", "value"));
169    /// assert_eq!(signed_jar.get("name").unwrap().value(), "value");
170    /// ```
171    pub fn get(&self, name: &str) -> Option<Cookie<'static>> {
172        self.parent
173            .borrow()
174            .get(name)
175            .and_then(|c| self.verify(c.clone()))
176    }
177}
178
179impl<J: BorrowMut<CookieJar>> AdditionalSignedJar<J> {
180    /// Adds `cookie` to the parent jar. The cookie's value is signed assuring
181    /// integrity and authenticity.
182    ///
183    /// # Example
184    ///
185    /// ```rust ignore
186    /// use cookie::{CookieJar, Cookie, Key};
187    ///
188    /// let key = Key::generate();
189    /// let mut jar = CookieJar::new();
190    /// jar.message_signed_mut(&key, "".to_owned()).add(("name", "value"));
191    ///
192    /// assert_ne!(jar.get("name").unwrap().value(), "value");
193    /// assert!(jar.get("name").unwrap().value().contains("value"));
194    /// assert_eq!(jar.message_signed(&key, "".to_owned()).get("name").unwrap().value(), "value");
195    /// ```
196    pub fn add<C: Into<Cookie<'static>>>(&mut self, cookie: C) {
197        let mut cookie = cookie.into();
198        self.sign_cookie(&mut cookie);
199        self.parent.borrow_mut().add(cookie);
200    }
201
202    /// Adds an "original" `cookie` to this jar. The cookie's value is signed
203    /// assuring integrity and authenticity. Adding an original cookie does not
204    /// affect the [`CookieJar::delta()`] computation. This method is intended
205    /// to be used to seed the cookie jar with cookies received from a client's
206    /// HTTP message.
207    ///
208    /// For accurate `delta` computations, this method should not be called
209    /// after calling `remove`.
210    ///
211    /// # Example
212    ///
213    /// ```rust ignore
214    /// use cookie::{CookieJar, Cookie, Key};
215    ///
216    /// let key = Key::generate();
217    /// let mut jar = CookieJar::new();
218    /// jar.message_signed_mut(&key, "".to_owned()).add_original(("name", "value"));
219    ///
220    /// assert_eq!(jar.iter().count(), 1);
221    /// assert_eq!(jar.delta().count(), 0);
222    /// ```
223    pub fn add_original<C: Into<Cookie<'static>>>(&mut self, cookie: C) {
224        let mut cookie = cookie.into();
225        self.sign_cookie(&mut cookie);
226        self.parent.borrow_mut().add_original(cookie);
227    }
228
229    /// Removes `cookie` from the parent jar.
230    ///
231    /// For correct removal, the passed in `cookie` must contain the same `path`
232    /// and `domain` as the cookie that was initially set.
233    ///
234    /// This is identical to [`CookieJar::remove()`]. See the method's
235    /// documentation for more details.
236    ///
237    /// # Example
238    ///
239    /// ```rust ignore
240    /// use cookie::{CookieJar, Cookie, Key};
241    ///
242    /// let key = Key::generate();
243    /// let mut jar = CookieJar::new();
244    /// let mut signed_jar = jar.message_signed_mut(&key, "".to_owned());
245    ///
246    /// signed_jar.add(("name", "value"));
247    /// assert!(signed_jar.get("name").is_some());
248    ///
249    /// signed_jar.remove("name");
250    /// assert!(signed_jar.get("name").is_none());
251    /// ```
252    pub fn remove<C: Into<Cookie<'static>>>(&mut self, cookie: C) {
253        self.parent.borrow_mut().remove(cookie.into());
254    }
255}
256
257pub(crate) fn sign_header(value: &str, key: &Key, message: &str) -> Result<String, &'static str> {
258    // Compute HMAC-SHA256 of the cookie's value.
259    let mut mac = Hmac::<Sha256>::new_from_slice(key.signing()).map_err(|_| "Key was invalid.")?;
260    // Add the payload to the message first.
261    let message = format!("{value}{message}");
262    mac.update(message.as_bytes());
263
264    // Cookie's new value is [MAC | original-value].
265    let mut new_value = encode(mac.finalize().into_bytes());
266    new_value.push_str(value);
267    Ok(new_value)
268}
269
270/// Given a signed value `str` where the signature is prepended to `value`,
271/// verifies the signed value and returns it. If there's a problem, returns
272/// an `Err` with a string describing the issue.
273pub(crate) fn verify_header(
274    header_value: &str,
275    key: &Key,
276    message: &str,
277) -> Result<String, &'static str> {
278    if !header_value.is_char_boundary(BASE64_DIGEST_LEN) {
279        return Err("missing or invalid digest");
280    }
281
282    // Split [MAC | original-value] into its two parts.
283    let (digest_str, value) = header_value.split_at(BASE64_DIGEST_LEN);
284    let digest = decode(digest_str).map_err(|_| "bad base64 digest")?;
285
286    // Perform the verification.
287    let mut mac = Hmac::<Sha256>::new_from_slice(key.signing()).map_err(|_| "Key was invalid.")?;
288    // Add message here so we can check if it matches.
289    let message = format!("{value}{message}");
290    mac.update(message.as_bytes());
291    mac.verify_slice(&digest)
292        .map(|_| value.to_string())
293        .map_err(|_| "value did not verify")
294}
295
296#[cfg(test)]
297mod test {
298    use crate::sec::signed::CookiesAdditionJar;
299    use cookie::{Cookie, CookieJar, Key};
300
301    #[test]
302    fn roundtrip() {
303        // Secret is SHA-256 hash of 'Super secret!' passed through HKDF-SHA256.
304        let key = Key::from(&[
305            89, 202, 200, 125, 230, 90, 197, 245, 166, 249, 34, 169, 135, 31, 20, 197, 94, 154,
306            254, 79, 60, 26, 8, 143, 254, 24, 116, 138, 92, 225, 159, 60, 157, 41, 135, 129, 31,
307            226, 196, 16, 198, 168, 134, 4, 42, 1, 196, 24, 57, 103, 241, 147, 201, 185, 233, 10,
308            180, 170, 187, 89, 252, 137, 110, 107,
309        ]);
310
311        let mut jar = CookieJar::new();
312        jar.add(Cookie::new(
313            "signed_with_ring014",
314            "3tdHXEQ2kf6fxC7dWzBGmpSLMtJenXLKrZ9cHkSsl1w=Tamper-proof",
315        ));
316        jar.add(Cookie::new(
317            "signed_with_ring016",
318            "3tdHXEQ2kf6fxC7dWzBGmpSLMtJenXLKrZ9cHkSsl1w=Tamper-proof",
319        ));
320
321        let signed = jar.message_signed(&key, "".to_owned());
322        assert_eq!(
323            signed.get("signed_with_ring014").unwrap().value(),
324            "Tamper-proof"
325        );
326        assert_eq!(
327            signed.get("signed_with_ring016").unwrap().value(),
328            "Tamper-proof"
329        );
330    }
331}