[feat]: add support for EC/ED25519 public keys for token authentication (#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>
This commit is contained in:
Evan
2025-03-06 23:32:13 +01:00
committed by GitHub
parent e7fb9c5e60
commit d465690630
11 changed files with 1413 additions and 759 deletions
+83 -1
View File
@@ -1,15 +1,20 @@
package auth
import (
"crypto"
"fmt"
"net/http"
"net/http/httptest"
"os"
"regexp"
"strings"
"time"
"github.com/chartmuseum/auth"
"github.com/golang-jwt/jwt/v5"
"github.com/mitchellh/mapstructure"
"zotregistry.dev/zot/pkg/api"
)
type (
@@ -24,7 +29,60 @@ type (
}
)
func MakeAuthTestServer(serverKey string, unauthorizedNamespace string) *httptest.Server {
func MakeAuthTestServer(serverKey, signAlg string, unauthorizedNamespace string) *httptest.Server {
signingKey := loadPrivateKeyFromFile(serverKey)
signingMethod := jwt.GetSigningMethod(signAlg)
authTestServer := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
var access []api.ResourceAccess
scope := request.URL.Query().Get("scope")
if scope != "" {
parts := strings.Split(scope, ":")
name := parts[1]
actions := strings.Split(parts[2], ",")
if name == unauthorizedNamespace {
actions = []string{}
}
access = []api.ResourceAccess{
{
Name: name,
Type: "repository",
Actions: actions,
},
}
}
now := time.Now()
claims := api.ClaimsWithAccess{
Access: access,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(now.Add(time.Minute * 1)),
IssuedAt: jwt.NewNumericDate(now),
Issuer: "Zot",
Audience: []string{"Zot Registry"},
},
}
token := jwt.NewWithClaims(signingMethod, claims)
signedString, err := token.SignedString(signingKey)
if err != nil {
panic(err)
}
response.Header().Set("Content-Type", "application/json")
fmt.Fprintf(response, `{"access_token": "%s"}`, signedString)
}))
return authTestServer
}
// MakeAuthTestServerLegacy makes a test HTTP server to generate bearer tokens using the github.com/chartmuseum/auth
// package, to verify backward compatibility of the token authentication process with older versions of zot.
func MakeAuthTestServerLegacy(serverKey string, unauthorizedNamespace string) *httptest.Server {
cmTokenGenerator, err := auth.NewTokenGenerator(&auth.TokenGeneratorOptions{
PrivateKeyPath: serverKey,
Audience: "Zot Registry",
@@ -85,3 +143,27 @@ func ParseBearerAuthHeader(authHeaderRaw string) *AuthHeader {
return &h
}
func loadPrivateKeyFromFile(path string) crypto.PrivateKey {
privateKeyBytes, err := os.ReadFile(path)
if err != nil {
panic(err)
}
rsaKey, err := jwt.ParseRSAPrivateKeyFromPEM(privateKeyBytes)
if err == nil {
return rsaKey
}
ecKey, err := jwt.ParseECPrivateKeyFromPEM(privateKeyBytes)
if err == nil {
return ecKey
}
edKey, err := jwt.ParseEdPrivateKeyFromPEM(privateKeyBytes)
if err == nil {
return edKey
}
panic("no valid private key found in file " + path)
}
+7 -1
View File
@@ -10,6 +10,12 @@ import (
func TestBearerServer(t *testing.T) {
Convey("test MakeAuthTestServer() no serve key", t, func() {
So(func() { auth.MakeAuthTestServer("", "") }, ShouldPanic)
So(func() { auth.MakeAuthTestServer("", "", "") }, ShouldPanic)
})
}
func TestBearerServerLegacy(t *testing.T) {
Convey("test MakeAuthTestServerLegacy() no serve key", t, func() {
So(func() { auth.MakeAuthTestServerLegacy("", "") }, ShouldPanic)
})
}