1
0
mirror of https://github.com/distribution/distribution.git synced 2025-05-07 07:37:17 +00:00

fix: Add the token's rootcert public key to the list of known keys ()

This commit is contained in:
Milos Gajdos 2024-10-02 18:51:54 +01:00 committed by GitHub
commit 2c7d93a0b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 177 additions and 14 deletions

View File

@ -666,6 +666,11 @@ Default `signingalgorithms`:
- PS384
- PS512
Additional notes on `rootcertbundle`:
- The public key of this certificate will be automatically added to the list of known keys.
- The public key will be identified by it's [RFC7638 Thumbprint](https://datatracker.ietf.org/doc/html/rfc7638).
For more information about Token based authentication configuration, see the
[specification](../spec/auth/token.md).

View File

@ -12,7 +12,7 @@ Web Token schema that `distribution/distribution` has adopted to implement the
client-opaque Bearer token issued by an authentication service and
understood by the registry.
This document borrows heavily from the [JSON Web Token Draft Spec](https://tools.ietf.org/html/draft-ietf-oauth-json-web-token-32)
This document borrows heavily from the [JSON Web Token Spec: RFC7519](https://datatracker.ietf.org/doc/html/rfc7519)
## Getting a Bearer Token
@ -63,14 +63,19 @@ Token has 3 main parts:
1. Headers
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 header of a JSON Web Token is a standard JOSE header compliant with
[Section 5 of RFC7519](https://datatracker.ietf.org/doc/html/rfc7515#section-5).
It specifies that this object is going to be a JSON Web token signed using
the key with the given ID using the Elliptic Curve signature algorithm
using a SHA256 hash.
It **must** have:
* `alg` **(Algorithm)**: Identifies the signing algorithm used to produce the signature.
* `typ` **(Type)**: Must be equal to `JWT` as recommended by [Section 5.1 RFC7519](https://datatracker.ietf.org/doc/html/rfc7519#section-5.1)
It should have at least one of:
* `kid` **(KeyID)**: Represents the ID of the key which was used to sign the token.
* `jwk` **(JWK)**: Represents the public key used to sign the token, compliant with [RFC7517](https://datatracker.ietf.org/doc/html/rfc7517)
* `x5c` **(X.509 Certificate Chain)**: Represents the chain of certificates used to sign the token.
2. Claim Set
@ -226,7 +231,7 @@ Token has 3 main parts:
This is then used as the payload to a the `ES256` signature algorithm
specified in the JOSE header and specified fully in [Section 3.4 of the JSON Web Algorithms (JWA)
draft specification](https://tools.ietf.org/html/draft-ietf-jose-json-web-algorithms-38#section-3.4)
specification](https://datatracker.ietf.org/doc/html/rfc7518)
This example signature will use the following ECDSA key for the server:
@ -281,6 +286,12 @@ This is also described in [Section 2.1 of RFC 6750: The OAuth 2.0 Authorization
The registry must now verify the token presented by the user by inspecting the
claim set within. The registry will:
- Ensure that the certificate chain provided (in the `x5c` header) is valid.
- If it fails (eg. `x5c` header not present), then the registry will either:
- If provided, verify the provided JWK (in the `jwt` header) in the JWT is
known or trusted.
- If provided, verify that the provided KeyID (in the `kid` header) is a
known (as per configured in `auth.token.jwks` config).
- Ensure that the issuer (`iss` claim) is an authority it trusts.
- Ensure that the registry identifies as the audience (`aud` claim).
- Check that the current time is between the `nbf` and `exp` claim times.

View File

@ -16,7 +16,8 @@ This document outlines the v2 Distribution registry authentication scheme:
3. The registry client makes a request to the authorization service for a
Bearer token.
4. The authorization service returns an opaque Bearer token representing the
client's authorized access.
client's authorized access. The token must comply with the structure
described in the [Token Authentication Implementation page](./jwt.md).
5. The client retries the original request with the Bearer token embedded in
the request's Authorization header.
6. The Registry authorizes the client by validating the Bearer token and the

View File

@ -238,6 +238,11 @@ func checkOptions(options map[string]interface{}) (tokenAccessOptions, error) {
return opts, nil
}
var (
rootCertFetcher func(string) ([]*x509.Certificate, error) = getRootCerts
jwkFetcher func(string) (*jose.JSONWebKeySet, error) = getJwks
)
func getRootCerts(path string) ([]*x509.Certificate, error) {
fp, err := os.Open(path)
if err != nil {
@ -316,14 +321,14 @@ func newAccessController(options map[string]interface{}) (auth.AccessController,
)
if config.rootCertBundle != "" {
rootCerts, err = getRootCerts(config.rootCertBundle)
rootCerts, err = rootCertFetcher(config.rootCertBundle)
if err != nil {
return nil, err
}
}
if config.jwks != "" {
jwks, err = getJwks(config.jwks)
jwks, err = jwkFetcher(config.jwks)
if err != nil {
return nil, err
}
@ -334,12 +339,15 @@ func newAccessController(options map[string]interface{}) (auth.AccessController,
return nil, errors.New("token auth requires at least one token signing key")
}
trustedKeys := make(map[string]crypto.PublicKey)
rootPool := x509.NewCertPool()
for _, rootCert := range rootCerts {
rootPool.AddCert(rootCert)
if key := GetRFC7638Thumbprint(rootCert.PublicKey); key != "" {
trustedKeys[key] = rootCert.PublicKey
}
}
trustedKeys := make(map[string]crypto.PublicKey)
if jwks != nil {
for _, key := range jwks.Keys {
trustedKeys[key.KeyID] = key.Public()

View File

@ -1,9 +1,16 @@
package token
import (
"testing"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"net/http"
"net/http/httptest"
"testing"
"github.com/go-jose/go-jose/v4"
)
func TestBuildAutoRedirectURL(t *testing.T) {
@ -87,3 +94,86 @@ func TestCheckOptions(t *testing.T) {
t.Fatal("autoredirectpath should be /auth/token")
}
}
func mockGetRootCerts(path string) ([]*x509.Certificate, error) {
caPrivKey, err := rsa.GenerateKey(rand.Reader, 1024) // not to slow down the test that much
if err != nil {
return nil, err
}
ca := &x509.Certificate{
PublicKey: &caPrivKey.PublicKey,
}
return []*x509.Certificate{ca}, nil
}
func mockGetJwks(path string) (*jose.JSONWebKeySet, error) {
return &jose.JSONWebKeySet{
Keys: []jose.JSONWebKey{
{
KeyID: "sample-key-id",
},
},
}, nil
}
func TestRootCertIncludedInTrustedKeys(t *testing.T) {
old := rootCertFetcher
rootCertFetcher = mockGetRootCerts
defer func() { rootCertFetcher = old }()
realm := "https://auth.example.com/token/"
issuer := "test-issuer.example.com"
service := "test-service.example.com"
options := map[string]interface{}{
"realm": realm,
"issuer": issuer,
"service": service,
"rootcertbundle": "something-to-trigger-our-mock",
"autoredirect": true,
"autoredirectpath": "/auth",
}
ac, err := newAccessController(options)
if err != nil {
t.Fatal(err)
}
// newAccessController return type is an interface built from
// accessController struct. The type check can be safely ignored.
ac2, _ := ac.(*accessController)
if got := len(ac2.trustedKeys); got != 1 {
t.Fatalf("Unexpected number of trusted keys, expected 1 got: %d", got)
}
}
func TestJWKSIncludedInTrustedKeys(t *testing.T) {
old := jwkFetcher
jwkFetcher = mockGetJwks
defer func() { jwkFetcher = old }()
realm := "https://auth.example.com/token/"
issuer := "test-issuer.example.com"
service := "test-service.example.com"
options := map[string]interface{}{
"realm": realm,
"issuer": issuer,
"service": service,
"jwks": "something-to-trigger-our-mock",
"autoredirect": true,
"autoredirectpath": "/auth",
}
ac, err := newAccessController(options)
if err != nil {
t.Fatal(err)
}
// newAccessController return type is an interface built from
// accessController struct. The type check can be safely ignored.
ac2, _ := ac.(*accessController)
if got := len(ac2.trustedKeys); got != 1 {
t.Fatalf("Unexpected number of trusted keys, expected 1 got: %d", got)
}
}

View File

@ -1,5 +1,15 @@
package token
import (
"crypto"
"crypto/ecdsa"
"crypto/rsa"
"crypto/sha256"
"encoding/base64"
"fmt"
"math/big"
)
// actionSet is a special type of stringSet.
type actionSet struct {
stringSet
@ -36,3 +46,41 @@ func containsAny(ss []string, q []string) bool {
return false
}
// NOTE: RFC7638 does not prescribe which hashing function to use, but suggests
// sha256 as a sane default as of time of writing
func hashAndEncode(payload string) string {
shasum := sha256.Sum256([]byte(payload))
return base64.RawURLEncoding.EncodeToString(shasum[:])
}
// RFC7638 states in section 3 sub 1 that the keys in the JSON object payload
// are required to be ordered lexicographical order. Golang does not guarantee
// order of keys[0]
// [0]: https://groups.google.com/g/golang-dev/c/zBQwhm3VfvU
//
// The payloads are small enough to create the JSON strings manually
func GetRFC7638Thumbprint(publickey crypto.PublicKey) string {
var payload string
switch pubkey := publickey.(type) {
case *rsa.PublicKey:
e_big := big.NewInt(int64(pubkey.E)).Bytes()
e := base64.RawURLEncoding.EncodeToString(e_big)
n := base64.RawURLEncoding.EncodeToString(pubkey.N.Bytes())
payload = fmt.Sprintf(`{"e":"%s","kty":"RSA","n":"%s"}`, e, n)
case *ecdsa.PublicKey:
params := pubkey.Params()
crv := params.Name
x := base64.RawURLEncoding.EncodeToString(params.Gx.Bytes())
y := base64.RawURLEncoding.EncodeToString(params.Gy.Bytes())
payload = fmt.Sprintf(`{"crv":"%s","kty":"EC","x":"%s","y":"%s"}`, crv, x, y)
default:
return ""
}
return hashAndEncode(payload)
}