-
Notifications
You must be signed in to change notification settings - Fork 2.7k
don't check kid, check jwt signature against all keys #3625
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
Conversation
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 great catch. Thanks for the PR!
One thing I'm wondering is the speed, I guess with a reasonable number of loaded keys should be ok.
#2875 Signed-off-by: Jens Langhammer <[email protected]>
Codecov Report
@@ Coverage Diff @@
## main #3625 +/- ##
==========================================
+ Coverage 56.33% 56.57% +0.23%
==========================================
Files 101 103 +2
Lines 7313 7523 +210
==========================================
+ Hits 4120 4256 +136
- Misses 2536 2598 +62
- Partials 657 669 +12
Continue to review full report at Codecov.
|
|
Hiya, sorry for bumping this but I'd quite like to see this get merged, mostly so I can stop building my own distribution images, and it would benefit a couple other people too |
|
CC: @thaJeztah @distribution/maintainers |
| signingKey, err = parseAndVerifyRawJWK(rawJWK, verifyOpts) | ||
| signingKey, err := parseAndVerifyRawJWK(rawJWK, verifyOpts) | ||
| return []libtrust.PublicKey{signingKey}, err | ||
| case len(keyID) > 0: |
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'm not very familiar with this logic, so would appreciate more eyes on this. (Perhaps @dmcgowan has time to have a look to double check?)
Look like this at least needs an update in the function's documentation
distribution/registry/auth/token/token.go
Lines 192 to 194 in ffbd94c
| // `kid` - The unique identifier for the key. This library interprets it | |
| // as a libtrust fingerprint. The key itself can be looked up in | |
| // the trustedKeys field of the given verify options. |
I'm a bit confused though; what's the purpose of keyID now? If it was previously to specify which key to verify against, now it is fully ignored (any non-empty string would do?) and any key present in verifyOpts.TrustedKeys is accepted?
If keyID is no longer needed, should verifyOpts.TrustedKeys be changed to a slice instead of a map, and this function check for len(verifyOpts.TrustedKeys) > 0?
The new logic treats all rootcerts that are loaded equal; is that a safe assumption? Again, I'm not an expert in this area, but could there be (e.g.) intermediates that are now accepted, but should not?.
Looking at https://datatracker.ietf.org/doc/html/rfc7517#section-4.5
4.5. "kid" (Key ID) Parameter
The "kid" (key ID) parameter is used to match a specific key. This
is used, for instance, to choose among a set of keys within a JWK Set
during key rollover. The structure of the "kid" value is
unspecified. When "kid" values are used within a JWK Set, different
keys within the JWK Set SHOULD use distinct "kid" values. (One
example in which different keys might use the same "kid" value is if
they have different "kty" (key type) values but are considered to be
equivalent alternatives by the application using them.) The "kid"
value is a case-sensitive string. Use of this member is OPTIONAL.
When used with JWS or JWE, the "kid" value is used to match a JWS or
JWE "kid" Header Parameter value.That wording makes me think that the kid should NOT be ignored ("is used to match a specific key"), and in general, key-ID's SHOULD be unique ("different keys within the JWK Set SHOULD use distinct "kid" values"), BUT, based on this sentence;
One example in which different keys might use the same "kid" value is if they have different "kty" (key type) values but are considered to be equivalent alternatives by the application using them.
It looks like a single kid MAY have multiple alternatives (in different formats, but considered equivalent). If my interpretation of that is correct, then trustedKeys should be something like map[string][]libtrust.PublicKey (single keyID having multiple alternative public-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.
As far as I understand, the JWK Key ID is used in the JWKS URI from OpenID, where multiple keys might be specified (see https://code.beryju.org/oauth/discovery/keys), and in that case the Key ID used in that document needs to match the Key ID in the JWT, as well as signature being validated with that same key.
Since this project directly compares against a list of certificates instead of JWK-formatted key entries, there's no way to pre-specify the key ID since it is not encoded in the certificate in any way
I also don't think this will allow the loading of keys from different certificates, since a JWT is always signed by a single public/private key pair, and there is no certificate chain embedded into the signature, so given certificateA is issued by intermediateA, and a JWT signed by certificateA, it will only be validated with the certificateA and not intermediateA
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.
CC: @justincormack , can you please weigh in here so we move forward with this.
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.
Sorry to bump this again, it's already been another month and I'd like to stop using my local dev build in production to circumvent this
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 a JWT is always signed by a single public/private key pair, and there is no certificate chain embedded into the signature, so given certificateA is issued by intermediateA, and a JWT signed by certificateA, it will only be validated with the certificateA and not intermediateA
That is incorrect. JSON Web Signatures can and do embed the certificate chain so that tokens signed by certificateA can be verified using the CA certificate which issued intermediateA.
|
Bump again, I'd still like to see this merged |
|
Bump again, still an issue |
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.
distribution/docs/spec/auth/jwt.md
Lines 66 to 71 in 8e29e87
| The header of a JSON Web Token is a standard JOSE header. The "typ" field | |
| will be "JWT" and it will also contain the "alg" which identifies the | |
| signing algorithm used to produce the signature. It also must have a "kid" | |
| field, representing the ID of the key which was used to sign the token. | |
| The "kid" field has to be in a libtrust fingerprint compatible format. |
I can completely understand the desire to use third-party IdP's to issue registry bearer tokens, and the need to make distribution more widely compatible with IdP's. But I don't think that disregarding the signature's kid header and trying to verify the signature against every configured trusted key is the right solution. While it may not have too much security impact today (aside from possibly enabling timing attacks) to start ignoring the kid signature header value, we'd be stuck with that behaviour "forever" as it would then be a breaking change to begin enforcing kid values again. For example, reusing a keypair is common practice when renewing an PKIX certificate, so without kid validation we would not be able to discriminate between tokens signed by an expired certificate vs. the valid one when the certificate is only referenced by kid and not embedded with x5c.
Distribution requiring that Key IDs be derived from the public key using the libtrust algorithm is dumb. Let's fix that by allowing the Key ID for each trusted root certificate to be loaded. One option is to add support for loading JWKS files, for which kid is part of the standard. Or the Key ID could be loaded from a PEM header, which is non-standard but easily added with a text editor.
|
Cheers @corhere, I agree that removing the |
|
@BeryJu Have you since created a new PR as you've mentioned in your last comment? What's the current state of this PR? If the decision is to not merge this PR we should close it. |
@davidspek Hi david, I wanted to make a PR for that but I haven't been able to get around to it yet |
|
Closing as the We cut a new release last night, feel free to take it for a spin https://github.com/distribution/distribution/releases/tag/v3.0.0-alpha.1 and report any issues. If you still face issues with cert validation please open a new issue and/or PR against the latest code. |
The libtrust library always generates the
kidlike thisHowever, according to the RFC this format is not standard (see https://datatracker.ietf.org/doc/html/rfc7517#section-4.5), and different IDPs use different formats (which is what people in #2875 (and also me now) are running into)
This PR changes the logic to check the signature against every key that was loaded from the bundle. If any validation matches, the key is valid, and if none matches it's not.