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}