Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@BeryJu
Copy link

@BeryJu BeryJu commented Apr 11, 2022

The libtrust library always generates the kid like this

	// KeyID returns a distinct identifier which is unique to this Public Key.
	// The format generated by this library is a base32 encoding of a 240 bit
	// hash of the public key data divided into 12 groups like so:
	//    ABCD:EFGH:IJKL:MNOP:QRST:UVWX:YZ23:4567:ABCD:EFGH:IJKL:MNOP

However, 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.

Copy link
Member

@milosgajdos milosgajdos left a 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.

@milosgajdos milosgajdos requested a review from wy65701436 May 3, 2022 07:05
@codecov-commenter
Copy link

Codecov Report

Merging #3625 (02ffeea) into main (cd51f38) will increase coverage by 0.23%.
The diff coverage is n/a.

@@            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     
Impacted Files Coverage Δ
...ribution/distribution/registry/auth/token/token.go 55.78% <0.00%> (-0.47%) ⬇️
...hub.com/distribution/distribution/health/health.go 45.26% <0.00%> (ø)
...hub.com/distribution/distribution/digestset/set.go 82.14% <0.00%> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update cd51f38...02ffeea. Read the comment docs.

@BeryJu
Copy link
Author

BeryJu commented Jun 4, 2022

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

@milosgajdos
Copy link
Member

CC: @thaJeztah @distribution/maintainers

signingKey, err = parseAndVerifyRawJWK(rawJWK, verifyOpts)
signingKey, err := parseAndVerifyRawJWK(rawJWK, verifyOpts)
return []libtrust.PublicKey{signingKey}, err
case len(keyID) > 0:
Copy link
Member

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

// `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?)

Copy link
Author

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

Copy link
Member

@milosgajdos milosgajdos Jun 6, 2022

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.

Copy link
Author

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

Copy link
Collaborator

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.

@milosgajdos milosgajdos requested a review from thaJeztah July 13, 2022 08:05
@BeryJu
Copy link
Author

BeryJu commented Sep 14, 2022

Bump again, I'd still like to see this merged

@BeryJu
Copy link
Author

BeryJu commented Dec 23, 2022

Bump again, still an issue

@milosgajdos milosgajdos requested review from corhere and squizzi May 10, 2023 08:18
Copy link
Collaborator

@corhere corhere left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@BeryJu
Copy link
Author

BeryJu commented May 11, 2023

Cheers @corhere, I agree that removing the kid check all together is not the best solution. I think the best solution would be the option to configure a JWKS json document (maybe even a URL?) as an alternative to configuring a single/multiple PEM certificates. I'm gonna have a try at creating a new PR for this (I haven't looked around the code in a while but shouldn't be too hard)

@davidspek
Copy link
Collaborator

@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.

@BeryJu
Copy link
Author

BeryJu commented Aug 15, 2023

@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

@milosgajdos
Copy link
Member

Closing as the libtrust has been deprecated in #4096 in favour of go-jose/go-jose.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants