-
Notifications
You must be signed in to change notification settings - Fork 116
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[feat]: add support for EC/ED25519 public keys for token authenticati…
…on (#2998) * feat: rework token auth to allow ED25519/EC public keys Signed-off-by: evanebb <git@evanus.nl> * fix: shadow err variable to hopefully avoid data race Signed-off-by: evanebb <git@evanus.nl> * fix: apply golangci-lint feedback Signed-off-by: evanebb <git@evanus.nl> * fix: simplify public key loading by only supporting certificates, fixes ED25519 certificate handling Signed-off-by: evanebb <git@evanus.nl> * test: add golang-jwt based test auth server and test RSA/EC/ED25519 keys Signed-off-by: evanebb <git@evanus.nl> * fix: restrict allowed signing algorithms as recommended by library Signed-off-by: evanebb <git@evanus.nl> * test: add more bearer authorizer tests Signed-off-by: evanebb <git@evanus.nl> * fix: apply more golangci-lint feedback Signed-off-by: evanebb <git@evanus.nl> * test: ensure chmod calls run on test failure for authn errors test Signed-off-by: evanebb <git@evanus.nl> * fix: verify issued-at in given token if present Pulls the validation in-line with the old library Signed-off-by: evanebb <git@evanus.nl> --------- Signed-off-by: evanebb <git@evanus.nl>
- Loading branch information
Showing
11 changed files
with
1,398 additions
and
744 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
package api | ||
|
||
import ( | ||
"crypto" | ||
"fmt" | ||
"regexp" | ||
"slices" | ||
|
||
"github.com/golang-jwt/jwt/v5" | ||
|
||
zerr "zotregistry.dev/zot/errors" | ||
) | ||
|
||
var bearerTokenMatch = regexp.MustCompile("(?i)bearer (.*)") | ||
|
||
// ResourceAccess is a single entry in the private 'access' claim specified by the distribution token authentication | ||
// specification. | ||
type ResourceAccess struct { | ||
Type string `json:"type"` | ||
Name string `json:"name"` | ||
Actions []string `json:"actions"` | ||
} | ||
|
||
type ResourceAction struct { | ||
Type string `json:"type"` | ||
Name string `json:"name"` | ||
Action string `json:"action"` | ||
} | ||
|
||
// ClaimsWithAccess is a claim set containing the private 'access' claim specified by the distribution token | ||
// authentication specification, in addition to the standard registered claims. | ||
// https://distribution.github.io/distribution/spec/auth/jwt/ | ||
type ClaimsWithAccess struct { | ||
Access []ResourceAccess `json:"access"` | ||
jwt.RegisteredClaims | ||
} | ||
|
||
type AuthChallengeError struct { | ||
err error | ||
realm string | ||
service string | ||
resourceAction *ResourceAction | ||
} | ||
|
||
func (c AuthChallengeError) Error() string { | ||
return c.err.Error() | ||
} | ||
|
||
// Header constructs an appropriate value for the WWW-Authenticate header to be returned to the client. | ||
func (c AuthChallengeError) Header() string { | ||
if c.resourceAction == nil { | ||
// no access was requested, so return an empty scope | ||
return fmt.Sprintf("Bearer realm=\"%s\",service=\"%s\",scope=\"\"", | ||
c.realm, c.service) | ||
} | ||
|
||
return fmt.Sprintf("Bearer realm=\"%s\",service=\"%s\",scope=\"%s:%s:%s\"", | ||
c.realm, c.service, c.resourceAction.Type, c.resourceAction.Name, c.resourceAction.Action) | ||
} | ||
|
||
type BearerAuthorizer struct { | ||
realm string | ||
service string | ||
key crypto.PublicKey | ||
} | ||
|
||
func NewBearerAuthorizer(realm string, service string, key crypto.PublicKey) BearerAuthorizer { | ||
return BearerAuthorizer{ | ||
realm: realm, | ||
service: service, | ||
key: key, | ||
} | ||
} | ||
|
||
// Authorize verifies whether the bearer token in the given Authorization header is valid, and whether it has sufficient | ||
// scope for the requested resource action. If an authorization error occurs (e.g. no token is given or the token has | ||
// insufficient scope), an AuthChallengeError is returned as the error. | ||
func (a *BearerAuthorizer) Authorize(header string, requested *ResourceAction) error { | ||
challenge := &AuthChallengeError{ | ||
realm: a.realm, | ||
service: a.service, | ||
resourceAction: requested, | ||
} | ||
|
||
if header == "" { | ||
// if no bearer token is set in the authorization header, return the authentication challenge | ||
challenge.err = zerr.ErrNoBearerToken | ||
|
||
return challenge | ||
} | ||
|
||
signedString := bearerTokenMatch.ReplaceAllString(header, "$1") | ||
|
||
token, err := jwt.ParseWithClaims(signedString, &ClaimsWithAccess{}, func(token *jwt.Token) (interface{}, error) { | ||
return a.key, nil | ||
}, jwt.WithValidMethods(a.allowedSigningAlgorithms()), jwt.WithIssuedAt()) | ||
if err != nil { | ||
return fmt.Errorf("%w: %w", zerr.ErrInvalidBearerToken, err) | ||
} | ||
|
||
if requested == nil { | ||
// the token is valid and no access is requested, so we do not have to validate the access claim | ||
return nil | ||
} | ||
|
||
claims, ok := token.Claims.(*ClaimsWithAccess) | ||
if !ok { | ||
return fmt.Errorf("%w: invalid claims type", zerr.ErrInvalidBearerToken) | ||
} | ||
|
||
// check whether the requested access is allowed by the scope of the token | ||
for _, allowed := range claims.Access { | ||
if allowed.Type != requested.Type { | ||
continue | ||
} | ||
|
||
if allowed.Name != requested.Name { | ||
continue | ||
} | ||
|
||
if !slices.Contains(allowed.Actions, requested.Action) { | ||
continue | ||
} | ||
|
||
// requested action is allowed, so don't return an error | ||
return nil | ||
} | ||
|
||
challenge.err = zerr.ErrInsufficientScope | ||
|
||
return challenge | ||
} | ||
|
||
func (a *BearerAuthorizer) allowedSigningAlgorithms() []string { | ||
return []string{"EdDSA", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512", "PS256", "PS384", "PS512"} | ||
} |
Oops, something went wrong.