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")