From a8a2d7e8f62d24c49e8d9282d089529f2e69d882 Mon Sep 17 00:00:00 2001 From: Nils Goroll Date: Sat, 24 Jan 2026 11:55:21 +0100 Subject: [PATCH] Refactor services/lfs: Change token code to use SigningKey This now also enables use of token algorithms other than HS256. In this case, signing key initialization also happens during settings initialization, because LFS is also used in CLI commands. --- cmd/serv.go | 3 +-- custom/conf/app.example.ini | 4 ++++ modules/setting/lfs.go | 26 +++++++++------------- modules/storage/storage.go | 1 + services/lfs/server.go | 5 +++-- services/lfs/server_test.go | 38 ++++++++++++++++++++++++++++++--- services/repository/lfs_test.go | 18 ++++++++++++++-- 7 files changed, 70 insertions(+), 25 deletions(-) diff --git a/cmd/serv.go b/cmd/serv.go index 0e0551d297..3a92b0e5fb 100644 --- a/cmd/serv.go +++ b/cmd/serv.go @@ -290,10 +290,9 @@ func runServ(ctx context.Context, c *cli.Command) error { Op: lfsVerb, UserID: results.UserID, } - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) // Sign and get the complete encoded token as a string using the secret - tokenString, err := token.SignedString(setting.LFS.JWTSecretBytes) + tokenString, err := setting.LFS.SigningKey.JWT(claims) if err != nil { return fail(ctx, "Failed to sign JWT Token", "Failed to sign JWT token: %v", err) } diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index b7aa3571df..f655027660 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -313,6 +313,9 @@ RUN_USER = ; git ;LFS_START_SERVER = false ;; ;; +;; see JWT_* under [oauth2] +;LFS_JWT_SIGNING_ALGORITHM = HS256 +;LFS_JWT_SIGNING_PRIVATE_KEY_FILE = jwt/lfs_private.pem ;; LFS authentication secret, change this yourself ;LFS_JWT_SECRET = ;; @@ -544,6 +547,7 @@ ENABLED = true ;; Private key file path used to sign OAuth2 tokens. The path is relative to APP_DATA_PATH. ;; This setting is only needed if JWT_SIGNING_ALGORITHM is set to RS256, RS384, RS512, ES256, ES384 or ES512. ;; The file must contain a RSA or ECDSA private key in the PKCS8 format. If no key exists a 4096 bit key will be created for you. +;; XXX jwt/ is a misnomer, it should rather be oauth2/, because we use many JWTs ;JWT_SIGNING_PRIVATE_KEY_FILE = jwt/private.pem ;; ;; OAuth2 authentication secret for access and refresh tokens, change this yourself to a unique string. CLI generate option is helpful in this case. https://forgejo.org/docs/latest/admin/command-line/#generate-secret diff --git a/modules/setting/lfs.go b/modules/setting/lfs.go index 452bfae737..47de5ff8ce 100644 --- a/modules/setting/lfs.go +++ b/modules/setting/lfs.go @@ -7,7 +7,7 @@ import ( "fmt" "time" - "forgejo.org/modules/generate" + "forgejo.org/modules/jwtx" ) // LFS represents the server-side configuration for Git LFS. @@ -16,13 +16,13 @@ import ( // Could be refactored in the future while keeping backwards compatibility. var LFS = struct { StartServer bool `ini:"LFS_START_SERVER"` - JWTSecretBytes []byte `ini:"-"` HTTPAuthExpiry time.Duration `ini:"LFS_HTTP_AUTH_EXPIRY"` MaxFileSize int64 `ini:"LFS_MAX_FILE_SIZE"` LocksPagingNum int `ini:"LFS_LOCKS_PAGING_NUM"` MaxBatchSize int `ini:"LFS_MAX_BATCH_SIZE"` - Storage *Storage + SigningKey jwtx.SigningKey + Storage *Storage }{} // LFSClient represents configuration for Gitea's LFS clients, for example: mirroring upstream Git LFS @@ -77,20 +77,14 @@ func loadLFSFrom(rootCfg ConfigProvider) error { return nil } - jwtSecretBase64 := loadSecret(rootCfg.Section("server"), "LFS_JWT_SECRET_URI", "LFS_JWT_SECRET") - LFS.JWTSecretBytes, err = generate.DecodeJwtSecret(jwtSecretBase64) - if err != nil { - LFS.JWTSecretBytes, jwtSecretBase64 = generate.NewJwtSecret() - - // Save secret - saveCfg, err := rootCfg.PrepareSaving() - if err != nil { - return fmt.Errorf("error saving JWT Secret for custom config: %v", err) + // XXX #11024 check nil because settings loaded twice + if LFS.SigningKey == nil { + keyCfg, err := loadKeyCfg(rootCfg, "server", "LFS_JWT_", "HS256", "lfs/private.pem") + if err == nil { + LFS.SigningKey, err = jwtx.InitSigningKey(&keyCfg.Signing) } - rootCfg.Section("server").Key("LFS_JWT_SECRET").SetValue(jwtSecretBase64) - saveCfg.Section("server").Key("LFS_JWT_SECRET").SetValue(jwtSecretBase64) - if err := saveCfg.Save(); err != nil { - return fmt.Errorf("error saving JWT Secret for custom config: %v", err) + if err != nil { + return fmt.Errorf("lfs key initialization failed: %v", err) } } diff --git a/modules/storage/storage.go b/modules/storage/storage.go index db081e0768..fe9222060f 100644 --- a/modules/storage/storage.go +++ b/modules/storage/storage.go @@ -164,6 +164,7 @@ func initLFS() (err error) { LFS = DiscardStorage("LFS isn't enabled") return nil } + log.Info("Initialising LFS storage with type: %s", setting.LFS.Storage.Type) LFS, err = NewStorage(setting.LFS.Storage.Type, setting.LFS.Storage) return err diff --git a/services/lfs/server.go b/services/lfs/server.go index 30878d8edd..fa58b96cef 100644 --- a/services/lfs/server.go +++ b/services/lfs/server.go @@ -581,10 +581,11 @@ func authenticate(ctx *context.Context, repository *repo_model.Repository, autho func handleLFSToken(ctx stdCtx.Context, tokenSHA string, target *repo_model.Repository, mode perm.AccessMode) (*user_model.User, error) { token, err := jwt.ParseWithClaims(tokenSHA, &Claims{}, func(t *jwt.Token) (any, error) { - if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { + k := setting.LFS.SigningKey + if t.Method != k.SigningMethod() { return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"]) } - return setting.LFS.JWTSecretBytes, nil + return k.VerifyKey(), nil }) if err != nil { return nil, errors.New("invalid token") diff --git a/services/lfs/server_test.go b/services/lfs/server_test.go index 13e7e73010..7f35dc133d 100644 --- a/services/lfs/server_test.go +++ b/services/lfs/server_test.go @@ -41,18 +41,23 @@ func getLFSAuthTokenWithBearer(opts authTokenOptions) (string, error) { Op: opts.Op, UserID: opts.UserID, } - token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) // Sign and get the complete encoded token as a string using the secret - tokenString, err := token.SignedString(setting.LFS.JWTSecretBytes) + tokenString, err := setting.LFS.SigningKey.JWT(claims) if err != nil { return "", fmt.Errorf("failed to sign LFS JWT token: %w", err) } return "Bearer " + tokenString, nil } -func TestAuthenticate(t *testing.T) { +func testAuthenticate(t *testing.T, cfg string) { require.NoError(t, unittest.PrepareTestDatabase()) + var err error + setting.CfgProvider, err = setting.NewConfigProviderFromData(cfg) + require.NoError(t, err, "Config") + setting.LoadCommonSettings() + assert.True(t, setting.LFS.StartServer, "LFS_START_SERVER = true") + assert.NotNil(t, setting.LFS.SigningKey, "SigningKey initialized") repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) token2, _ := getLFSAuthTokenWithBearer(authTokenOptions{Op: "download", UserID: 2, RepoID: 1}) @@ -80,3 +85,30 @@ func TestAuthenticate(t *testing.T) { assert.True(t, authenticate(ctx, repo1, prefixBearer+token2, true, false)) }) } + +type namedCfg struct { + name, cfg string +} + +var iniCommon = `[security] +INSTALL_LOCK = true +INTERNAL_TOKEN = ForgejoForgejoForgejoForgejoForgejoForgejo_ # don't use in prod +[oauth2] +JWT_SECRET = ForgejoForgejoForgejoForgejoForgejoForgejo_ # don't use in prod +[server] +LFS_START_SERVER = true + ` + +var cfgVariants = []namedCfg{ + {name: "HS256_default", cfg: `LFS_JWT_SECRET = ForgejoForgejoForgejoForgejoForgejoForgejo_`}, + {name: "RS256", cfg: `LFS_JWT_SIGNING_ALGORITHM = RS256`}, +} + +func TestAuthenticate(t *testing.T) { + // XXX #11024 + setting.InstallLock = true + for _, v := range cfgVariants { + cfg := iniCommon + v.cfg + t.Run(v.name, func(t *testing.T) { testAuthenticate(t, cfg) }) + } +} diff --git a/services/repository/lfs_test.go b/services/repository/lfs_test.go index e38c38e29c..0224b71100 100644 --- a/services/repository/lfs_test.go +++ b/services/repository/lfs_test.go @@ -21,11 +21,25 @@ import ( "github.com/stretchr/testify/require" ) +var ini = `[security] +INSTALL_LOCK = true +INTERNAL_TOKEN = ForgejoForgejoForgejoForgejoForgejoForgejo_ # don't use in prod +[oauth2] +JWT_SECRET = ForgejoForgejoForgejoForgejoForgejoForgejo_ # don't use in prod +[server] +LFS_START_SERVER = true +LFS_JWT_SECRET = ForgejoForgejoForgejoForgejoForgejoForgejo_ # don't use in prod + ` + func TestGarbageCollectLFSMetaObjects(t *testing.T) { + var err error + setting.CfgProvider, err = setting.NewConfigProviderFromData(ini) + require.NoError(t, err, "Config") + setting.LoadCommonSettings() + unittest.PrepareTestEnv(t) - setting.LFS.StartServer = true - err := storage.Init() + err = storage.Init() require.NoError(t, err) repo, err := repo_model.GetRepositoryByOwnerAndName(db.DefaultContext, "user2", "lfs")