mirror of
https://github.com/distribution/distribution.git
synced 2025-09-18 16:35:48 +00:00
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:
@@ -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
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user