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
Add Multifernet.rotate method #3979
Conversation
I just realized that there should be some documentation for this as well. |
tests/test_fernet.py
Outdated
|
||
rotated = mf2.rotate([mf1.encrypt(plaintext)])[0] | ||
|
||
assert rotated != mf1_ciphertext |
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 assert would be true even if the rotation didn't occur because you're calling mf1.encrypt(plaintext)
again.
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.
whoops - I intended to reuse mf1_ciphertext instead of encrypting it again.
src/cryptography/fernet.py
Outdated
@@ -134,6 +134,9 @@ def __init__(self, fernets): | |||
def encrypt(self, msg): | |||
return self._fernets[0].encrypt(msg) | |||
|
|||
def rotate(self, msgs, ttl=None): | |||
return [self.encrypt(self.decrypt(m, ttl)) for m in msgs] |
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 does this work on lists of messages? No other fernet APIs do.
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 main use case I was targeting was rotating an entire set of secrets from one key to another. That said, this could just operate on a single token, and we could leave it up to the consumer to handle larger groups. @alex is that what you'd prefer?
Yes, I would :-)
…On Fri, Oct 13, 2017 at 9:15 AM, Chris Wolfe ***@***.***> wrote:
***@***.**** commented on this pull request.
------------------------------
In src/cryptography/fernet.py
<#3979 (comment)>:
> @@ -134,6 +134,9 @@ def __init__(self, fernets):
def encrypt(self, msg):
return self._fernets[0].encrypt(msg)
+ def rotate(self, msgs, ttl=None):
+ return [self.encrypt(self.decrypt(m, ttl)) for m in msgs]
The main use case I was targeting was rotating an entire set of secrets
from one key to another. That said, this could just operate on a single
token, and we could leave it up to the consumer to handle larger groups.
@alex <https://github.com/alex> is that what you'd prefer?
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#3979 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AAADBOkW5lKFVR6HHjsyaFxDHSIQp9crks5sr2JYgaJpZM4P33Mm>
.
--
"I disapprove of what you say, but I will defend to the death your right to
say it." -- Evelyn Beatrice Hall (summarizing Voltaire)
"The people's good is the highest law." -- Cicero
GPG Key fingerprint: D1B3 ADC0 E023 8CA6
|
docs/fernet.rst
Outdated
@@ -109,6 +115,34 @@ has support for implementing key rotation via :class:`MultiFernet`. | |||
the front of the list to start encrypting new messages, and remove old keys | |||
as they are no longer needed. | |||
|
|||
.. method:: rotate(msg, ttl=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.
Needs a .. versionadded:: 2.2
docs/fernet.rst
Outdated
``ttl``, it is malformed, or | ||
it does not have a valid | ||
signature. | ||
:raises TypeError: This exception is raised if the ```msg`` is not |
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.
Double back ticks here
docs/fernet.rst
Outdated
|
||
:param bytes msg: The token to re-encrypt. | ||
:param int ttl: Optionally, the number of seconds old a message may be | ||
for it to be valid. If the message is older than |
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 is a petty style complaint but I prefer this with a single indent level for multiple lines and not aligned.
docs/fernet.rst
Outdated
@@ -99,6 +100,11 @@ has support for implementing key rotation via :class:`MultiFernet`. | |||
'...' | |||
>>> f.decrypt(token) | |||
'Secret message!' | |||
>>> key3 = Fernet(Fernet.generate_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 think we should move the doctest example to under the rotate method itself
add versionadded, drop rotate from class doctest
Can you pleaese add a changelog entry? (Haven't reviewed teh rest of this patch) |
Sure!
On Fri, Oct 13, 2017 at 12:26 Alex Gaynor ***@***.***> wrote:
Can you pleaese add a changelog entry? (Haven't reviewed teh rest of this
patch)
—
You are receiving this because you authored the thread.
Reply to this email directly, view it on GitHub
<#3979 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AAnJTYgwYncmFBF1G9rrgxmmOOBdqtNMks5sr51cgaJpZM4P33Mm>
.
--
- Chris
|
docs/fernet.rst
Outdated
|
||
Re-encrypts the provided fernet token. If the token has successfully | ||
been re-encrypted, then the re-encrypted token will be returned. This | ||
will raise an exception if re-encryption fails. |
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.
Please explain why someone would want this.
rotated = mf2.rotate(mf1_ciphertext) | ||
|
||
assert rotated != mf1_ciphertext | ||
assert mf2.decrypt(rotated) == plaintext |
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.
Please add an asseriotn that mf1
can't decrypt something rotated by mf2
.
mmm, I just realized this resets the timestamp in the token. Is that intended? is that good? |
@alex good point. I think it is also made somewhat worse by the caller being able to pass in the TTL, thereby setting up the assumption that token might contain the same timestamp that it did when encrypted. I don't really have a good solution for this. The following two options come to mind:
I do think that option #1 would be easier to understand and maintain. I also think that it most closely matches what a user would expect of a rotation function given Fernet's current interface. What do you think? Also, thank you for both for your reviews :) |
I think the right thing to do is probably reuse the original timestamp; though I'd be interested in hearing @reaperhulk's thought. The way to accomplish that would be to refactor the decrypt method to have an internal method that returned the timestamp, and then use that + the internal encrypt method. |
Didn't think of that! :) This does seem like a safer, more user friendly solution. |
My preference would be for the default of rotate be ttl preservation. We could entirely remove the ttl arg, but it does seem reasonable for callers who use ttl to rotate and update ttl. |
Sounds good - when I get some time this week, I'll make the changes. |
I actually think removing the |
@alex it doesn't seem out of the question for a caller to want to rotate the encryption key and update the ttl to a later time. Of course, to allow ttl alteration and rotation the ttl arg actually needs to have 3 types of values. Preserve, no ttl, and a new ttl. In encrypt we have If we want to wait for a user to request ttl update we could minimize our API surface area by not supporting ttl update for now. A future PR could add the above with no backwards compatibility concerns. |
There's two distinct things here: a) Changin the stored timestamp I don't believe we should do either (and therefore we can drop the |
I wrote a lot of words about various concerns I have but while doing so have talked myself into agreeing. Yes, since a Fernet token actually only encodes timestamp and the I'm not convinced that we shouldn't support |
src/cryptography/fernet.py
Outdated
return self._decrypt_data(data, timestamp, ttl) | ||
|
||
@staticmethod | ||
def _get_token_data(token): |
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.
Since this returns a timestamp that may be invalid (we haven't performed the HMAC verification), let's call this _get_unvalidated_token_data
.
(For our purposes it's okay to not have validated it yet as the call to _decrypt_data
does the HMAC check)
docs/fernet.rst
Outdated
@@ -86,7 +86,8 @@ has support for implementing key rotation via :class:`MultiFernet`. | |||
.. versionadded:: 0.7 | |||
|
|||
This class implements key rotation for Fernet. It takes a ``list`` of | |||
:class:`Fernet` instances, and implements the same API: | |||
:class:`Fernet` instances, and implements the same API with the exception |
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: there shouldn't be a comma here (You didn't add it, but let's go ahead and fix it now)
docs/fernet.rst
Outdated
|
||
.. versionadded:: 2.2 | ||
|
||
Rotating tokens is necessary when any of the fernet keys of a |
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 don't like this first sentence. Token rotation isn't just to limit the compromise window -- It's also for policy (rotation post-employee departure). Rotation in the event of actual compromise is a much more involved affair. Rotation as a best practice/cryptographic hygiene policy is designed to limit the damage in the event of an undetected event/increase the difficulty of attacks.
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.
@reaperhulk does this work a bit better?
Rotates a token by re-encrypting it under the :class:`MultiFernet`
instance's primary key. This preserves the timestamp that was originally
saved with the token. If a token has successfully been rotated then the
rotated token will be returned. If rotation fails this will raise an
exception.
Token rotation is a best practice and manner of cryptographic hygiene
designed to limit damage in the event of an undetected event and to
increase the difficulty of attacks. For example, it would be necessary
to rotate all fernet tokens in the event of an employee leaving who
previously had access to the fernet key used to encrypt/decrypt fernet
tokens.
I initially added the sentence "rotating tokens is necessary..." to try provide a reason for someone to want to use rotate
. I'm wondering if it might make more sense to pull the second part of the docstring I've proposed out of rotate
and into the docs for MultiFernet
itself.
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.
Yeah we have a section about key rotation in the multifernet class introduction so it probably does make sense to expand on it up there and link to the rotate
method.
docs/fernet.rst
Outdated
instance and then re-encrypts the token using the primary key. This | ||
preserves the timestamp that was originally saved with the token. | ||
|
||
If a token has successfully been rotated, then the rotated token will be |
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 comma and the one in the next sentence are both superfluous.
docs/fernet.rst
Outdated
the key. This is URL-safe base64-encoded. This is referred to as a | ||
"Fernet token". | ||
:raises cryptography.fernet.InvalidToken: If a ``token`` is in any | ||
way invalid, this exception is raised. A token may be invalid for a |
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 a token is invalid this exception is raised" is probably all we need here.
@@ -21,6 +21,7 @@ cryptographic | |||
cryptographically | |||
Debian | |||
decrypt | |||
decrypts |
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'd like to register my (continued) displeasure that enchant can't figure out plurals.
src/cryptography/fernet.py
Outdated
@@ -134,6 +141,20 @@ def __init__(self, fernets): | |||
def encrypt(self, msg): | |||
return self._fernets[0].encrypt(msg) | |||
|
|||
def rotate(self, msg): | |||
timestamp, data = self._fernets[0]._get_unverified_token_data(msg) |
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.
Since this is a static method we can just call Fernet._get_unverified_token_data
else: | ||
raise InvalidToken | ||
|
||
iv = os.urandom(16) |
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 does _encrypt_from_parts
take an iv
? The only other caller also does this os.urandom
call. I don't think we want to have two places in our code where we generate the random IV. Maybe we should modify _encrypt_from_parts
to just take data and timestamp and hoist iv generation into the method?
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 the tests.
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.
Ah. Well that's a good reason. I still don't like os.urandom being called in two places, but maybe I need to just be okay with that.
tests/test_fernet.py
Outdated
mf1_ciphertext = mf1.encrypt(plaintext) | ||
|
||
original_time, _ = Fernet._get_unverified_token_data(mf1_ciphertext) | ||
rotated_time, _ = Fernet._get_unverified_token_data( |
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 test would pass most of the time even if we weren't preserving timestamp since fernet timestamps have second granularity. I think we need to monkeypatch time to test this properly.
… due to calls occuring in less than one second
tests/test_fernet.py
Outdated
later = datetime.datetime.now() + datetime.timedelta(minutes=1) | ||
|
||
t1 = time.mktime(earlier.timetuple()) | ||
t2 = time.mktime(later.timetuple()) |
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 only need to patch time once, after encryption. I'll change the PR to do that.
… as part of re-encryption
@@ -109,6 +110,50 @@ has support for implementing key rotation via :class:`MultiFernet`. | |||
the front of the list to start encrypting new messages, and remove old keys | |||
as they are no longer needed. | |||
|
|||
Token rotation as offered by :meth:`MultiFernet.rotate` is a best practice |
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 language still bugs me but I don't have constructive improvement to offer.
This addresses #3882.
Adds a
rotate(msgs, ttl)
method toMultifernet
to make it a easier to re-encrypt old tokens under a new primary key.Thanks!