-
-
Notifications
You must be signed in to change notification settings - Fork 119
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Migrate from rust-openssl to *ring* for 'secure' functionality. #68
Conversation
let mut signer = Signer::new(MessageDigest::sha1(), &pkey).unwrap(); | ||
signer.update(val.as_bytes()).unwrap(); | ||
signer.finish().unwrap() | ||
fn dosign(key: &[u8], val: &str) -> digest::Digest { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: If you eliminate dosign(), you can replace the second of its only two usages with verify()
instead of another sign()
https://briansmith.org/rustdoc/ring/hmac/fn.verify.html
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great idea! Implemented in cae8d23
fn encrypt_data(key: &[u8], val: &str) -> String { | ||
let iv = random_iv(); | ||
let iv_str = iv.to_base64(STANDARD); | ||
assert_eq!(key.len(), ENCRYPTION_KEY_LEN); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think ring could benefit from a helper function that handles lines 443-464 in a single call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For reference, here are the lines @Philipp91 mentioned.
cc @briansmith in case you find this helpful.
|
||
#[cfg(feature = "secure")] | ||
fn _new(secret: &[u8]) -> CookieJar<'static> { | ||
let (key256, key512) = secure::generate_keys(secret); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This generate_keys
call does two 10k PBKDF2 each time a cookie jar is created. A cookie jar is typically created once per request based on the incoming headers. As such, this is an unacceptable performance regression.
Why do we need to generate keys in this manner? The typical way to do this is to use SHA256/512 to pad the randomness coming in, or demand randomness of a certain length so that it suffices for use as the key. For instance, you could require 512 bits as an input.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If secret
is a random value already then HKDF (ring::hkdf
) should be used to derive the two keys if you insist on still using HMAC for the authentication-only case.
If secret
is a password then something like PBKDF2 needs to be used to stretch the key material once. Then use HKDF to derive the two keys.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Regarding the expensive operation, @sfackler had a suggestion to design a struct Secret
and have it be constructed once where it does the PBKDF2 during construction. Then that structure can be passed into CookieJar
whenever secrets are needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To be honest, PBKDF2 doesn't make much sense in this application. PBKDF2 is for converting a password into a key. But nobody should be using a password for this stuff. instead, they should be randomly generating a key.
Once you have one key, you can derive multiple keys from it using a KDF such as ring::hkdf
. This is much faster than PBKDF2 but still not so fast that you want to do it more than necessary. Thus, it is good to create an object for holding the outputs of HKDF.
However, if you go with my suggestion to just use a nonce-misuse-resistant AEAD for both encrypt-and-authenticate and for authenticate-only, then you only need one key, and you can skip using a KDF completely, if you are given a key.
And, again, don't support the mode where one is given a password instead of a key, if you can avoid it.
#[cfg(not(feature = "secure"))] | ||
fn prepare_key(_key: &[u8]) -> () { | ||
() | ||
struct SecureKeys { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need two keys of different lengths?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The key size for AEAD ChaCha20 Poly1305 is 256 bits. The key size for HMAC SHA1 is 512 bits.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, but why not have a 512-bit key and truncate to 256-bits for ChaCha? We're not gaining anything by having two keys here, as far as I can tell; they're both derived from one key.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wasn't sure if truncating a 512-bit KDF-derived key would be secure, but people online are telling me it's fine. I'll do that then.
return None | ||
let verification_key = | ||
hmac::VerificationKey::new(SIGNING_ALGORITHM, key); | ||
let is_valid_signature = hmac::verify( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like this line and the following few could be chained into something like .ok().map()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's possible I could do that, but I think the way I wrote it is easier to follow.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I disagree, especially because .ok().map()
is very idiomatic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you share a concrete example of what you're proposing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmac::verify(&verification_key, text.as_bytes(), &signature).ok()
.map(move |_| {
cookie.value = text.to_owned();
cookie
});
The following also works:
if let Ok(_) = hmac::verify(&verification_key, text.as_bytes(), &signature) {
cookie.value = text.to_owned();
Some(cookie)
} else {
None
}
or even:
match hmac::verify(&verification_key, text.as_bytes(), &signature) {
Ok(_) => {
cookie.value = text.to_owned();
Some(cookie)
}
Err(_) => None
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
None of those look marginally better to me, but I'll do it anyways.
// Prepare bytes to be sealed | ||
let in_out_len = cookie.value.as_bytes().len() + overhead_len; | ||
let mut in_out = vec![0; in_out_len]; | ||
in_out[..value_len].clone_from_slice(cookie.value.as_bytes()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this clone
and not copy
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accident. Fixed in e60534d.
Could the crypto stay the same? This is all modeled after Rails is which quite tried and true and I'd prefer to stick to that if possible. |
@alexcrichton Are you actually concerned about compatibility with existing encrypted cookies? Or do you just want a more familiar set of algorithms? |
@alexcrichton I believe the main motivation here is the switch from OpenSSL to ring, which is a less painful dependency to deal with particularly on Windows. However, ring doesn't support AES-CBC, but only AES-GCM and ChaCha20-Poly1305, so we'd need to switch to one of those. One nice property of both of these is that they're AEAD ciphers, meaning that they self-authenticate so we don't have to separately HMAC. They're both well regarded, and are actually the preferred ciphers in Mozilla's Server Side TLS recommendations: https://wiki.mozilla.org/Security/Server_Side_TLS. @frewsxcv I think AES-256-GCM will be faster than ChaCha20 on hardware with AES-NI. I wonder if it'd make more sense to use that since ~all server hardware will support those? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that I didn't look at this very carefully. My main advice is to wait until we have nonce-misuse-resistant encryption in ring before doing this.
In particular, I recommend using AES-GCM-SIV instead of either ChaCha20-Poly1305 and HMAC, once it's available.
Ok(cookie) | ||
} | ||
|
||
pub fn generate_keys(secret: &[u8]) -> ([u8; 32], [u8; 64]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be better to return (aead::SIVSealingKey, hmac::SigningKey)
. Then you could use aead::seal_in_place()
/hmac::sign()
and open_in_place_with_own_key()
/verify_with_own_key()
for the verification.
|
||
/// Algorithm used to encrypt the cookie value | ||
static ENCRYPTION_ALGORITHM: &'static aead::Algorithm = | ||
&aead::CHACHA20_POLY1305; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-
ChaCha20-Poly1305 isn't an encryption algorithm, it is an AEAD algorithm. In particular, it does encryption AND authentication.
-
ChaCha20-Poly1305 isn't the best choice for this application because 96-bit random nonces aren't a good choice. It would be better to use the upcoming SIV mode AEADs in ring or XChaCha20 (if we decide to add that). The SIV stuff isn't open source in ring yet, but AES-GCM-SIV probably will become open source "soon."
use self::rustc_serialize::base64::{ToBase64, FromBase64, STANDARD}; | ||
|
||
pub const MIN_KEY_LEN: usize = 32; | ||
/// Algorithm used to sign the cookie value | ||
static SIGNING_ALGORITHM: &'static digest::Algorithm = &digest::SHA1; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-
SHA-1 Isn't a signing/authentication algorithm. HMAC-SHA1 is.
-
It is not necessary to use HMAC for the authentication-without-encryption case. You can use the AEAD for authentication without encryption by passing
[]
as the plaintext/ciphertext and passing the data to be signed/authenticated in thead
parameter. For ChaCha20-Poly1305, this would result in authentication using Poly1305, for example.
|
||
let actual = match val.from_base64() { | ||
Ok(actual) => actual, Err(_) => return None | ||
pub fn design_and_decrypt(key: &[u8], mut cookie: Cookie) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It took me a bit to realize that "design" means "de-sign." The English word for what this does is "authenticate." Also, "open" == "authenticate and decrypt" and "seal" == "encrypt and sign."
/// Separator between sealed cookie value and nonce | ||
static SEALED_NONCE_SEPARATOR: &'static str = "--"; | ||
/// Key length (in bytes) used for encryption | ||
const ENCRYPTION_KEY_LEN: usize = 256 / 8; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could use ENCRYPTION_ALGORITHM.key_len()
for this.
|
||
#[cfg(feature = "secure")] | ||
fn _new(secret: &[u8]) -> CookieJar<'static> { | ||
let (key256, key512) = secure::generate_keys(secret); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If secret
is a random value already then HKDF (ring::hkdf
) should be used to derive the two keys if you insist on still using HMAC for the authentication-only case.
If secret
is a password then something like PBKDF2 needs to be used to stretch the key material once. Then use HKDF to derive the two keys.
@dreid oh nah I'm not worried about backcompat here, just the crypto! @sfackler yeah I'm all for moving to ring as the OpenSSL dependency is a bummer. My main concern here is that I'm not a cryptographer by any means and I'm hesitant to change from the "accepted practice" of what Rails does which presumably has been tried, true, and vetted for quite awhile. That being said, though, the crypto here is relatively simple IIRC and I'd be open to changing of course! Thanks for the review help @sfackler and @briansmith! |
Hello, I just wanted to pipe in let everyone here know that I wrote a crate called My next step was to build a small crate for parsing Rails' session cookies. I plan on writing some blog posts about building Rust micro web services that work with Rails applications. Perhaps EDIT: Here is a link to the |
Thank you everyone for your feedback ❤️ I'm a bit busy for a few days, but I'll try to get around to addressing all the comments this weekend |
See in particular briansmith/ring#411, briansmith/ring#412, and briansmith/ring#413. |
Thanks @briansmith! |
I'm still interested in seeing the ring migration happen, but considering these comments and my lack of free time right now, I'm not planning on making any changes to this PR for the foreseeable future. |
This also changes the encrypted' strategy from AES 256 CBC with an HMAC to AEAD ChaCha20 Poly1305.
#50
[breaking-change]