cookie/secure/signed.rs
1use std::convert::TryInto;
2use std::borrow::{Borrow, BorrowMut};
3
4use sha2::Sha256;
5use hmac::{Hmac, Mac};
6
7use crate::secure::{base64, Key};
8use crate::{Cookie, CookieJar};
9
10// Keep these in sync, and keep the key len synced with the `signed` docs as
11// well as the `KEYS_INFO` const in secure::Key.
12pub(crate) const BASE64_DIGEST_LEN: usize = 44;
13pub(crate) const KEY_LEN: usize = 32;
14
15/// A child cookie jar that authenticates its cookies.
16///
17/// A _signed_ child jar signs all the cookies added to it and verifies cookies
18/// retrieved from it. Any cookies stored in a `SignedJar` are provided
19/// integrity and authenticity. In other words, clients cannot tamper with the
20/// contents of a cookie nor can they fabricate cookie values, but the data is
21/// visible in plaintext.
22#[cfg_attr(all(nightly, doc), doc(cfg(feature = "signed")))]
23pub struct SignedJar<J> {
24 parent: J,
25 key: [u8; KEY_LEN],
26}
27
28impl<J> SignedJar<J> {
29 /// Creates a new child `SignedJar` with parent `parent` and key `key`. This
30 /// method is typically called indirectly via the `signed{_mut}` methods of
31 /// `CookieJar`.
32 pub(crate) fn new(parent: J, key: &Key) -> SignedJar<J> {
33 SignedJar { parent, key: key.signing().try_into().expect("sign key len") }
34 }
35
36 /// Signs the cookie's value providing integrity and authenticity.
37 fn sign_cookie(&self, cookie: &mut Cookie) {
38 // Compute HMAC-SHA256 of the cookie's value.
39 let mut mac = Hmac::<Sha256>::new_from_slice(&self.key).expect("good key");
40 mac.update(cookie.value().as_bytes());
41
42 // Cookie's new value is [MAC | original-value].
43 let mut new_value = base64::encode(&mac.finalize().into_bytes());
44 new_value.push_str(cookie.value());
45 cookie.set_value(new_value);
46 }
47
48 /// Given a signed value `str` where the signature is prepended to `value`,
49 /// verifies the signed value and returns it. If there's a problem, returns
50 /// an `Err` with a string describing the issue.
51 fn _verify(&self, cookie_value: &str) -> Result<String, &'static str> {
52 if !cookie_value.is_char_boundary(BASE64_DIGEST_LEN) {
53 return Err("missing or invalid digest");
54 }
55
56 // Split [MAC | original-value] into its two parts.
57 let (digest_str, value) = cookie_value.split_at(BASE64_DIGEST_LEN);
58 let digest = base64::decode(digest_str).map_err(|_| "bad base64 digest")?;
59
60 // Perform the verification.
61 let mut mac = Hmac::<Sha256>::new_from_slice(&self.key).expect("good key");
62 mac.update(value.as_bytes());
63 mac.verify_slice(&digest)
64 .map(|_| value.to_string())
65 .map_err(|_| "value did not verify")
66 }
67
68 /// Verifies the authenticity and integrity of `cookie`, returning the
69 /// plaintext version if verification succeeds or `None` otherwise.
70 /// Verification _always_ succeeds if `cookie` was generated by a
71 /// `SignedJar` with the same key as `self`.
72 ///
73 /// # Example
74 ///
75 /// ```rust
76 /// use cookie::{CookieJar, Cookie, Key};
77 ///
78 /// let key = Key::generate();
79 /// let mut jar = CookieJar::new();
80 /// assert!(jar.signed(&key).get("name").is_none());
81 ///
82 /// jar.signed_mut(&key).add(("name", "value"));
83 /// assert_eq!(jar.signed(&key).get("name").unwrap().value(), "value");
84 ///
85 /// let plain = jar.get("name").cloned().unwrap();
86 /// assert_ne!(plain.value(), "value");
87 /// let verified = jar.signed(&key).verify(plain).unwrap();
88 /// assert_eq!(verified.value(), "value");
89 ///
90 /// let plain = Cookie::new("plaintext", "hello");
91 /// assert!(jar.signed(&key).verify(plain).is_none());
92 /// ```
93 pub fn verify(&self, mut cookie: Cookie<'static>) -> Option<Cookie<'static>> {
94 if let Ok(value) = self._verify(cookie.value()) {
95 cookie.set_value(value);
96 return Some(cookie);
97 }
98
99 None
100 }
101}
102
103impl<J: Borrow<CookieJar>> SignedJar<J> {
104 /// Returns a reference to the `Cookie` inside this jar with the name `name`
105 /// and verifies the authenticity and integrity of the cookie's value,
106 /// returning a `Cookie` with the authenticated value. If the cookie cannot
107 /// be found, or the cookie fails to verify, `None` is returned.
108 ///
109 /// # Example
110 ///
111 /// ```rust
112 /// use cookie::{CookieJar, Cookie, Key};
113 ///
114 /// let key = Key::generate();
115 /// let jar = CookieJar::new();
116 /// assert!(jar.signed(&key).get("name").is_none());
117 ///
118 /// let mut jar = jar;
119 /// let mut signed_jar = jar.signed_mut(&key);
120 /// signed_jar.add(Cookie::new("name", "value"));
121 /// assert_eq!(signed_jar.get("name").unwrap().value(), "value");
122 /// ```
123 pub fn get(&self, name: &str) -> Option<Cookie<'static>> {
124 self.parent.borrow().get(name).and_then(|c| self.verify(c.clone()))
125 }
126}
127
128impl<J: BorrowMut<CookieJar>> SignedJar<J> {
129 /// Adds `cookie` to the parent jar. The cookie's value is signed assuring
130 /// integrity and authenticity.
131 ///
132 /// # Example
133 ///
134 /// ```rust
135 /// use cookie::{CookieJar, Cookie, Key};
136 ///
137 /// let key = Key::generate();
138 /// let mut jar = CookieJar::new();
139 /// jar.signed_mut(&key).add(("name", "value"));
140 ///
141 /// assert_ne!(jar.get("name").unwrap().value(), "value");
142 /// assert!(jar.get("name").unwrap().value().contains("value"));
143 /// assert_eq!(jar.signed(&key).get("name").unwrap().value(), "value");
144 /// ```
145 pub fn add<C: Into<Cookie<'static>>>(&mut self, cookie: C) {
146 let mut cookie = cookie.into();
147 self.sign_cookie(&mut cookie);
148 self.parent.borrow_mut().add(cookie);
149 }
150
151 /// Adds an "original" `cookie` to this jar. The cookie's value is signed
152 /// assuring integrity and authenticity. Adding an original cookie does not
153 /// affect the [`CookieJar::delta()`] computation. This method is intended
154 /// to be used to seed the cookie jar with cookies received from a client's
155 /// HTTP message.
156 ///
157 /// For accurate `delta` computations, this method should not be called
158 /// after calling `remove`.
159 ///
160 /// # Example
161 ///
162 /// ```rust
163 /// use cookie::{CookieJar, Cookie, Key};
164 ///
165 /// let key = Key::generate();
166 /// let mut jar = CookieJar::new();
167 /// jar.signed_mut(&key).add_original(("name", "value"));
168 ///
169 /// assert_eq!(jar.iter().count(), 1);
170 /// assert_eq!(jar.delta().count(), 0);
171 /// ```
172 pub fn add_original<C: Into<Cookie<'static>>>(&mut self, cookie: C) {
173 let mut cookie = cookie.into();
174 self.sign_cookie(&mut cookie);
175 self.parent.borrow_mut().add_original(cookie);
176 }
177
178 /// Removes `cookie` from the parent jar.
179 ///
180 /// For correct removal, the passed in `cookie` must contain the same `path`
181 /// and `domain` as the cookie that was initially set.
182 ///
183 /// This is identical to [`CookieJar::remove()`]. See the method's
184 /// documentation for more details.
185 ///
186 /// # Example
187 ///
188 /// ```rust
189 /// use cookie::{CookieJar, Cookie, Key};
190 ///
191 /// let key = Key::generate();
192 /// let mut jar = CookieJar::new();
193 /// let mut signed_jar = jar.signed_mut(&key);
194 ///
195 /// signed_jar.add(("name", "value"));
196 /// assert!(signed_jar.get("name").is_some());
197 ///
198 /// signed_jar.remove("name");
199 /// assert!(signed_jar.get("name").is_none());
200 /// ```
201 pub fn remove<C: Into<Cookie<'static>>>(&mut self, cookie: C) {
202 self.parent.borrow_mut().remove(cookie.into());
203 }
204}
205
206#[cfg(test)]
207mod test {
208 use crate::{CookieJar, Cookie, Key};
209
210 #[test]
211 fn simple() {
212 let key = Key::generate();
213 let mut jar = CookieJar::new();
214 assert_simple_behaviour!(jar, jar.signed_mut(&key));
215 }
216
217 #[test]
218 fn private() {
219 let key = Key::generate();
220 let mut jar = CookieJar::new();
221 assert_secure_behaviour!(jar, jar.signed_mut(&key));
222 }
223
224 #[test]
225 fn roundtrip() {
226 // Secret is SHA-256 hash of 'Super secret!' passed through HKDF-SHA256.
227 let key = Key::from(&[89, 202, 200, 125, 230, 90, 197, 245, 166, 249,
228 34, 169, 135, 31, 20, 197, 94, 154, 254, 79, 60, 26, 8, 143, 254,
229 24, 116, 138, 92, 225, 159, 60, 157, 41, 135, 129, 31, 226, 196, 16,
230 198, 168, 134, 4, 42, 1, 196, 24, 57, 103, 241, 147, 201, 185, 233,
231 10, 180, 170, 187, 89, 252, 137, 110, 107]);
232
233 let mut jar = CookieJar::new();
234 jar.add(Cookie::new("signed_with_ring014",
235 "3tdHXEQ2kf6fxC7dWzBGmpSLMtJenXLKrZ9cHkSsl1w=Tamper-proof"));
236 jar.add(Cookie::new("signed_with_ring016",
237 "3tdHXEQ2kf6fxC7dWzBGmpSLMtJenXLKrZ9cHkSsl1w=Tamper-proof"));
238
239 let signed = jar.signed(&key);
240 assert_eq!(signed.get("signed_with_ring014").unwrap().value(), "Tamper-proof");
241 assert_eq!(signed.get("signed_with_ring016").unwrap().value(), "Tamper-proof");
242 }
243
244 #[test]
245 fn issue_178() {
246 let data = "x=yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy£";
247 let c = Cookie::parse(data).expect("failed to parse cookie");
248 let key = Key::from(&[0u8; 64]);
249 let mut jar = CookieJar::new();
250 let signed = jar.signed_mut(&key);
251 assert!(signed.verify(c).is_none());
252 }
253}