-
Notifications
You must be signed in to change notification settings - Fork 361
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
Native support for key rotation in verifications #170
Conversation
note: this is ported from PR from the old repo: dgrijalva/jwt-go#372 |
Thanks for submitting a PR, this is actually quite interesting. I've had to do something similar in previous projects I've worked on so can certainly see this being useful. I'll scope out some time to review this and make sure it covers any corner cases. Would also be interested what others think.. |
a side note is that we already forked the original project with this change (because there was no one reviewing the original pr), and have been running this version in our production for years now :) see: |
Definitely an interesting idea. I have two main concerns with the approach and we need to discuss if/how we deal with them:
Some random thoughts:
|
@oxisto Thanks for the feedback!
That's simply because EdDSA support was added after my original PR :) I just added that in a fixup commit. I also added benchhmark test for RS256 in another fixup commit, with 3 rotating keys (last one being the good one). Here are the numbers:
So although time-wise it's mostly negligible, memory-wise it saves ~1/4 of the memory and ~1/2 of the allocations in both good and claim already expired cases. Besides performance, another benefit of native key rotation support is error handling. There's potentially a case that the payload has a valid signature, but the claim itself is having issues. With naive, manual key rotation (try each key one by one), the implementation usually needs to do something like this: var lastErr error
for _, key := range keys {
token, err := tryValidatePayload(payload, key)
if err == nil {
return token, nil
}
lastErr = err
}
return nil, lastErr if the first key was valid (but it got other claim errors), the actual error could be masked by signature errors caused by later keys, so instead of getting the actual error about the claim, they got a key error instead, which could be hard to debug issues. (if this PR is approved I'll squash the commits, the fixup commits are just for easier reviewing) |
btw if I change from parallel benchmark to sequential benchmark, the time saving is slightly more significant:
|
@fishy Thanks for providing the benchmarks, I will go over them over on the next couple of days. Thanks for adding EdDSA! Don't worry about adding more commits, we are using the squash and merge strategy, so we will squash them on our end eventually. |
That doesn't preserve my git commit message, so I always squash myself and keep the git commit message sane/meaningful :) |
invalidKey.(ed25519.PublicKey), | ||
ed25519Key.(ed25519.PublicKey), |
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: the typecastings here are needed because ParseEdPublicKeyFromPEM
returns crypto.PublicKey
instead of ed25519.PublicKey
, which, in my opinion, was a big mistake (all other similar functions, for example ParseRSAPublicKeyFromPEM
, returns the expected type).
I can see this could become a footgun, as people might try to return []crypto.PublicKey
in their Keyfunc
. If we think that's a problem we want to prevent, there are two ways to do that:
- Make
SigningMethodEd25519.Verify
to also accept[]crypto.PublicKey
, but this will make that one more special than others; - Deprecate
ParseEdPublicKeyFromPEM
and use a different named function to returned25519.PublicKey
. If we go this route we probably would want to do the same withParseEdPrivateKeyFromPEM
.
(We can't just fix ParseEdPublicKeyFromPEM
because that's a breaking change, unfortunately)
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.
Ouch, indeed special casing it by returning an interface was probably not the best approach.
Given these 2 options I think I'd vote for option 1. Although it's my least favorite because it requires doubling-down on making this one even more special. The benefit is we get to keep this an implementation detail of the library instead of punting the problem to the public API.
With option 2 even though we deprecate it, to truly get rid of it would require a major version release. And I think we want to keep the scope of this within the current /v4 release and save breaking changes/re-thinking the package design in /v5.
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.
done in new commit
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.
Thanks for putting that through, overall this looks good to me given the available options.
What are your thoughts about? Not advocating for a change in this PR, just curious to tease this out a bit.
This isn't a bad idea, I really wish |
I don't think that would resolve any of the problems. As in most cases people are not using |
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 looks good to me, let's see if anyone else has an opinion.
Add native support for key rotation for ES*, Ed*, HS*, RS*, and PS* verifications. In those SigningMethod's Verify implementations, also allow the key to be the type of the slice of the supported key type, so that the caller can implement the KeyFunc to return all the accepted keys together to support key rotation. While key rotation verification can be done on the callers' side without this change, this change provides better performance because: - When trying the next key, the steps before actually using the key do not need to be performed again. - If a verification process failed for non-key reasons (for example, because it's already expired), it saves the effort to try the next key. The native key rotation support also helps callers to get more accurate errors.
force pushed with a few minor comment updates, here are the diff from the previous state: $ git diff --cached
diff --git a/ed25519.go b/ed25519.go
index d62de12..5f0f565 100644
--- a/ed25519.go
+++ b/ed25519.go
@@ -33,7 +33,6 @@ func (m *SigningMethodEd25519) Alg() string {
}
// Verify implements token verification for the SigningMethod.
-// For this verify method, key must be an ed25519.PublicKey
// For this verify method, key must be in types of one of ed25519.PublicKey,
// []ed25519.PublicKey, or []crypto.PublicKey (slice types for rotation keys),
// and each key must be of the size ed25519.PublicKeySize.
diff --git a/hmac.go b/hmac.go
index 928bbca..c9a2d7e 100644
--- a/hmac.go
+++ b/hmac.go
@@ -46,6 +46,7 @@ func (m *SigningMethodHMAC) Alg() string {
}
// Verify implements token verification for the SigningMethod. Returns nil if the signature is valid.
+// key must be of type of either []byte (a single key) or [][]byte (rotation keys).
func (m *SigningMethodHMAC) Verify(signingString, signature string, key interface{}) error {
// Verify the keys are the right types
var keys [][]byte
diff --git a/rsa_pss.go b/rsa_pss.go
index 3ad7f28..497f7be 100644
--- a/rsa_pss.go
+++ b/rsa_pss.go
@@ -81,7 +81,6 @@ func init() {
}
// Verify implements token verification for the SigningMethod.
-// Implements the Verify method from SigningMethod
// For this verify method, key must be in the types of either *rsa.PublicKey or
// []*rsa.PublicKey (for rotation keys).
func (m *SigningMethodRSAPSS) Verify(signingString, signature string, key interface{}) error { |
how long do we have to wait? :) |
This is an open source project maintained by a group of people which maintain it in their spare, free time (unpaid). Therefore it might take a while until a verdict is reached. Personally, I am still struggling a little bit with the overall approach, given that this PR rewrites a lot of the core functionality of this library (although the public API does not change) for a use case that only applies to certain limited scenarios. The more I think about this, the less am I convinced that this a good approach in general. Implementing key rotation using standards like JWKS (which is supported through the excellent library https://github.com/MicahParks/keyfunc) is probably a more elegant fit. That is of course very opinionated of me and maybe some of the other maintainers can help decide how to move forward with this. |
Ye, I'll echo what @oxisto said .. we are trying our best to maintain this project with the limited bandwidth we have. It's useful to get a few 👀 on a given PR, esp. if it touches the core of the project. A bug would affect a lot of downstream consumers. We have to be extremely careful... so the delay in merging PR's is more the result of being cautious. We also don't always agree on certain implementations, but that's perfectly normal for an open-source project. Although I'm in favor of this PR, I value that other users and maintainers disagree, which in turn makes this project better overall. |
It's been a while, have any of the opinions changed? I need to be able to rotate keys & would rather use this code vs. write my own duplicate if possible. Key rotation seems like a very basic use case and having the library include that would be pretty awesome if possible. |
@oxisto / @mfridman if you're not happy with this approach for supporting multiple keys, do you have a path that you'd like for this use case? The interface for providing the key to try results in some fairly convoluted code being needed:
Alternatively the JWT can be decoded "carefully" to determine the algo, which slightly simplifies things, but there's still a fair number of the same steps above. As @fishy pointed out, the ideal time to try 2 or 3 keys is after everything is setup. I'm not trying to push the approach in this PR, but I'd like a way that complements this library and it's thoughtful design. I'm happy to help, but I'm not sure how at the present. |
My major concern was (and still is) that the approach presented in this PR touches all existing signing methods and forces them to loop over a key. So we are stuck with a lot of extra code that we (@mfridman and I) need to maintain and is only applicable for a small percentage of our users. Personally, I still don't really see the use case. What you probably want to have flexibility in terms of exchanging keys is setting up JWKS (see above) and rotate the keys in the set. Although this still leaves you with the issue that some clients might still have old key material, I get that. Looking forward, what I could possibly get on board with is that we deal with the multiple keys issue directly in Lines 83 to 96 in 8b7470d
The "good" thing is that the existing Hopefully this small "is it an array?" check is negilble in terms of perfomance, but of course it would be good to have some numbers on this after the implementation. The "bad" thing still remains that we are sort of mis-using |
Closing this in favor of #344 I think we've settled on a descent (and simpler) design while keeping things type-safe. |
Add native support for key rotation for ES*, Ed*, HS*, RS*, and PS*
verifications.
In those SigningMethod's Verify implementations, also allow the key to
be the type of the slice of the supported key type, so that the caller
can implement the KeyFunc to return all the accepted keys together to
support key rotation.
While key rotation verification can be done on the callers' side without
this change, this change provides better performance because:
When trying the next key, the steps before actually using the key do
not need to be performed again.
If a verification process failed for non-key reasons (for example,
because it's already expired), it saves the effort to try the next
key.
The native key rotation support also helps callers to get more accurate
errors.