Skip to content

Commit

Permalink
SignedJar: add key-rotatable version
Browse files Browse the repository at this point in the history
Implements rwf2#158

Maybe this should be the default (`::new()`) in the future.
  • Loading branch information
Fishrock123 committed Jul 23, 2020
1 parent f7a781b commit 4f6a244
Show file tree
Hide file tree
Showing 2 changed files with 108 additions and 5 deletions.
42 changes: 42 additions & 0 deletions src/jar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,48 @@ impl CookieJar {
pub fn signed(&mut self, key: &Key) -> SignedJar {
SignedJar::new(self, key)
}

/// Returns a `SignedJar` with `self` as its parent jar using the provided keys
/// to sign/verify cookies added/retrieved from the child jar.
///
/// The first key will be used to create all new signed cookies and is optimized.
/// Any further keys are only able to verify older cookies as a fallback.
///
/// Any modifications to the child jar will be reflected on the parent jar,
/// and any retrievals from the child jar will be made from the parent jar.
///
/// # Example
///
/// ```rust
/// use cookie::{Cookie, CookieJar, Key};
///
/// // Generate secure kesy.
/// let key_new = Key::generate();
/// let key_old = Key::generate();
///
/// // Add a signed cookie.
/// let mut jar = CookieJar::new();
/// let mut signed = jar.signed_rotatable(&vec![&key_new, &key_old]);
/// signed.add(Cookie::new("signed", "text"));
///
/// // The cookie's contents are signed but still in plaintext.
/// assert_ne!(jar.get("signed").unwrap().value(), "text");
/// assert!(jar.get("signed").unwrap().value().contains("text"));
///
/// // They can be verified through the child jar.
/// assert_eq!(jar.signed(&key_new).get("signed").unwrap().value(), "text");
///
/// // A tampered with cookie does not validate but still exists.
/// let mut cookie = jar.get("signed").unwrap().clone();
/// jar.add(Cookie::new("signed", cookie.value().to_string() + "!"));
/// assert!(jar.signed(&key_new).get("signed").is_none());
/// assert!(jar.get("signed").is_some());
/// ```
#[cfg(feature = "signed")]
#[cfg_attr(all(doc, not(doctest)), doc(cfg(feature = "signed")))]
pub fn signed_rotatable(&mut self, keys: &Vec<&Key>) -> SignedJar {
SignedJar::new_rotatable(self, keys)
}
}

use std::collections::hash_set::Iter as HashSetIter;
Expand Down
71 changes: 66 additions & 5 deletions src/secure/signed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,31 @@ pub(crate) const KEY_LEN: usize = 32;
#[cfg_attr(all(doc, not(doctest)), doc(cfg(feature = "signed")))]
pub struct SignedJar<'a> {
parent: &'a mut CookieJar,
key: [u8; KEY_LEN],
rotated_keys: Vec<[u8; KEY_LEN]>, // Older rotated keys.
key: [u8; KEY_LEN], // The primary (newest) key.
}

impl<'a> SignedJar<'a> {
/// Creates a new child `SignedJar` with parent `parent` and key `key`. This
/// method is typically called indirectly via the `signed` method of
/// `CookieJar`.
pub(crate) fn new(parent: &'a mut CookieJar, key: &Key) -> SignedJar<'a> {
SignedJar { parent, key: key.signing }
SignedJar {
parent,
key: key.signing,
rotated_keys: vec![],
}
}

/// Creates a new child `SignedJar` with parent `parent` and a set of rotatable `keys`.
/// This method is typically called indirectly via the `signed` method of `CookieJar`.
pub(crate) fn new_rotatable(parent: &'a mut CookieJar, keys: &Vec<&Key>) -> SignedJar<'a> {
let rotated_keys = keys.split_at(1).1.iter().map(|key| key.signing).collect();
SignedJar {
parent,
key: keys[0].signing,
rotated_keys,
}
}

/// Signs the cookie's value providing integrity and authenticity.
Expand Down Expand Up @@ -57,9 +73,18 @@ impl<'a> SignedJar<'a> {
// Perform the verification.
let mut mac = Hmac::<Sha256>::new_varkey(&self.key).expect("good key");
mac.update(value.as_bytes());
mac.verify(&digest)
.map(|_| value.to_string())
.map_err(|_| "value did not verify")
if mac.verify(&digest).is_ok() {
return Ok(value.to_string());
}

for key in &self.rotated_keys {
let mut mac = Hmac::<Sha256>::new_varkey(key).expect("good key");
mac.update(value.as_bytes());
if mac.verify(&digest).is_ok() {
return Ok(value.to_string());
}
}
Err("value did not verify")
}

/// Returns a reference to the `Cookie` inside this jar with the name `name`
Expand Down Expand Up @@ -203,4 +228,40 @@ mod test {
assert_eq!(signed.get("signed_with_ring014").unwrap().value(), "Tamper-proof");
assert_eq!(signed.get("signed_with_ring016").unwrap().value(), "Tamper-proof");
}

#[test]
fn rotating_keys() {
// Secret is SHA-512 hash of 'Super secret!'.
let key_new = Key::from(&[
33, 67, 213, 207, 60, 35, 188, 129, 181, 18, 75, 142, 79, 74,
82, 88, 141, 94, 5, 87, 164, 213, 172, 164, 195, 185, 194, 154,
203, 102, 24, 20, 121, 211, 230, 9, 205, 151, 193, 12, 240,
186, 198, 163, 239, 226, 208, 156, 99, 188, 245, 108, 84, 188,
177, 108, 191, 89, 198, 151, 12, 190, 51, 187
]);
// Secret is SHA-512 hash of 'Old secret!'.
let key_old = Key::from(&[
237, 50, 109, 19, 90, 25, 201, 206, 238, 47, 124, 229, 10, 191,
231, 91, 231, 145, 2, 26, 190, 32, 246, 190, 131, 82, 231,
249, 28, 243, 217, 227, 153, 161, 144, 65, 91, 192, 107, 130,
38, 131, 229, 107, 42, 214, 195, 103, 14, 92, 184, 25, 148, 62,
250, 58, 127, 59, 51, 40, 224, 89, 239, 121
]);

let mut jar = CookieJar::new();
jar.add(Cookie::new("using_new_key",
"IIP0fH9nFQMPSauP/US8rZql3HZvzqC9HjY5EfcY3/g=Tamper-proof"));
jar.add(Cookie::new("using_old_key",
"ElLdnp9/IWK4N7DpsG3zogF48iKQN2813GpCynTn1C4=Tamper-proof"));

let mut signed = jar.signed_rotatable(&vec![&key_new, &key_old]);
assert_eq!(signed.get("using_new_key").unwrap().value(), "Tamper-proof");
assert_eq!(signed.get("using_old_key").unwrap().value(), "Tamper-proof");

signed.add(Cookie::new("made_with_new", "Tamper-proof"));
assert_eq!(
signed.get("using_new_key").unwrap().value(),
signed.get("made_with_new").unwrap().value()
);
}
}

0 comments on commit 4f6a244

Please sign in to comment.