From 26602fd2070886a1e7e0545f11f5541a38396003 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Sun, 14 Dec 2025 09:34:45 -0800 Subject: [PATCH] Remove undocumented support of signing key in the repository git configuration file (#36143) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per-repository signing keys have never been officially supported, as they would require users to modify the repository’s config file. At this point, it is clear that only global signing keys (GPG or SSH) should be allowed. If we want to introduce per-repository signing keys in the future, it will require a complete design proposal. The endpoint will not be removed for repository special signing key, but it will reference the global signing key. --------- Signed-off-by: Lunny Xiao Co-authored-by: delvh --- modules/git/commit.go | 8 -- modules/git/gpg.go | 102 +++++++++++++++++++++++++ modules/git/key.go | 12 +-- modules/git/repo.go | 10 --- modules/git/repo_base_gogit.go | 1 - modules/git/repo_base_nogogit.go | 2 - modules/git/repo_gpg.go | 71 ----------------- modules/gitrepo/signing.go | 4 +- routers/api/v1/misc/signing.go | 9 +-- routers/web/repo/setting/setting.go | 4 +- services/asymkey/commit.go | 2 +- services/asymkey/sign.go | 24 +++--- services/context/repo.go | 2 +- services/repository/files/patch.go | 2 +- services/repository/files/temp_repo.go | 4 +- services/repository/files/update.go | 2 +- services/repository/init.go | 2 +- 17 files changed, 133 insertions(+), 128 deletions(-) create mode 100644 modules/git/gpg.go delete mode 100644 modules/git/repo_gpg.go diff --git a/modules/git/commit.go b/modules/git/commit.go index af09697018..1917a72bbf 100644 --- a/modules/git/commit.go +++ b/modules/git/commit.go @@ -323,14 +323,6 @@ func GetFullCommitID(ctx context.Context, repoPath, shortID string) (string, err return strings.TrimSpace(commitID), nil } -// GetRepositoryDefaultPublicGPGKey returns the default public key for this commit -func (c *Commit) GetRepositoryDefaultPublicGPGKey(forceUpdate bool) (*GPGSettings, error) { - if c.repo == nil { - return nil, nil - } - return c.repo.GetDefaultPublicGPGKey(forceUpdate) -} - func IsStringLikelyCommitID(objFmt ObjectFormat, s string, minLength ...int) bool { maxLen := 64 // sha256 if objFmt != nil { diff --git a/modules/git/gpg.go b/modules/git/gpg.go new file mode 100644 index 0000000000..dbc5569309 --- /dev/null +++ b/modules/git/gpg.go @@ -0,0 +1,102 @@ +// Copyright 2015 The Gogs Authors. All rights reserved. +// Copyright 2017 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +import ( + "context" + "fmt" + "os" + "strings" + "sync" + + "code.gitea.io/gitea/modules/git/gitcmd" + "code.gitea.io/gitea/modules/process" +) + +// GPGSettings represents the default GPG settings for this repository +type GPGSettings struct { + Sign bool + KeyID string + Email string + Name string + PublicKeyContent string + Format string +} + +// LoadPublicKeyContent will load the key from gpg +func (gpgSettings *GPGSettings) LoadPublicKeyContent() error { + if gpgSettings.PublicKeyContent != "" { + return nil + } + + if gpgSettings.Format == SigningKeyFormatSSH { + content, err := os.ReadFile(gpgSettings.KeyID) + if err != nil { + return fmt.Errorf("unable to read SSH public key file: %s, %w", gpgSettings.KeyID, err) + } + gpgSettings.PublicKeyContent = string(content) + return nil + } + content, stderr, err := process.GetManager().Exec( + "gpg -a --export", + "gpg", "-a", "--export", gpgSettings.KeyID) + if err != nil { + return fmt.Errorf("unable to get default signing key: %s, %s, %w", gpgSettings.KeyID, stderr, err) + } + gpgSettings.PublicKeyContent = content + return nil +} + +var ( + loadPublicGPGKeyMutex sync.RWMutex + globalGPGSettings *GPGSettings +) + +// GetDefaultPublicGPGKey will return and cache the default public GPG settings +func GetDefaultPublicGPGKey(ctx context.Context, forceUpdate bool) (*GPGSettings, error) { + if !forceUpdate { + loadPublicGPGKeyMutex.RLock() + if globalGPGSettings != nil { + defer loadPublicGPGKeyMutex.RUnlock() + return globalGPGSettings, nil + } + loadPublicGPGKeyMutex.RUnlock() + } + + loadPublicGPGKeyMutex.Lock() + defer loadPublicGPGKeyMutex.Unlock() + + if globalGPGSettings != nil && !forceUpdate { + return globalGPGSettings, nil + } + + globalGPGSettings = &GPGSettings{ + Sign: true, + } + + value, _, _ := gitcmd.NewCommand("config", "--global", "--get", "commit.gpgsign").RunStdString(ctx) + sign, valid := ParseBool(strings.TrimSpace(value)) + if !sign || !valid { + globalGPGSettings.Sign = false + return globalGPGSettings, nil + } + + signingKey, _, _ := gitcmd.NewCommand("config", "--global", "--get", "user.signingkey").RunStdString(ctx) + globalGPGSettings.KeyID = strings.TrimSpace(signingKey) + + format, _, _ := gitcmd.NewCommand("config", "--global", "--default", SigningKeyFormatOpenPGP, "--get", "gpg.format").RunStdString(ctx) + globalGPGSettings.Format = strings.TrimSpace(format) + + defaultEmail, _, _ := gitcmd.NewCommand("config", "--global", "--get", "user.email").RunStdString(ctx) + globalGPGSettings.Email = strings.TrimSpace(defaultEmail) + + defaultName, _, _ := gitcmd.NewCommand("config", "--global", "--get", "user.name").RunStdString(ctx) + globalGPGSettings.Name = strings.TrimSpace(defaultName) + + if err := globalGPGSettings.LoadPublicKeyContent(); err != nil { + return nil, err + } + return globalGPGSettings, nil +} diff --git a/modules/git/key.go b/modules/git/key.go index 39e79ddbe0..9d51704595 100644 --- a/modules/git/key.go +++ b/modules/git/key.go @@ -32,23 +32,23 @@ func (s *SigningKey) String() string { } // GetSigningKey returns the KeyID and git Signature for the repo -func GetSigningKey(ctx context.Context, repoPath string) (*SigningKey, *Signature) { +func GetSigningKey(ctx context.Context) (*SigningKey, *Signature) { if setting.Repository.Signing.SigningKey == "none" { return nil, nil } if setting.Repository.Signing.SigningKey == "default" || setting.Repository.Signing.SigningKey == "" { // Can ignore the error here as it means that commit.gpgsign is not set - value, _, _ := gitcmd.NewCommand("config", "--get", "commit.gpgsign").WithDir(repoPath).RunStdString(ctx) + value, _, _ := gitcmd.NewCommand("config", "--global", "--get", "commit.gpgsign").RunStdString(ctx) sign, valid := ParseBool(strings.TrimSpace(value)) if !sign || !valid { return nil, nil } - format, _, _ := gitcmd.NewCommand("config", "--default", SigningKeyFormatOpenPGP, "--get", "gpg.format").WithDir(repoPath).RunStdString(ctx) - signingKey, _, _ := gitcmd.NewCommand("config", "--get", "user.signingkey").WithDir(repoPath).RunStdString(ctx) - signingName, _, _ := gitcmd.NewCommand("config", "--get", "user.name").WithDir(repoPath).RunStdString(ctx) - signingEmail, _, _ := gitcmd.NewCommand("config", "--get", "user.email").WithDir(repoPath).RunStdString(ctx) + format, _, _ := gitcmd.NewCommand("config", "--global", "--default", SigningKeyFormatOpenPGP, "--get", "gpg.format").RunStdString(ctx) + signingKey, _, _ := gitcmd.NewCommand("config", "--global", "--get", "user.signingkey").RunStdString(ctx) + signingName, _, _ := gitcmd.NewCommand("config", "--global", "--get", "user.name").RunStdString(ctx) + signingEmail, _, _ := gitcmd.NewCommand("config", "--global", "--get", "user.email").RunStdString(ctx) if strings.TrimSpace(signingKey) == "" { return nil, nil diff --git a/modules/git/repo.go b/modules/git/repo.go index baf29432ec..579accf92e 100644 --- a/modules/git/repo.go +++ b/modules/git/repo.go @@ -20,16 +20,6 @@ import ( "code.gitea.io/gitea/modules/proxy" ) -// GPGSettings represents the default GPG settings for this repository -type GPGSettings struct { - Sign bool - KeyID string - Email string - Name string - PublicKeyContent string - Format string -} - const prettyLogFormat = `--pretty=format:%H` func (repo *Repository) ShowPrettyFormatLogToList(ctx context.Context, revisionRange string) ([]*Commit, error) { diff --git a/modules/git/repo_base_gogit.go b/modules/git/repo_base_gogit.go index e0d0b45372..986264fd93 100644 --- a/modules/git/repo_base_gogit.go +++ b/modules/git/repo_base_gogit.go @@ -32,7 +32,6 @@ type Repository struct { gogitRepo *gogit.Repository gogitStorage *filesystem.Storage - gpgSettings *GPGSettings Ctx context.Context LastCommitCache *LastCommitCache diff --git a/modules/git/repo_base_nogogit.go b/modules/git/repo_base_nogogit.go index 4091e70846..17c71da5ef 100644 --- a/modules/git/repo_base_nogogit.go +++ b/modules/git/repo_base_nogogit.go @@ -23,8 +23,6 @@ type Repository struct { tagCache *ObjectCache[*Tag] - gpgSettings *GPGSettings - batchInUse bool batch *Batch diff --git a/modules/git/repo_gpg.go b/modules/git/repo_gpg.go deleted file mode 100644 index eb1e71e30a..0000000000 --- a/modules/git/repo_gpg.go +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright 2015 The Gogs Authors. All rights reserved. -// Copyright 2017 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package git - -import ( - "fmt" - "os" - "strings" - - "code.gitea.io/gitea/modules/git/gitcmd" - "code.gitea.io/gitea/modules/process" -) - -// LoadPublicKeyContent will load the key from gpg -func (gpgSettings *GPGSettings) LoadPublicKeyContent() error { - if gpgSettings.Format == SigningKeyFormatSSH { - content, err := os.ReadFile(gpgSettings.KeyID) - if err != nil { - return fmt.Errorf("unable to read SSH public key file: %s, %w", gpgSettings.KeyID, err) - } - gpgSettings.PublicKeyContent = string(content) - return nil - } - content, stderr, err := process.GetManager().Exec( - "gpg -a --export", - "gpg", "-a", "--export", gpgSettings.KeyID) - if err != nil { - return fmt.Errorf("unable to get default signing key: %s, %s, %w", gpgSettings.KeyID, stderr, err) - } - gpgSettings.PublicKeyContent = content - return nil -} - -// GetDefaultPublicGPGKey will return and cache the default public GPG settings for this repository -func (repo *Repository) GetDefaultPublicGPGKey(forceUpdate bool) (*GPGSettings, error) { - if repo.gpgSettings != nil && !forceUpdate { - return repo.gpgSettings, nil - } - - gpgSettings := &GPGSettings{ - Sign: true, - } - - value, _, _ := gitcmd.NewCommand("config", "--get", "commit.gpgsign").WithDir(repo.Path).RunStdString(repo.Ctx) - sign, valid := ParseBool(strings.TrimSpace(value)) - if !sign || !valid { - gpgSettings.Sign = false - repo.gpgSettings = gpgSettings - return gpgSettings, nil - } - - signingKey, _, _ := gitcmd.NewCommand("config", "--get", "user.signingkey").WithDir(repo.Path).RunStdString(repo.Ctx) - gpgSettings.KeyID = strings.TrimSpace(signingKey) - - format, _, _ := gitcmd.NewCommand("config", "--default", SigningKeyFormatOpenPGP, "--get", "gpg.format").WithDir(repo.Path).RunStdString(repo.Ctx) - gpgSettings.Format = strings.TrimSpace(format) - - defaultEmail, _, _ := gitcmd.NewCommand("config", "--get", "user.email").WithDir(repo.Path).RunStdString(repo.Ctx) - gpgSettings.Email = strings.TrimSpace(defaultEmail) - - defaultName, _, _ := gitcmd.NewCommand("config", "--get", "user.name").WithDir(repo.Path).RunStdString(repo.Ctx) - gpgSettings.Name = strings.TrimSpace(defaultName) - - if err := gpgSettings.LoadPublicKeyContent(); err != nil { - return nil, err - } - repo.gpgSettings = gpgSettings - return repo.gpgSettings, nil -} diff --git a/modules/gitrepo/signing.go b/modules/gitrepo/signing.go index c50978d15a..2f77758d8c 100644 --- a/modules/gitrepo/signing.go +++ b/modules/gitrepo/signing.go @@ -9,6 +9,6 @@ import ( "code.gitea.io/gitea/modules/git" ) -func GetSigningKey(ctx context.Context, repo Repository) (*git.SigningKey, *git.Signature) { - return git.GetSigningKey(ctx, repoPath(repo)) +func GetSigningKey(ctx context.Context) (*git.SigningKey, *git.Signature) { + return git.GetSigningKey(ctx) } diff --git a/routers/api/v1/misc/signing.go b/routers/api/v1/misc/signing.go index db70e04b8f..6e1a9a09b2 100644 --- a/routers/api/v1/misc/signing.go +++ b/routers/api/v1/misc/signing.go @@ -10,13 +10,8 @@ import ( ) func getSigningKey(ctx *context.APIContext, expectedFormat string) { - // if the handler is in the repo's route group, get the repo's signing key - // otherwise, get the global signing key - path := "" - if ctx.Repo != nil && ctx.Repo.Repository != nil { - path = ctx.Repo.Repository.RepoPath() - } - content, format, err := asymkey_service.PublicSigningKey(ctx, path) + // get the global signing key + content, format, err := asymkey_service.PublicSigningKey(ctx) if err != nil { ctx.APIErrorInternal(err) return diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index 60eb35f56d..0c73c1490f 100644 --- a/routers/web/repo/setting/setting.go +++ b/routers/web/repo/setting/setting.go @@ -61,7 +61,7 @@ func SettingsCtxData(ctx *context.Context) { ctx.Data["MinimumMirrorInterval"] = setting.Mirror.MinInterval ctx.Data["CanConvertFork"] = ctx.Repo.Repository.IsFork && ctx.Doer.CanCreateRepoIn(ctx.Repo.Repository.Owner) - signing, _ := gitrepo.GetSigningKey(ctx, ctx.Repo.Repository) + signing, _ := gitrepo.GetSigningKey(ctx) ctx.Data["SigningKeyAvailable"] = signing != nil ctx.Data["SigningSettings"] = setting.Repository.Signing ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled @@ -104,7 +104,7 @@ func SettingsPost(ctx *context.Context) { ctx.Data["DefaultMirrorInterval"] = setting.Mirror.DefaultInterval ctx.Data["MinimumMirrorInterval"] = setting.Mirror.MinInterval - signing, _ := gitrepo.GetSigningKey(ctx, ctx.Repo.Repository) + signing, _ := gitrepo.GetSigningKey(ctx) ctx.Data["SigningKeyAvailable"] = signing != nil ctx.Data["SigningSettings"] = setting.Repository.Signing ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled diff --git a/services/asymkey/commit.go b/services/asymkey/commit.go index 54ef052a50..6286588a60 100644 --- a/services/asymkey/commit.go +++ b/services/asymkey/commit.go @@ -162,7 +162,7 @@ func parseCommitWithGPGSignature(ctx context.Context, c *git.Commit, committer * } } - defaultGPGSettings, err := c.GetRepositoryDefaultPublicGPGKey(false) + defaultGPGSettings, err := git.GetDefaultPublicGPGKey(ctx, false) if err != nil { log.Error("Error getting default public gpg key: %v", err) } else if defaultGPGSettings == nil { diff --git a/services/asymkey/sign.go b/services/asymkey/sign.go index 1ed05ba287..eb6e461346 100644 --- a/services/asymkey/sign.go +++ b/services/asymkey/sign.go @@ -108,34 +108,34 @@ func IsErrWontSign(err error) bool { return ok } -// PublicSigningKey gets the public signing key within a provided repository directory -func PublicSigningKey(ctx context.Context, repoPath string) (content, format string, err error) { - signingKey, _ := git.GetSigningKey(ctx, repoPath) +// PublicSigningKey gets the public signing key of the entire instance +func PublicSigningKey(ctx context.Context) (content, format string, err error) { + signingKey, _ := git.GetSigningKey(ctx) if signingKey == nil { return "", "", nil } if signingKey.Format == git.SigningKeyFormatSSH { content, err := os.ReadFile(signingKey.KeyID) if err != nil { - log.Error("Unable to read SSH public key file in %s: %s, %v", repoPath, signingKey, err) + log.Error("Unable to read SSH public key file: %s, %v", signingKey, err) return "", signingKey.Format, err } return string(content), signingKey.Format, nil } - content, stderr, err := process.GetManager().ExecDir(ctx, -1, repoPath, + content, stderr, err := process.GetManager().ExecDir(ctx, -1, setting.Git.HomePath, "gpg --export -a", "gpg", "--export", "-a", signingKey.KeyID) if err != nil { - log.Error("Unable to get default signing key in %s: %s, %s, %v", repoPath, signingKey, stderr, err) + log.Error("Unable to get default signing key: %s, %s, %v", signingKey, stderr, err) return "", signingKey.Format, err } return content, signingKey.Format, nil } // SignInitialCommit determines if we should sign the initial commit to this repository -func SignInitialCommit(ctx context.Context, repoPath string, u *user_model.User) (bool, *git.SigningKey, *git.Signature, error) { +func SignInitialCommit(ctx context.Context, u *user_model.User) (bool, *git.SigningKey, *git.Signature, error) { rules := signingModeFromStrings(setting.Repository.Signing.InitialCommit) - signingKey, sig := git.GetSigningKey(ctx, repoPath) + signingKey, sig := git.GetSigningKey(ctx) if signingKey == nil { return false, nil, nil, &ErrWontSign{noKey} } @@ -171,7 +171,7 @@ Loop: // SignWikiCommit determines if we should sign the commits to this repository wiki func SignWikiCommit(ctx context.Context, repo *repo_model.Repository, u *user_model.User) (bool, *git.SigningKey, *git.Signature, error) { rules := signingModeFromStrings(setting.Repository.Signing.Wiki) - signingKey, sig := gitrepo.GetSigningKey(ctx, repo.WikiStorageRepo()) + signingKey, sig := gitrepo.GetSigningKey(ctx) if signingKey == nil { return false, nil, nil, &ErrWontSign{noKey} } @@ -222,9 +222,9 @@ Loop: } // SignCRUDAction determines if we should sign a CRUD commit to this repository -func SignCRUDAction(ctx context.Context, repoPath string, u *user_model.User, tmpBasePath, parentCommit string) (bool, *git.SigningKey, *git.Signature, error) { +func SignCRUDAction(ctx context.Context, u *user_model.User, tmpBasePath, parentCommit string) (bool, *git.SigningKey, *git.Signature, error) { rules := signingModeFromStrings(setting.Repository.Signing.CRUDActions) - signingKey, sig := git.GetSigningKey(ctx, repoPath) + signingKey, sig := git.GetSigningKey(ctx) if signingKey == nil { return false, nil, nil, &ErrWontSign{noKey} } @@ -288,7 +288,7 @@ func SignMerge(ctx context.Context, pr *issues_model.PullRequest, u *user_model. } repo := pr.BaseRepo - signingKey, signer := gitrepo.GetSigningKey(ctx, repo) + signingKey, signer := gitrepo.GetSigningKey(ctx) if signingKey == nil { return false, nil, nil, &ErrWontSign{noKey} } diff --git a/services/context/repo.go b/services/context/repo.go index 5a313e6f15..e70e83e233 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -140,7 +140,7 @@ func PrepareCommitFormOptions(ctx *Context, doer *user_model.User, targetRepo *r protectionRequireSigned = protectedBranch.RequireSignedCommits } - willSign, signKey, _, err := asymkey_service.SignCRUDAction(ctx, targetRepo.RepoPath(), doer, targetRepo.RepoPath(), refName.String()) + willSign, signKey, _, err := asymkey_service.SignCRUDAction(ctx, doer, targetRepo.RepoPath(), refName.String()) wontSignReason := "" if asymkey_service.IsErrWontSign(err) { wontSignReason = string(err.(*asymkey_service.ErrWontSign).Reason) diff --git a/services/repository/files/patch.go b/services/repository/files/patch.go index 8fe6bb917b..5361091c90 100644 --- a/services/repository/files/patch.go +++ b/services/repository/files/patch.go @@ -95,7 +95,7 @@ func (opts *ApplyDiffPatchOptions) Validate(ctx context.Context, repo *repo_mode } } if protectedBranch != nil && protectedBranch.RequireSignedCommits { - _, _, _, err := asymkey_service.SignCRUDAction(ctx, repo.RepoPath(), doer, repo.RepoPath(), opts.OldBranch) + _, _, _, err := asymkey_service.SignCRUDAction(ctx, doer, repo.RepoPath(), opts.OldBranch) if err != nil { if !asymkey_service.IsErrWontSign(err) { return err diff --git a/services/repository/files/temp_repo.go b/services/repository/files/temp_repo.go index aaf9566aec..cb39abfd6e 100644 --- a/services/repository/files/temp_repo.go +++ b/services/repository/files/temp_repo.go @@ -303,9 +303,9 @@ func (t *TemporaryUploadRepository) CommitTree(ctx context.Context, opts *Commit var key *git.SigningKey var signer *git.Signature if opts.ParentCommitID != "" { - sign, key, signer, _ = asymkey_service.SignCRUDAction(ctx, t.repo.RepoPath(), opts.DoerUser, t.basePath, opts.ParentCommitID) + sign, key, signer, _ = asymkey_service.SignCRUDAction(ctx, opts.DoerUser, t.basePath, opts.ParentCommitID) } else { - sign, key, signer, _ = asymkey_service.SignInitialCommit(ctx, t.repo.RepoPath(), opts.DoerUser) + sign, key, signer, _ = asymkey_service.SignInitialCommit(ctx, opts.DoerUser) } if sign { if key.Format != "" { diff --git a/services/repository/files/update.go b/services/repository/files/update.go index 4830f711fc..967c4d928e 100644 --- a/services/repository/files/update.go +++ b/services/repository/files/update.go @@ -686,7 +686,7 @@ func VerifyBranchProtection(ctx context.Context, repo *repo_model.Repository, do } } if protectedBranch.RequireSignedCommits { - _, _, _, err := asymkey_service.SignCRUDAction(ctx, repo.RepoPath(), doer, repo.RepoPath(), branchName) + _, _, _, err := asymkey_service.SignCRUDAction(ctx, doer, repo.RepoPath(), branchName) if err != nil { if !asymkey_service.IsErrWontSign(err) { return err diff --git a/services/repository/init.go b/services/repository/init.go index 8d9decf811..51cc113d63 100644 --- a/services/repository/init.go +++ b/services/repository/init.go @@ -41,7 +41,7 @@ func initRepoCommit(ctx context.Context, tmpPath string, repo *repo_model.Reposi cmd := gitcmd.NewCommand("commit", "--message=Initial commit"). AddOptionFormat("--author='%s <%s>'", sig.Name, sig.Email) - sign, key, signer, _ := asymkey_service.SignInitialCommit(ctx, tmpPath, u) + sign, key, signer, _ := asymkey_service.SignInitialCommit(ctx, u) if sign { if key.Format != "" { cmd.AddConfig("gpg.format", key.Format)