feature: Bump go-jose and require signing algorithms in auth

This bumps go-jose to the latest available version: v4.0.3.
This slightly breaks the backwards compatibility with the existing
registry deployments but brings more security with it.

We now require the users to specify the list of token signing algorithms in
the configuration. We do strive to maintain the b/w compat by providing
a list of supported algorithms, though, this isn't something we
recommend due to security issues, see:
* https://github.com/go-jose/go-jose/issues/64
* https://github.com/go-jose/go-jose/pull/69

As part of this change we now return to the original flow of the token
signature validation:
1. X2C (tls) headers
2. JWKS
3. KeyID

Signed-off-by: Milos Gajdos <milosthegajdos@gmail.com>
This commit is contained in:
Milos Gajdos
2024-05-14 09:21:38 +01:00
parent 56a020f7f1
commit 52d68216c0
46 changed files with 628 additions and 319 deletions

View File

@@ -14,7 +14,7 @@ import (
"strings"
"github.com/distribution/distribution/v3/registry/auth"
"github.com/go-jose/go-jose/v3"
"github.com/go-jose/go-jose/v4"
"github.com/sirupsen/logrus"
)
@@ -151,13 +151,14 @@ func (ac authChallenge) SetHeaders(r *http.Request, w http.ResponseWriter) {
// accessController implements the auth.AccessController interface.
type accessController struct {
realm string
autoRedirect bool
autoRedirectPath string
issuer string
service string
rootCerts *x509.CertPool
trustedKeys map[string]crypto.PublicKey
realm string
autoRedirect bool
autoRedirectPath string
issuer string
service string
rootCerts *x509.CertPool
trustedKeys map[string]crypto.PublicKey
signingAlgorithms []jose.SignatureAlgorithm
}
const (
@@ -167,13 +168,14 @@ const (
// tokenAccessOptions is a convenience type for handling
// options to the constructor of an accessController.
type tokenAccessOptions struct {
realm string
autoRedirect bool
autoRedirectPath string
issuer string
service string
rootCertBundle string
jwks string
realm string
autoRedirect bool
autoRedirectPath string
issuer string
service string
rootCertBundle string
jwks string
signingAlgorithms []string
}
// checkOptions gathers the necessary options
@@ -206,7 +208,7 @@ func checkOptions(options map[string]interface{}) (tokenAccessOptions, error) {
if ok {
autoRedirect, ok := autoRedirectVal.(bool)
if !ok {
return opts, fmt.Errorf("token auth requires a valid option bool: autoredirect")
return opts, errors.New("token auth requires a valid option bool: autoredirect")
}
opts.autoRedirect = autoRedirect
}
@@ -215,7 +217,7 @@ func checkOptions(options map[string]interface{}) (tokenAccessOptions, error) {
if ok {
autoRedirectPath, ok := autoRedirectPathVal.(string)
if !ok {
return opts, fmt.Errorf("token auth requires a valid option string: autoredirectpath")
return opts, errors.New("token auth requires a valid option string: autoredirectpath")
}
opts.autoRedirectPath = autoRedirectPath
}
@@ -224,6 +226,15 @@ func checkOptions(options map[string]interface{}) (tokenAccessOptions, error) {
}
}
signingAlgos, ok := options["signingalgorithms"]
if ok {
signingAlgorithmsVals, ok := signingAlgos.([]string)
if !ok {
return opts, errors.New("signingalgorithms must be a list of signing algorithms")
}
opts.signingAlgorithms = signingAlgorithmsVals
}
return opts, nil
}
@@ -279,6 +290,18 @@ func getJwks(path string) (*jose.JSONWebKeySet, error) {
return &jwks, nil
}
func getSigningAlgorithms(algos []string) ([]jose.SignatureAlgorithm, error) {
signAlgVals := make([]jose.SignatureAlgorithm, 0, len(algos))
for _, alg := range algos {
alg, ok := signingAlgorithms[alg]
if !ok {
return nil, fmt.Errorf("unsupported signing algorithm: %s", alg)
}
signAlgVals = append(signAlgVals, alg)
}
return signAlgVals, nil
}
// newAccessController creates an accessController using the given options.
func newAccessController(options map[string]interface{}) (auth.AccessController, error) {
config, err := checkOptions(options)
@@ -289,6 +312,7 @@ func newAccessController(options map[string]interface{}) (auth.AccessController,
var (
rootCerts []*x509.Certificate
jwks *jose.JSONWebKeySet
signAlgos []jose.SignatureAlgorithm
)
if config.rootCertBundle != "" {
@@ -322,14 +346,25 @@ func newAccessController(options map[string]interface{}) (auth.AccessController,
}
}
signAlgos, err = getSigningAlgorithms(config.signingAlgorithms)
if err != nil {
return nil, err
}
if len(signAlgos) == 0 {
// NOTE: this is to maintain backwards compat
// with existing registry deployments
signAlgos = defaultSigningAlgorithms
}
return &accessController{
realm: config.realm,
autoRedirect: config.autoRedirect,
autoRedirectPath: config.autoRedirectPath,
issuer: config.issuer,
service: config.service,
rootCerts: rootPool,
trustedKeys: trustedKeys,
realm: config.realm,
autoRedirect: config.autoRedirect,
autoRedirectPath: config.autoRedirectPath,
issuer: config.issuer,
service: config.service,
rootCerts: rootPool,
trustedKeys: trustedKeys,
signingAlgorithms: signAlgos,
}, nil
}
@@ -350,7 +385,7 @@ func (ac *accessController) Authorized(req *http.Request, accessItems ...auth.Ac
return nil, challenge
}
token, err := NewToken(rawToken)
token, err := NewToken(rawToken, ac.signingAlgorithms)
if err != nil {
challenge.err = err
return nil, challenge

View File

@@ -4,6 +4,7 @@ import (
"testing"
fuzz "github.com/AdaLogics/go-fuzz-headers"
"github.com/go-jose/go-jose/v4"
)
func FuzzToken1(f *testing.F) {
@@ -18,7 +19,7 @@ func FuzzToken1(f *testing.F) {
if err != nil {
return
}
token, err := NewToken(rawToken)
token, err := NewToken(rawToken, []jose.SignatureAlgorithm{jose.EdDSA, jose.RS384})
if err != nil {
return
}

View File

@@ -7,8 +7,8 @@ import (
"fmt"
"time"
"github.com/go-jose/go-jose/v3"
"github.com/go-jose/go-jose/v3/jwt"
"github.com/go-jose/go-jose/v4"
"github.com/go-jose/go-jose/v4/jwt"
log "github.com/sirupsen/logrus"
"github.com/distribution/distribution/v3/registry/auth"
@@ -23,6 +23,38 @@ const (
Leeway = 60 * time.Second
)
var signingAlgorithms = map[string]jose.SignatureAlgorithm{
"EdDSA": jose.EdDSA,
"HS256": jose.HS256,
"HS384": jose.HS384,
"HS512": jose.HS512,
"RS256": jose.RS256,
"RS384": jose.RS384,
"RS512": jose.RS512,
"ES256": jose.ES256,
"ES384": jose.ES384,
"ES512": jose.ES512,
"PS256": jose.PS256,
"PS384": jose.PS384,
"PS512": jose.PS512,
}
var defaultSigningAlgorithms = []jose.SignatureAlgorithm{
jose.EdDSA,
jose.HS256,
jose.HS384,
jose.HS512,
jose.RS256,
jose.RS384,
jose.RS512,
jose.ES256,
jose.ES384,
jose.ES512,
jose.PS256,
jose.PS384,
jose.PS512,
}
// Errors used by token parsing and verification.
var (
ErrMalformedToken = errors.New("malformed token")
@@ -69,8 +101,8 @@ type VerifyOptions struct {
// NewToken parses the given raw token string
// and constructs an unverified JSON Web Token.
func NewToken(rawToken string) (*Token, error) {
token, err := jwt.ParseSigned(rawToken)
func NewToken(rawToken string, signingAlgs []jose.SignatureAlgorithm) (*Token, error) {
token, err := jwt.ParseSigned(rawToken, signingAlgs)
if err != nil {
return nil, ErrMalformedToken
}
@@ -140,6 +172,13 @@ func (t *Token) VerifySigningKey(verifyOpts VerifyOptions) (signingKey crypto.Pu
// verifying the first one in the list only at the moment.
header := t.JWT.Headers[0]
signingKey, err = verifyCertChain(header, verifyOpts.Roots)
// NOTE(milosgajdos): if the x5c header is missing
// the token may have been signed by a JWKS.
if err != nil && err != jose.ErrMissingX5cHeader {
return
}
switch {
case header.JSONWebKey != nil:
signingKey, err = verifyJWK(header, verifyOpts)
@@ -149,7 +188,7 @@ func (t *Token) VerifySigningKey(verifyOpts VerifyOptions) (signingKey crypto.Pu
err = fmt.Errorf("token signed by untrusted key with ID: %q", header.KeyID)
}
default:
signingKey, err = verifyCertChain(header, verifyOpts.Roots)
err = ErrInvalidToken
}
return

View File

@@ -19,8 +19,8 @@ import (
"time"
"github.com/distribution/distribution/v3/registry/auth"
"github.com/go-jose/go-jose/v3"
"github.com/go-jose/go-jose/v3/jwt"
"github.com/go-jose/go-jose/v4"
"github.com/go-jose/go-jose/v4/jwt"
)
func makeRootKeys(numKeys int) ([]*ecdsa.PrivateKey, error) {
@@ -123,12 +123,12 @@ func makeTestToken(jwk *jose.JSONWebKey, issuer, audience string, access []*Reso
Access: access,
}
tokenString, err := jwt.Signed(signer).Claims(claimSet).CompactSerialize()
tokenString, err := jwt.Signed(signer).Claims(claimSet).Serialize()
if err != nil {
return nil, fmt.Errorf("unable to build token string: %v", err)
}
return NewToken(tokenString)
return NewToken(tokenString, []jose.SignatureAlgorithm{signingKey.Algorithm})
}
// NOTE(milosgajdos): certTemplateInfo type as well