From fc8fc5a4c092636676e3db45f9ea4549335586d7 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Fri, 2 May 2025 11:17:04 +0200 Subject: [PATCH 1/8] Improve instance wide ssh commit signing * Signed SSH commits can look like on GitHub * No user account of the committer needed * SSH format can be added in gitea config * No gitconfig changes needed * Set gpg.format git key for signing command * Previously only the default gpg key had global trust in Gitea * SSH Signing worked before with DEFAULT_TRUST_MODEL=committer, but not with model default and manually changing the .gitconfig e.g. the following is all needed ``` [repository.signing] SIGNING_KEY = /data/id_ed25519.pub SIGNING_NAME = Gitea SIGNING_EMAIL = git@domain.com SIGNING_FORMAT = ssh INITIAL_COMMIT = always CRUD_ACTIONS = always WIKI = always MERGES = always ``` `TRUSTED_SSH_KEYS` can be a list of additional ssh public keys to trust for every user of this instance --- models/asymkey/key.go | 6 + modules/git/command.go | 12 +- modules/git/repo.go | 1 + modules/git/repo_gpg.go | 12 ++ modules/git/repo_tree.go | 4 + modules/setting/repository.go | 5 + routers/api/v1/api.go | 1 + routers/api/v1/misc/signing.go | 61 +++++++++- routers/web/repo/setting/setting.go | 4 +- services/asymkey/commit.go | 70 ++++++++++- services/asymkey/sign.go | 162 ++++++++++++++----------- services/context/repo.go | 3 +- services/pull/merge.go | 7 +- services/pull/merge_prepare.go | 5 +- services/pull/merge_squash.go | 7 +- services/repository/files/temp_repo.go | 12 +- services/repository/init.go | 7 +- services/wiki/wiki.go | 6 +- templates/swagger/v1_json.tmpl | 56 +++++++++ 19 files changed, 348 insertions(+), 93 deletions(-) create mode 100644 models/asymkey/key.go diff --git a/models/asymkey/key.go b/models/asymkey/key.go new file mode 100644 index 0000000000000..4e7c50f437b70 --- /dev/null +++ b/models/asymkey/key.go @@ -0,0 +1,6 @@ +package asymkey + +type SigningKey struct { + KeyID string + Format string +} diff --git a/modules/git/command.go b/modules/git/command.go index eaaa4969d0bb1..7910f597f2696 100644 --- a/modules/git/command.go +++ b/modules/git/command.go @@ -47,6 +47,7 @@ type Command struct { globalArgsLength int brokenArgs []string cmd *exec.Cmd // for debug purpose only + configArgs []string } func logArgSanitize(arg string) string { @@ -196,6 +197,15 @@ func (c *Command) AddDashesAndList(list ...string) *Command { return c } +func (c *Command) AddConfig(key string, value string) *Command { + kv := key + "=" + value + if !isSafeArgumentValue(kv) { + c.brokenArgs = append(c.brokenArgs, key) + } + c.configArgs = append(c.configArgs, "-c", kv) + return c +} + // ToTrustedCmdArgs converts a list of strings (trusted as argument) to TrustedCmdArgs // In most cases, it shouldn't be used. Use NewCommand().AddXxx() function instead func ToTrustedCmdArgs(args []string) TrustedCmdArgs { @@ -321,7 +331,7 @@ func (c *Command) run(ctx context.Context, skip int, opts *RunOpts) error { startTime := time.Now() - cmd := exec.CommandContext(ctx, c.prog, c.args...) + cmd := exec.CommandContext(ctx, c.prog, append(append([]string{}, c.configArgs...), c.args...)...) c.cmd = cmd // for debug purpose only if opts.Env == nil { cmd.Env = os.Environ() diff --git a/modules/git/repo.go b/modules/git/repo.go index 45937a8d5fa54..239866fe9d613 100644 --- a/modules/git/repo.go +++ b/modules/git/repo.go @@ -28,6 +28,7 @@ type GPGSettings struct { Email string Name string PublicKeyContent string + Format string } const prettyLogFormat = `--pretty=format:%H` diff --git a/modules/git/repo_gpg.go b/modules/git/repo_gpg.go index 8f91b4dce558b..0a2ceb92187a7 100644 --- a/modules/git/repo_gpg.go +++ b/modules/git/repo_gpg.go @@ -6,6 +6,7 @@ package git import ( "fmt" + "os" "strings" "code.gitea.io/gitea/modules/process" @@ -13,6 +14,14 @@ import ( // LoadPublicKeyContent will load the key from gpg func (gpgSettings *GPGSettings) LoadPublicKeyContent() error { + if gpgSettings.Format == "ssh" { + 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) @@ -44,6 +53,9 @@ func (repo *Repository) GetDefaultPublicGPGKey(forceUpdate bool) (*GPGSettings, signingKey, _, _ := NewCommand("config", "--get", "user.signingkey").RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) gpgSettings.KeyID = strings.TrimSpace(signingKey) + format, _, _ := NewCommand("config", "--get", "gpg.format").RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) + gpgSettings.Format = strings.TrimSpace(format) + defaultEmail, _, _ := NewCommand("config", "--get", "user.email").RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) gpgSettings.Email = strings.TrimSpace(defaultEmail) diff --git a/modules/git/repo_tree.go b/modules/git/repo_tree.go index 70e5aee02353f..064b1317f07ad 100644 --- a/modules/git/repo_tree.go +++ b/modules/git/repo_tree.go @@ -16,6 +16,7 @@ type CommitTreeOpts struct { Parents []string Message string KeyID string + KeyFormat string NoGPGSign bool AlwaysSign bool } @@ -44,6 +45,9 @@ func (repo *Repository) CommitTree(author, committer *Signature, tree *Tree, opt _, _ = messageBytes.WriteString("\n") if opts.KeyID != "" || opts.AlwaysSign { + if opts.KeyFormat != "" { + cmd.AddConfig("gpg.format", opts.KeyFormat) + } cmd.AddOptionFormat("-S%s", opts.KeyID) } diff --git a/modules/setting/repository.go b/modules/setting/repository.go index c6bdc65b3218e..c8268eaf4460a 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -100,11 +100,13 @@ var ( SigningKey string SigningName string SigningEmail string + SigningFormat string InitialCommit []string CRUDActions []string `ini:"CRUD_ACTIONS"` Merges []string Wiki []string DefaultTrustModel string + TrustedSSHKeys []string `ini:"TRUSTED_SSH_KEYS"` } `ini:"repository.signing"` }{ DetectedCharsetsOrder: []string{ @@ -242,11 +244,13 @@ var ( SigningKey string SigningName string SigningEmail string + SigningFormat string InitialCommit []string CRUDActions []string `ini:"CRUD_ACTIONS"` Merges []string Wiki []string DefaultTrustModel string + TrustedSSHKeys []string `ini:"TRUSTED_SSH_KEYS"` }{ SigningKey: "default", SigningName: "", @@ -256,6 +260,7 @@ var ( Merges: []string{"pubkey", "twofa", "basesigned", "commitssigned"}, Wiki: []string{"never"}, DefaultTrustModel: "collaborator", + TrustedSSHKeys: []string{}, }, } RepoRootPath string diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index b98863b418dc4..3b1447dc68a09 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -972,6 +972,7 @@ func Routes() *web.Router { m.Group("", func() { m.Get("/version", misc.Version) m.Get("/signing-key.gpg", misc.SigningKey) + m.Get("/signing-key.pub", misc.SigningKeySSH) m.Post("/markup", reqToken(), bind(api.MarkupOption{}), misc.Markup) m.Post("/markdown", reqToken(), bind(api.MarkdownOption{}), misc.Markdown) m.Post("/markdown/raw", reqToken(), misc.MarkdownRaw) diff --git a/routers/api/v1/misc/signing.go b/routers/api/v1/misc/signing.go index 667396e39ca36..4135aa664eaf2 100644 --- a/routers/api/v1/misc/signing.go +++ b/routers/api/v1/misc/signing.go @@ -50,11 +50,70 @@ func SigningKey(ctx *context.APIContext) { path = ctx.Repo.Repository.RepoPath() } - content, err := asymkey_service.PublicSigningKey(ctx, path) + content, format, err := asymkey_service.PublicSigningKey(ctx, path) if err != nil { ctx.APIErrorInternal(err) return } + if format == "ssh" { + ctx.APIErrorNotFound(fmt.Errorf("SSH keys are used for signing, not GPG")) + return + } + _, err = ctx.Write([]byte(content)) + if err != nil { + ctx.APIErrorInternal(fmt.Errorf("Error writing key content %w", err)) + } +} + +// SigningKey returns the public key of the default signing key if it exists +func SigningKeySSH(ctx *context.APIContext) { + // swagger:operation GET /signing-key.pub miscellaneous getSigningKeySSH + // --- + // summary: Get default signing-key.pub + // produces: + // - text/plain + // responses: + // "200": + // description: "ssh public key" + // schema: + // type: string + + // swagger:operation GET /repos/{owner}/{repo}/signing-key.pub repository repoSigningKeySSH + // --- + // summary: Get signing-key.pub for given repository + // produces: + // - text/plain + // parameters: + // - name: owner + // in: path + // description: owner of the repo + // type: string + // required: true + // - name: repo + // in: path + // description: name of the repo + // type: string + // required: true + // responses: + // "200": + // description: "ssh public key" + // schema: + // type: string + + path := "" + if ctx.Repo != nil && ctx.Repo.Repository != nil { + path = ctx.Repo.Repository.RepoPath() + } + + content, format, err := asymkey_service.PublicSigningKey(ctx, path) + if err != nil { + ctx.APIErrorInternal(err) + return + } + if format != "ssh" { + ctx.APIErrorNotFound(fmt.Errorf("GPG keys are used for signing, not SSH")) + return + } _, err = ctx.Write([]byte(content)) if err != nil { ctx.APIErrorInternal(fmt.Errorf("Error writing key content %w", err)) diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index 5552a8726cc89..bebb4867bc4c3 100644 --- a/routers/web/repo/setting/setting.go +++ b/routers/web/repo/setting/setting.go @@ -62,7 +62,7 @@ func SettingsCtxData(ctx *context.Context) { ctx.Data["CanConvertFork"] = ctx.Repo.Repository.IsFork && ctx.Doer.CanCreateRepoIn(ctx.Repo.Repository.Owner) signing, _ := asymkey_service.SigningKey(ctx, ctx.Repo.Repository.RepoPath()) - ctx.Data["SigningKeyAvailable"] = len(signing) > 0 + ctx.Data["SigningKeyAvailable"] = len(signing.KeyID) > 0 ctx.Data["SigningSettings"] = setting.Repository.Signing ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled @@ -105,7 +105,7 @@ func SettingsPost(ctx *context.Context) { ctx.Data["MinimumMirrorInterval"] = setting.Mirror.MinInterval signing, _ := asymkey_service.SigningKey(ctx, ctx.Repo.Repository.RepoPath()) - ctx.Data["SigningKeyAvailable"] = len(signing) > 0 + ctx.Data["SigningKeyAvailable"] = len(signing.KeyID) > 0 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 105782a93a57c..6de4611389ae8 100644 --- a/services/asymkey/commit.go +++ b/services/asymkey/commit.go @@ -398,11 +398,79 @@ func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committer * } } } + // Trust more than one key for every User + for _, k := range setting.Repository.Signing.TrustedSSHKeys { + fingerprint, _ := asymkey_model.CalcFingerprint(k) + commitVerification := verifySSHCommitVerification(c.Signature.Signature, c.Signature.Payload, &asymkey_model.PublicKey{ + Verified: true, + Content: k, + Fingerprint: fingerprint, + HasUsed: true, + }, committer, committer, c.Committer.Email) + if commitVerification != nil { + return commitVerification + } + } + + defaultReason := asymkey_model.NoKeyFound + + if setting.Repository.Signing.SigningFormat == "ssh" && setting.Repository.Signing.SigningKey != "" && setting.Repository.Signing.SigningKey != "default" && setting.Repository.Signing.SigningKey != "none" { + // OK we should try the default key + gpgSettings := git.GPGSettings{ + Sign: true, + KeyID: setting.Repository.Signing.SigningKey, + Name: setting.Repository.Signing.SigningName, + Email: setting.Repository.Signing.SigningEmail, + Format: setting.Repository.Signing.SigningFormat, + } + if err := gpgSettings.LoadPublicKeyContent(); err != nil { + log.Error("Error getting default signing key: %s %v", gpgSettings.KeyID, err) + } + fingerprint, _ := asymkey_model.CalcFingerprint(gpgSettings.PublicKeyContent) + if commitVerification := verifySSHCommitVerification(c.Signature.Signature, c.Signature.Payload, &asymkey_model.PublicKey{ + Verified: true, + Content: gpgSettings.PublicKeyContent, + Fingerprint: fingerprint, + HasUsed: true, + }, committer, committer, committer.Email); commitVerification != nil { + if commitVerification.Reason == asymkey_model.BadSignature { + defaultReason = asymkey_model.BadSignature + } else { + return commitVerification + } + } + } + + defaultGPGSettings, err := c.GetRepositoryDefaultPublicGPGKey(false) + if defaultGPGSettings.Format == "ssh" { + if err != nil { + log.Error("Error getting default public gpg key: %v", err) + } else if defaultGPGSettings == nil { + log.Warn("Unable to get defaultGPGSettings for unattached commit: %s", c.ID.String()) + } else if defaultGPGSettings.Sign { + if err := defaultGPGSettings.LoadPublicKeyContent(); err != nil { + log.Error("Error getting default signing key: %s %v", defaultGPGSettings.KeyID, err) + } + fingerprint, _ := asymkey_model.CalcFingerprint(defaultGPGSettings.PublicKeyContent) + if commitVerification := verifySSHCommitVerification(c.Signature.Signature, c.Signature.Payload, &asymkey_model.PublicKey{ + Verified: true, + Content: defaultGPGSettings.PublicKeyContent, + Fingerprint: fingerprint, + HasUsed: true, + }, committer, committer, committer.Email); commitVerification != nil { + if commitVerification.Reason == asymkey_model.BadSignature { + defaultReason = asymkey_model.BadSignature + } else { + return commitVerification + } + } + } + } return &asymkey_model.CommitVerification{ CommittingUser: committer, Verified: false, - Reason: asymkey_model.NoKeyFound, + Reason: defaultReason, } } diff --git a/services/asymkey/sign.go b/services/asymkey/sign.go index 2216bca54ae67..57eb6782c5738 100644 --- a/services/asymkey/sign.go +++ b/services/asymkey/sign.go @@ -6,6 +6,7 @@ package asymkey import ( "context" "fmt" + "os" "strings" asymkey_model "code.gitea.io/gitea/models/asymkey" @@ -85,9 +86,9 @@ func IsErrWontSign(err error) bool { } // SigningKey returns the KeyID and git Signature for the repo -func SigningKey(ctx context.Context, repoPath string) (string, *git.Signature) { +func SigningKey(ctx context.Context, repoPath string) (asymkey_model.SigningKey, *git.Signature) { if setting.Repository.Signing.SigningKey == "none" { - return "", nil + return asymkey_model.SigningKey{}, nil } if setting.Repository.Signing.SigningKey == "default" || setting.Repository.Signing.SigningKey == "" { @@ -95,53 +96,68 @@ func SigningKey(ctx context.Context, repoPath string) (string, *git.Signature) { value, _, _ := git.NewCommand("config", "--get", "commit.gpgsign").RunStdString(ctx, &git.RunOpts{Dir: repoPath}) sign, valid := git.ParseBool(strings.TrimSpace(value)) if !sign || !valid { - return "", nil + return asymkey_model.SigningKey{}, nil } + format, _, _ := git.NewCommand("config", "--get", "gpg.format").RunStdString(ctx, &git.RunOpts{Dir: repoPath}) signingKey, _, _ := git.NewCommand("config", "--get", "user.signingkey").RunStdString(ctx, &git.RunOpts{Dir: repoPath}) signingName, _, _ := git.NewCommand("config", "--get", "user.name").RunStdString(ctx, &git.RunOpts{Dir: repoPath}) signingEmail, _, _ := git.NewCommand("config", "--get", "user.email").RunStdString(ctx, &git.RunOpts{Dir: repoPath}) - return strings.TrimSpace(signingKey), &git.Signature{ - Name: strings.TrimSpace(signingName), - Email: strings.TrimSpace(signingEmail), - } + return asymkey_model.SigningKey{ + KeyID: strings.TrimSpace(signingKey), + Format: strings.TrimSpace(format), + }, &git.Signature{ + Name: strings.TrimSpace(signingName), + Email: strings.TrimSpace(signingEmail), + } } - return setting.Repository.Signing.SigningKey, &git.Signature{ - Name: setting.Repository.Signing.SigningName, - Email: setting.Repository.Signing.SigningEmail, - } + return asymkey_model.SigningKey{ + KeyID: setting.Repository.Signing.SigningKey, + Format: setting.Repository.Signing.SigningFormat, + }, &git.Signature{ + Name: setting.Repository.Signing.SigningName, + Email: setting.Repository.Signing.SigningEmail, + } } // PublicSigningKey gets the public signing key within a provided repository directory -func PublicSigningKey(ctx context.Context, repoPath string) (string, error) { +func PublicSigningKey(ctx context.Context, repoPath string) (string, string, error) { signingKey, _ := SigningKey(ctx, repoPath) - if signingKey == "" { - return "", nil + if signingKey.KeyID == "" { + return "", signingKey.Format, nil + } + if signingKey.Format == "ssh" { + 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) + return "", signingKey.Format, err + } + return string(content), signingKey.Format, nil } content, stderr, err := process.GetManager().ExecDir(ctx, -1, repoPath, - "gpg --export -a", "gpg", "--export", "-a", signingKey) + "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) - return "", err + return "", signingKey.Format, err } - return content, nil + 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, string, *git.Signature, error) { +func SignInitialCommit(ctx context.Context, repoPath string, u *user_model.User) (bool, asymkey_model.SigningKey, *git.Signature, error) { rules := signingModeFromStrings(setting.Repository.Signing.InitialCommit) signingKey, sig := SigningKey(ctx, repoPath) - if signingKey == "" { - return false, "", nil, &ErrWontSign{noKey} + if signingKey.KeyID == "" { + return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{noKey} } Loop: for _, rule := range rules { switch rule { case never: - return false, "", nil, &ErrWontSign{never} + return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{never} case always: break Loop case pubkey: @@ -150,18 +166,18 @@ Loop: IncludeSubKeys: true, }) if err != nil { - return false, "", nil, err + return false, asymkey_model.SigningKey{}, nil, err } if len(keys) == 0 { - return false, "", nil, &ErrWontSign{pubkey} + return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{pubkey} } case twofa: twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID) if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) { - return false, "", nil, err + return false, asymkey_model.SigningKey{}, nil, err } if twofaModel == nil { - return false, "", nil, &ErrWontSign{twofa} + return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{twofa} } } } @@ -169,19 +185,19 @@ 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, string, *git.Signature, error) { +func SignWikiCommit(ctx context.Context, repo *repo_model.Repository, u *user_model.User) (bool, asymkey_model.SigningKey, *git.Signature, error) { repoWikiPath := repo.WikiPath() rules := signingModeFromStrings(setting.Repository.Signing.Wiki) signingKey, sig := SigningKey(ctx, repoWikiPath) - if signingKey == "" { - return false, "", nil, &ErrWontSign{noKey} + if signingKey.KeyID == "" { + return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{noKey} } Loop: for _, rule := range rules { switch rule { case never: - return false, "", nil, &ErrWontSign{never} + return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{never} case always: break Loop case pubkey: @@ -190,35 +206,35 @@ Loop: IncludeSubKeys: true, }) if err != nil { - return false, "", nil, err + return false, asymkey_model.SigningKey{}, nil, err } if len(keys) == 0 { - return false, "", nil, &ErrWontSign{pubkey} + return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{pubkey} } case twofa: twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID) if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) { - return false, "", nil, err + return false, asymkey_model.SigningKey{}, nil, err } if twofaModel == nil { - return false, "", nil, &ErrWontSign{twofa} + return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{twofa} } case parentSigned: gitRepo, err := gitrepo.OpenRepository(ctx, repo.WikiStorageRepo()) if err != nil { - return false, "", nil, err + return false, asymkey_model.SigningKey{}, nil, err } defer gitRepo.Close() commit, err := gitRepo.GetCommit("HEAD") if err != nil { - return false, "", nil, err + return false, asymkey_model.SigningKey{}, nil, err } if commit.Signature == nil { - return false, "", nil, &ErrWontSign{parentSigned} + return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{parentSigned} } verification := ParseCommitWithSignature(ctx, commit) if !verification.Verified { - return false, "", nil, &ErrWontSign{parentSigned} + return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{parentSigned} } } } @@ -226,18 +242,18 @@ 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, string, *git.Signature, error) { +func SignCRUDAction(ctx context.Context, repoPath string, u *user_model.User, tmpBasePath, parentCommit string) (bool, asymkey_model.SigningKey, *git.Signature, error) { rules := signingModeFromStrings(setting.Repository.Signing.CRUDActions) signingKey, sig := SigningKey(ctx, repoPath) - if signingKey == "" { - return false, "", nil, &ErrWontSign{noKey} + if signingKey.KeyID == "" { + return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{noKey} } Loop: for _, rule := range rules { switch rule { case never: - return false, "", nil, &ErrWontSign{never} + return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{never} case always: break Loop case pubkey: @@ -246,35 +262,35 @@ Loop: IncludeSubKeys: true, }) if err != nil { - return false, "", nil, err + return false, asymkey_model.SigningKey{}, nil, err } if len(keys) == 0 { - return false, "", nil, &ErrWontSign{pubkey} + return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{pubkey} } case twofa: twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID) if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) { - return false, "", nil, err + return false, asymkey_model.SigningKey{}, nil, err } if twofaModel == nil { - return false, "", nil, &ErrWontSign{twofa} + return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{twofa} } case parentSigned: gitRepo, err := git.OpenRepository(ctx, tmpBasePath) if err != nil { - return false, "", nil, err + return false, asymkey_model.SigningKey{}, nil, err } defer gitRepo.Close() commit, err := gitRepo.GetCommit(parentCommit) if err != nil { - return false, "", nil, err + return false, asymkey_model.SigningKey{}, nil, err } if commit.Signature == nil { - return false, "", nil, &ErrWontSign{parentSigned} + return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{parentSigned} } verification := ParseCommitWithSignature(ctx, commit) if !verification.Verified { - return false, "", nil, &ErrWontSign{parentSigned} + return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{parentSigned} } } } @@ -282,16 +298,16 @@ Loop: } // SignMerge determines if we should sign a PR merge commit to the base repository -func SignMerge(ctx context.Context, pr *issues_model.PullRequest, u *user_model.User, tmpBasePath, baseCommit, headCommit string) (bool, string, *git.Signature, error) { +func SignMerge(ctx context.Context, pr *issues_model.PullRequest, u *user_model.User, tmpBasePath, baseCommit, headCommit string) (bool, asymkey_model.SigningKey, *git.Signature, error) { if err := pr.LoadBaseRepo(ctx); err != nil { log.Error("Unable to get Base Repo for pull request") - return false, "", nil, err + return false, asymkey_model.SigningKey{}, nil, err } repo := pr.BaseRepo signingKey, signer := SigningKey(ctx, repo.RepoPath()) - if signingKey == "" { - return false, "", nil, &ErrWontSign{noKey} + if signingKey.KeyID == "" { + return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{noKey} } rules := signingModeFromStrings(setting.Repository.Signing.Merges) @@ -302,7 +318,7 @@ Loop: for _, rule := range rules { switch rule { case never: - return false, "", nil, &ErrWontSign{never} + return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{never} case always: break Loop case pubkey: @@ -311,91 +327,91 @@ Loop: IncludeSubKeys: true, }) if err != nil { - return false, "", nil, err + return false, asymkey_model.SigningKey{}, nil, err } if len(keys) == 0 { - return false, "", nil, &ErrWontSign{pubkey} + return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{pubkey} } case twofa: twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID) if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) { - return false, "", nil, err + return false, asymkey_model.SigningKey{}, nil, err } if twofaModel == nil { - return false, "", nil, &ErrWontSign{twofa} + return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{twofa} } case approved: protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, pr.BaseBranch) if err != nil { - return false, "", nil, err + return false, asymkey_model.SigningKey{}, nil, err } if protectedBranch == nil { - return false, "", nil, &ErrWontSign{approved} + return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{approved} } if issues_model.GetGrantedApprovalsCount(ctx, protectedBranch, pr) < 1 { - return false, "", nil, &ErrWontSign{approved} + return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{approved} } case baseSigned: if gitRepo == nil { gitRepo, err = git.OpenRepository(ctx, tmpBasePath) if err != nil { - return false, "", nil, err + return false, asymkey_model.SigningKey{}, nil, err } defer gitRepo.Close() } commit, err := gitRepo.GetCommit(baseCommit) if err != nil { - return false, "", nil, err + return false, asymkey_model.SigningKey{}, nil, err } verification := ParseCommitWithSignature(ctx, commit) if !verification.Verified { - return false, "", nil, &ErrWontSign{baseSigned} + return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{baseSigned} } case headSigned: if gitRepo == nil { gitRepo, err = git.OpenRepository(ctx, tmpBasePath) if err != nil { - return false, "", nil, err + return false, asymkey_model.SigningKey{}, nil, err } defer gitRepo.Close() } commit, err := gitRepo.GetCommit(headCommit) if err != nil { - return false, "", nil, err + return false, asymkey_model.SigningKey{}, nil, err } verification := ParseCommitWithSignature(ctx, commit) if !verification.Verified { - return false, "", nil, &ErrWontSign{headSigned} + return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{headSigned} } case commitsSigned: if gitRepo == nil { gitRepo, err = git.OpenRepository(ctx, tmpBasePath) if err != nil { - return false, "", nil, err + return false, asymkey_model.SigningKey{}, nil, err } defer gitRepo.Close() } commit, err := gitRepo.GetCommit(headCommit) if err != nil { - return false, "", nil, err + return false, asymkey_model.SigningKey{}, nil, err } verification := ParseCommitWithSignature(ctx, commit) if !verification.Verified { - return false, "", nil, &ErrWontSign{commitsSigned} + return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{commitsSigned} } // need to work out merge-base mergeBaseCommit, _, err := gitRepo.GetMergeBase("", baseCommit, headCommit) if err != nil { - return false, "", nil, err + return false, asymkey_model.SigningKey{}, nil, err } commitList, err := commit.CommitsBeforeUntil(mergeBaseCommit) if err != nil { - return false, "", nil, err + return false, asymkey_model.SigningKey{}, nil, err } for _, commit := range commitList { verification := ParseCommitWithSignature(ctx, commit) if !verification.Verified { - return false, "", nil, &ErrWontSign{commitsSigned} + return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{commitsSigned} } } } diff --git a/services/context/repo.go b/services/context/repo.go index 127d31325867a..3521b7cd7bebf 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -14,6 +14,7 @@ import ( "path" "strings" + asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" @@ -101,7 +102,7 @@ type CanCommitToBranchResults struct { UserCanPush bool RequireSigned bool WillSign bool - SigningKey string + SigningKey asymkey_model.SigningKey WontSignReason string } diff --git a/services/pull/merge.go b/services/pull/merge.go index 256db847ef39b..42b1871a9f44f 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -396,10 +396,13 @@ func doMergeAndPush(ctx context.Context, pr *issues_model.PullRequest, doer *use func commitAndSignNoAuthor(ctx *mergeContext, message string) error { cmdCommit := git.NewCommand("commit").AddOptionFormat("--message=%s", message) - if ctx.signKeyID == "" { + if ctx.signKey.KeyID == "" { cmdCommit.AddArguments("--no-gpg-sign") } else { - cmdCommit.AddOptionFormat("-S%s", ctx.signKeyID) + if ctx.signKey.Format != "" { + cmdCommit.AddConfig("gpg.format", ctx.signKey.Format) + } + cmdCommit.AddOptionFormat("-S%s", ctx.signKey.KeyID) } if err := cmdCommit.Run(ctx, ctx.RunOpts()); err != nil { log.Error("git commit %-v: %v\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String()) diff --git a/services/pull/merge_prepare.go b/services/pull/merge_prepare.go index 719cc6b965605..093ea0ed379b0 100644 --- a/services/pull/merge_prepare.go +++ b/services/pull/merge_prepare.go @@ -14,6 +14,7 @@ import ( "strings" "time" + asymkey_model "code.gitea.io/gitea/models/asymkey" issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" @@ -27,7 +28,7 @@ type mergeContext struct { doer *user_model.User sig *git.Signature committer *git.Signature - signKeyID string // empty for no-sign, non-empty to sign + signKey asymkey_model.SigningKey // empty for no-sign, non-empty to sign env []string } @@ -101,7 +102,7 @@ func createTemporaryRepoForMerge(ctx context.Context, pr *issues_model.PullReque // Determine if we should sign sign, keyID, signer, _ := asymkey_service.SignMerge(ctx, mergeCtx.pr, mergeCtx.doer, mergeCtx.tmpBasePath, "HEAD", trackingBranch) if sign { - mergeCtx.signKeyID = keyID + mergeCtx.signKey = keyID if pr.BaseRepo.GetTrustModel() == repo_model.CommitterTrustModel || pr.BaseRepo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel { mergeCtx.committer = signer } diff --git a/services/pull/merge_squash.go b/services/pull/merge_squash.go index 72660cd3c57d4..6de319ca2e444 100644 --- a/services/pull/merge_squash.go +++ b/services/pull/merge_squash.go @@ -74,10 +74,13 @@ func doMergeStyleSquash(ctx *mergeContext, message string) error { cmdCommit := git.NewCommand("commit"). AddOptionFormat("--author='%s <%s>'", sig.Name, sig.Email). AddOptionFormat("--message=%s", message) - if ctx.signKeyID == "" { + if ctx.signKey.KeyID == "" { cmdCommit.AddArguments("--no-gpg-sign") } else { - cmdCommit.AddOptionFormat("-S%s", ctx.signKeyID) + if ctx.signKey.Format != "" { + cmdCommit.AddConfig("gpg.format", ctx.signKey.Format) + } + cmdCommit.AddOptionFormat("-S%s", ctx.signKey.KeyID) } if err := cmdCommit.Run(ctx, ctx.RunOpts()); err != nil { log.Error("git commit %-v: %v\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String()) diff --git a/services/repository/files/temp_repo.go b/services/repository/files/temp_repo.go index 493ff9998d0e7..2f7a1c8b3d89b 100644 --- a/services/repository/files/temp_repo.go +++ b/services/repository/files/temp_repo.go @@ -14,6 +14,7 @@ import ( "strings" "time" + asymkey_model "code.gitea.io/gitea/models/asymkey" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" @@ -293,15 +294,18 @@ func (t *TemporaryUploadRepository) CommitTree(ctx context.Context, opts *Commit } var sign bool - var keyID string + var key asymkey_model.SigningKey var signer *git.Signature if opts.ParentCommitID != "" { - sign, keyID, signer, _ = asymkey_service.SignCRUDAction(ctx, t.repo.RepoPath(), opts.DoerUser, t.basePath, opts.ParentCommitID) + sign, key, signer, _ = asymkey_service.SignCRUDAction(ctx, t.repo.RepoPath(), opts.DoerUser, t.basePath, opts.ParentCommitID) } else { - sign, keyID, signer, _ = asymkey_service.SignInitialCommit(ctx, t.repo.RepoPath(), opts.DoerUser) + sign, key, signer, _ = asymkey_service.SignInitialCommit(ctx, t.repo.RepoPath(), opts.DoerUser) } if sign { - cmdCommitTree.AddOptionFormat("-S%s", keyID) + if key.Format != "" { + cmdCommitTree.AddConfig("gpg.format", key.Format) + } + cmdCommitTree.AddOptionFormat("-S%s", key.KeyID) if t.repo.GetTrustModel() == repo_model.CommitterTrustModel || t.repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel { if committerSig.Name != authorSig.Name || committerSig.Email != authorSig.Email { // Add trailers diff --git a/services/repository/init.go b/services/repository/init.go index bd777b8a2fc04..1eeeb4aa4faa8 100644 --- a/services/repository/init.go +++ b/services/repository/init.go @@ -42,9 +42,12 @@ func initRepoCommit(ctx context.Context, tmpPath string, repo *repo_model.Reposi cmd := git.NewCommand("commit", "--message=Initial commit"). AddOptionFormat("--author='%s <%s>'", sig.Name, sig.Email) - sign, keyID, signer, _ := asymkey_service.SignInitialCommit(ctx, tmpPath, u) + sign, key, signer, _ := asymkey_service.SignInitialCommit(ctx, tmpPath, u) if sign { - cmd.AddOptionFormat("-S%s", keyID) + if key.Format != "" { + cmd.AddConfig("gpg.format", key.Format) + } + cmd.AddOptionFormat("-S%s", key.KeyID) if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel { // need to set the committer to the KeyID owner diff --git a/services/wiki/wiki.go b/services/wiki/wiki.go index 45a08dc5d686d..b42c896740aa2 100644 --- a/services/wiki/wiki.go +++ b/services/wiki/wiki.go @@ -194,7 +194,8 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model sign, signingKey, signer, _ := asymkey_service.SignWikiCommit(ctx, repo, doer) if sign { - commitTreeOpts.KeyID = signingKey + commitTreeOpts.KeyID = signingKey.KeyID + commitTreeOpts.KeyFormat = signingKey.Format if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel { committer = signer } @@ -316,7 +317,8 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model sign, signingKey, signer, _ := asymkey_service.SignWikiCommit(ctx, repo, doer) if sign { - commitTreeOpts.KeyID = signingKey + commitTreeOpts.KeyID = signingKey.KeyID + commitTreeOpts.KeyFormat = signingKey.Format if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel { committer = signer } diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 223a2e84103f6..eb876050af94b 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -15103,6 +15103,42 @@ } } }, + "/repos/{owner}/{repo}/signing-key.pub": { + "get": { + "produces": [ + "text/plain" + ], + "tags": [ + "repository" + ], + "summary": "Get signing-key.pub for given repository", + "operationId": "repoSigningKeySSH", + "parameters": [ + { + "type": "string", + "description": "owner of the repo", + "name": "owner", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "name of the repo", + "name": "repo", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "ssh public key", + "schema": { + "type": "string" + } + } + } + } + }, "/repos/{owner}/{repo}/stargazers": { "get": { "produces": [ @@ -16936,6 +16972,26 @@ } } }, + "/signing-key.pub": { + "get": { + "produces": [ + "text/plain" + ], + "tags": [ + "miscellaneous" + ], + "summary": "Get default signing-key.pub", + "operationId": "getSigningKeySSH", + "responses": { + "200": { + "description": "ssh public key", + "schema": { + "type": "string" + } + } + } + } + }, "/teams/{id}": { "get": { "produces": [ From 0a35694007c15a54ff9b4b63e7bb60da1d7150ae Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Fri, 2 May 2025 11:32:37 +0200 Subject: [PATCH 2/8] fix code style --- modules/git/command.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/git/command.go b/modules/git/command.go index 7910f597f2696..dfc58fb3f5053 100644 --- a/modules/git/command.go +++ b/modules/git/command.go @@ -197,7 +197,7 @@ func (c *Command) AddDashesAndList(list ...string) *Command { return c } -func (c *Command) AddConfig(key string, value string) *Command { +func (c *Command) AddConfig(key, value string) *Command { kv := key + "=" + value if !isSafeArgumentValue(kv) { c.brokenArgs = append(c.brokenArgs, key) From 999860c2319fbcc996ea2b08cf02a1b53f91fafd Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Fri, 2 May 2025 11:44:24 +0200 Subject: [PATCH 3/8] fix error style --- routers/api/v1/misc/signing.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/routers/api/v1/misc/signing.go b/routers/api/v1/misc/signing.go index 4135aa664eaf2..78ac1bf02a07c 100644 --- a/routers/api/v1/misc/signing.go +++ b/routers/api/v1/misc/signing.go @@ -4,6 +4,7 @@ package misc import ( + "errors" "fmt" asymkey_service "code.gitea.io/gitea/services/asymkey" @@ -56,7 +57,7 @@ func SigningKey(ctx *context.APIContext) { return } if format == "ssh" { - ctx.APIErrorNotFound(fmt.Errorf("SSH keys are used for signing, not GPG")) + ctx.APIErrorNotFound(errors.New("SSH keys are used for signing, not GPG")) return } _, err = ctx.Write([]byte(content)) @@ -111,7 +112,7 @@ func SigningKeySSH(ctx *context.APIContext) { return } if format != "ssh" { - ctx.APIErrorNotFound(fmt.Errorf("GPG keys are used for signing, not SSH")) + ctx.APIErrorNotFound(errors.New("GPG keys are used for signing, not SSH")) return } _, err = ctx.Write([]byte(content)) From 6a29629d2f924418ab1e7dd0ea7432f43b227490 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Fri, 2 May 2025 11:53:34 +0200 Subject: [PATCH 4/8] add copyright --- models/asymkey/key.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/models/asymkey/key.go b/models/asymkey/key.go index 4e7c50f437b70..db01934346621 100644 --- a/models/asymkey/key.go +++ b/models/asymkey/key.go @@ -1,3 +1,6 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + package asymkey type SigningKey struct { From 6957ba958cd68742cf6dd072012bb0345c9690cb Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Sat, 3 May 2025 01:24:19 +0200 Subject: [PATCH 5/8] set default key format to openpgp --- models/asymkey/key.go | 9 -- modules/git/key.go | 16 ++++ modules/git/repo_gpg.go | 4 +- modules/setting/repository.go | 1 + routers/api/v1/misc/signing.go | 5 +- services/asymkey/commit.go | 4 +- services/asymkey/sign.go | 118 ++++++++++++------------- services/context/repo.go | 3 +- services/pull/merge_prepare.go | 3 +- services/repository/files/temp_repo.go | 3 +- 10 files changed, 86 insertions(+), 80 deletions(-) delete mode 100644 models/asymkey/key.go create mode 100644 modules/git/key.go diff --git a/models/asymkey/key.go b/models/asymkey/key.go deleted file mode 100644 index db01934346621..0000000000000 --- a/models/asymkey/key.go +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright 2025 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package asymkey - -type SigningKey struct { - KeyID string - Format string -} diff --git a/modules/git/key.go b/modules/git/key.go new file mode 100644 index 0000000000000..1653c9e49793e --- /dev/null +++ b/modules/git/key.go @@ -0,0 +1,16 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package git + +const ( + // KeyTypeOpenPGP is the key type for GPG keys + KeyTypeOpenPGP = "openpgp" + // KeyTypeSSH is the key type for SSH keys + KeyTypeSSH = "ssh" +) + +type SigningKey struct { + KeyID string + Format string +} diff --git a/modules/git/repo_gpg.go b/modules/git/repo_gpg.go index 0a2ceb92187a7..e226757af4c87 100644 --- a/modules/git/repo_gpg.go +++ b/modules/git/repo_gpg.go @@ -14,7 +14,7 @@ import ( // LoadPublicKeyContent will load the key from gpg func (gpgSettings *GPGSettings) LoadPublicKeyContent() error { - if gpgSettings.Format == "ssh" { + if gpgSettings.Format == KeyTypeOpenPGP { content, err := os.ReadFile(gpgSettings.KeyID) if err != nil { return fmt.Errorf("unable to read SSH public key file: %s, %w", gpgSettings.KeyID, err) @@ -53,7 +53,7 @@ func (repo *Repository) GetDefaultPublicGPGKey(forceUpdate bool) (*GPGSettings, signingKey, _, _ := NewCommand("config", "--get", "user.signingkey").RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) gpgSettings.KeyID = strings.TrimSpace(signingKey) - format, _, _ := NewCommand("config", "--get", "gpg.format").RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) + format, _, _ := NewCommand("config", "--default", KeyTypeOpenPGP, "--get", "gpg.format").RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) gpgSettings.Format = strings.TrimSpace(format) defaultEmail, _, _ := NewCommand("config", "--get", "user.email").RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) diff --git a/modules/setting/repository.go b/modules/setting/repository.go index c8268eaf4460a..7cb427f71e281 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -255,6 +255,7 @@ var ( SigningKey: "default", SigningName: "", SigningEmail: "", + SigningFormat: "openpgp", InitialCommit: []string{"always"}, CRUDActions: []string{"pubkey", "twofa", "parentsigned"}, Merges: []string{"pubkey", "twofa", "basesigned", "commitssigned"}, diff --git a/routers/api/v1/misc/signing.go b/routers/api/v1/misc/signing.go index 78ac1bf02a07c..e66a1f70df9d0 100644 --- a/routers/api/v1/misc/signing.go +++ b/routers/api/v1/misc/signing.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" + "code.gitea.io/gitea/modules/git" asymkey_service "code.gitea.io/gitea/services/asymkey" "code.gitea.io/gitea/services/context" ) @@ -56,7 +57,7 @@ func SigningKey(ctx *context.APIContext) { ctx.APIErrorInternal(err) return } - if format == "ssh" { + if format != git.KeyTypeOpenPGP { ctx.APIErrorNotFound(errors.New("SSH keys are used for signing, not GPG")) return } @@ -111,7 +112,7 @@ func SigningKeySSH(ctx *context.APIContext) { ctx.APIErrorInternal(err) return } - if format != "ssh" { + if format != git.KeyTypeSSH { ctx.APIErrorNotFound(errors.New("GPG keys are used for signing, not SSH")) return } diff --git a/services/asymkey/commit.go b/services/asymkey/commit.go index 6de4611389ae8..93cea85b37355 100644 --- a/services/asymkey/commit.go +++ b/services/asymkey/commit.go @@ -414,7 +414,7 @@ func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committer * defaultReason := asymkey_model.NoKeyFound - if setting.Repository.Signing.SigningFormat == "ssh" && setting.Repository.Signing.SigningKey != "" && setting.Repository.Signing.SigningKey != "default" && setting.Repository.Signing.SigningKey != "none" { + if setting.Repository.Signing.SigningFormat == git.KeyTypeSSH && setting.Repository.Signing.SigningKey != "" && setting.Repository.Signing.SigningKey != "default" && setting.Repository.Signing.SigningKey != "none" { // OK we should try the default key gpgSettings := git.GPGSettings{ Sign: true, @@ -442,7 +442,7 @@ func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committer * } defaultGPGSettings, err := c.GetRepositoryDefaultPublicGPGKey(false) - if defaultGPGSettings.Format == "ssh" { + if defaultGPGSettings.Format == git.KeyTypeSSH { 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 57eb6782c5738..90ce63c33b40e 100644 --- a/services/asymkey/sign.go +++ b/services/asymkey/sign.go @@ -86,9 +86,9 @@ func IsErrWontSign(err error) bool { } // SigningKey returns the KeyID and git Signature for the repo -func SigningKey(ctx context.Context, repoPath string) (asymkey_model.SigningKey, *git.Signature) { +func SigningKey(ctx context.Context, repoPath string) (git.SigningKey, *git.Signature) { if setting.Repository.Signing.SigningKey == "none" { - return asymkey_model.SigningKey{}, nil + return git.SigningKey{}, nil } if setting.Repository.Signing.SigningKey == "default" || setting.Repository.Signing.SigningKey == "" { @@ -96,14 +96,14 @@ func SigningKey(ctx context.Context, repoPath string) (asymkey_model.SigningKey, value, _, _ := git.NewCommand("config", "--get", "commit.gpgsign").RunStdString(ctx, &git.RunOpts{Dir: repoPath}) sign, valid := git.ParseBool(strings.TrimSpace(value)) if !sign || !valid { - return asymkey_model.SigningKey{}, nil + return git.SigningKey{}, nil } - format, _, _ := git.NewCommand("config", "--get", "gpg.format").RunStdString(ctx, &git.RunOpts{Dir: repoPath}) + format, _, _ := git.NewCommand("config", "--default", git.KeyTypeOpenPGP, "--get", "gpg.format").RunStdString(ctx, &git.RunOpts{Dir: repoPath}) signingKey, _, _ := git.NewCommand("config", "--get", "user.signingkey").RunStdString(ctx, &git.RunOpts{Dir: repoPath}) signingName, _, _ := git.NewCommand("config", "--get", "user.name").RunStdString(ctx, &git.RunOpts{Dir: repoPath}) signingEmail, _, _ := git.NewCommand("config", "--get", "user.email").RunStdString(ctx, &git.RunOpts{Dir: repoPath}) - return asymkey_model.SigningKey{ + return git.SigningKey{ KeyID: strings.TrimSpace(signingKey), Format: strings.TrimSpace(format), }, &git.Signature{ @@ -112,7 +112,7 @@ func SigningKey(ctx context.Context, repoPath string) (asymkey_model.SigningKey, } } - return asymkey_model.SigningKey{ + return git.SigningKey{ KeyID: setting.Repository.Signing.SigningKey, Format: setting.Repository.Signing.SigningFormat, }, &git.Signature{ @@ -127,7 +127,7 @@ func PublicSigningKey(ctx context.Context, repoPath string) (string, string, err if signingKey.KeyID == "" { return "", signingKey.Format, nil } - if signingKey.Format == "ssh" { + if signingKey.Format == git.KeyTypeSSH { 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) @@ -146,18 +146,18 @@ func PublicSigningKey(ctx context.Context, repoPath string) (string, string, err } // SignInitialCommit determines if we should sign the initial commit to this repository -func SignInitialCommit(ctx context.Context, repoPath string, u *user_model.User) (bool, asymkey_model.SigningKey, *git.Signature, error) { +func SignInitialCommit(ctx context.Context, repoPath string, u *user_model.User) (bool, git.SigningKey, *git.Signature, error) { rules := signingModeFromStrings(setting.Repository.Signing.InitialCommit) signingKey, sig := SigningKey(ctx, repoPath) if signingKey.KeyID == "" { - return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{noKey} + return false, git.SigningKey{}, nil, &ErrWontSign{noKey} } Loop: for _, rule := range rules { switch rule { case never: - return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{never} + return false, git.SigningKey{}, nil, &ErrWontSign{never} case always: break Loop case pubkey: @@ -166,18 +166,18 @@ Loop: IncludeSubKeys: true, }) if err != nil { - return false, asymkey_model.SigningKey{}, nil, err + return false, git.SigningKey{}, nil, err } if len(keys) == 0 { - return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{pubkey} + return false, git.SigningKey{}, nil, &ErrWontSign{pubkey} } case twofa: twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID) if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) { - return false, asymkey_model.SigningKey{}, nil, err + return false, git.SigningKey{}, nil, err } if twofaModel == nil { - return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{twofa} + return false, git.SigningKey{}, nil, &ErrWontSign{twofa} } } } @@ -185,19 +185,19 @@ 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, asymkey_model.SigningKey, *git.Signature, error) { +func SignWikiCommit(ctx context.Context, repo *repo_model.Repository, u *user_model.User) (bool, git.SigningKey, *git.Signature, error) { repoWikiPath := repo.WikiPath() rules := signingModeFromStrings(setting.Repository.Signing.Wiki) signingKey, sig := SigningKey(ctx, repoWikiPath) if signingKey.KeyID == "" { - return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{noKey} + return false, git.SigningKey{}, nil, &ErrWontSign{noKey} } Loop: for _, rule := range rules { switch rule { case never: - return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{never} + return false, git.SigningKey{}, nil, &ErrWontSign{never} case always: break Loop case pubkey: @@ -206,35 +206,35 @@ Loop: IncludeSubKeys: true, }) if err != nil { - return false, asymkey_model.SigningKey{}, nil, err + return false, git.SigningKey{}, nil, err } if len(keys) == 0 { - return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{pubkey} + return false, git.SigningKey{}, nil, &ErrWontSign{pubkey} } case twofa: twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID) if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) { - return false, asymkey_model.SigningKey{}, nil, err + return false, git.SigningKey{}, nil, err } if twofaModel == nil { - return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{twofa} + return false, git.SigningKey{}, nil, &ErrWontSign{twofa} } case parentSigned: gitRepo, err := gitrepo.OpenRepository(ctx, repo.WikiStorageRepo()) if err != nil { - return false, asymkey_model.SigningKey{}, nil, err + return false, git.SigningKey{}, nil, err } defer gitRepo.Close() commit, err := gitRepo.GetCommit("HEAD") if err != nil { - return false, asymkey_model.SigningKey{}, nil, err + return false, git.SigningKey{}, nil, err } if commit.Signature == nil { - return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{parentSigned} + return false, git.SigningKey{}, nil, &ErrWontSign{parentSigned} } verification := ParseCommitWithSignature(ctx, commit) if !verification.Verified { - return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{parentSigned} + return false, git.SigningKey{}, nil, &ErrWontSign{parentSigned} } } } @@ -242,18 +242,18 @@ 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, asymkey_model.SigningKey, *git.Signature, error) { +func SignCRUDAction(ctx context.Context, repoPath string, u *user_model.User, tmpBasePath, parentCommit string) (bool, git.SigningKey, *git.Signature, error) { rules := signingModeFromStrings(setting.Repository.Signing.CRUDActions) signingKey, sig := SigningKey(ctx, repoPath) if signingKey.KeyID == "" { - return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{noKey} + return false, git.SigningKey{}, nil, &ErrWontSign{noKey} } Loop: for _, rule := range rules { switch rule { case never: - return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{never} + return false, git.SigningKey{}, nil, &ErrWontSign{never} case always: break Loop case pubkey: @@ -262,35 +262,35 @@ Loop: IncludeSubKeys: true, }) if err != nil { - return false, asymkey_model.SigningKey{}, nil, err + return false, git.SigningKey{}, nil, err } if len(keys) == 0 { - return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{pubkey} + return false, git.SigningKey{}, nil, &ErrWontSign{pubkey} } case twofa: twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID) if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) { - return false, asymkey_model.SigningKey{}, nil, err + return false, git.SigningKey{}, nil, err } if twofaModel == nil { - return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{twofa} + return false, git.SigningKey{}, nil, &ErrWontSign{twofa} } case parentSigned: gitRepo, err := git.OpenRepository(ctx, tmpBasePath) if err != nil { - return false, asymkey_model.SigningKey{}, nil, err + return false, git.SigningKey{}, nil, err } defer gitRepo.Close() commit, err := gitRepo.GetCommit(parentCommit) if err != nil { - return false, asymkey_model.SigningKey{}, nil, err + return false, git.SigningKey{}, nil, err } if commit.Signature == nil { - return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{parentSigned} + return false, git.SigningKey{}, nil, &ErrWontSign{parentSigned} } verification := ParseCommitWithSignature(ctx, commit) if !verification.Verified { - return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{parentSigned} + return false, git.SigningKey{}, nil, &ErrWontSign{parentSigned} } } } @@ -298,16 +298,16 @@ Loop: } // SignMerge determines if we should sign a PR merge commit to the base repository -func SignMerge(ctx context.Context, pr *issues_model.PullRequest, u *user_model.User, tmpBasePath, baseCommit, headCommit string) (bool, asymkey_model.SigningKey, *git.Signature, error) { +func SignMerge(ctx context.Context, pr *issues_model.PullRequest, u *user_model.User, tmpBasePath, baseCommit, headCommit string) (bool, git.SigningKey, *git.Signature, error) { if err := pr.LoadBaseRepo(ctx); err != nil { log.Error("Unable to get Base Repo for pull request") - return false, asymkey_model.SigningKey{}, nil, err + return false, git.SigningKey{}, nil, err } repo := pr.BaseRepo signingKey, signer := SigningKey(ctx, repo.RepoPath()) if signingKey.KeyID == "" { - return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{noKey} + return false, git.SigningKey{}, nil, &ErrWontSign{noKey} } rules := signingModeFromStrings(setting.Repository.Signing.Merges) @@ -318,7 +318,7 @@ Loop: for _, rule := range rules { switch rule { case never: - return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{never} + return false, git.SigningKey{}, nil, &ErrWontSign{never} case always: break Loop case pubkey: @@ -327,91 +327,91 @@ Loop: IncludeSubKeys: true, }) if err != nil { - return false, asymkey_model.SigningKey{}, nil, err + return false, git.SigningKey{}, nil, err } if len(keys) == 0 { - return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{pubkey} + return false, git.SigningKey{}, nil, &ErrWontSign{pubkey} } case twofa: twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID) if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) { - return false, asymkey_model.SigningKey{}, nil, err + return false, git.SigningKey{}, nil, err } if twofaModel == nil { - return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{twofa} + return false, git.SigningKey{}, nil, &ErrWontSign{twofa} } case approved: protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, pr.BaseBranch) if err != nil { - return false, asymkey_model.SigningKey{}, nil, err + return false, git.SigningKey{}, nil, err } if protectedBranch == nil { - return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{approved} + return false, git.SigningKey{}, nil, &ErrWontSign{approved} } if issues_model.GetGrantedApprovalsCount(ctx, protectedBranch, pr) < 1 { - return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{approved} + return false, git.SigningKey{}, nil, &ErrWontSign{approved} } case baseSigned: if gitRepo == nil { gitRepo, err = git.OpenRepository(ctx, tmpBasePath) if err != nil { - return false, asymkey_model.SigningKey{}, nil, err + return false, git.SigningKey{}, nil, err } defer gitRepo.Close() } commit, err := gitRepo.GetCommit(baseCommit) if err != nil { - return false, asymkey_model.SigningKey{}, nil, err + return false, git.SigningKey{}, nil, err } verification := ParseCommitWithSignature(ctx, commit) if !verification.Verified { - return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{baseSigned} + return false, git.SigningKey{}, nil, &ErrWontSign{baseSigned} } case headSigned: if gitRepo == nil { gitRepo, err = git.OpenRepository(ctx, tmpBasePath) if err != nil { - return false, asymkey_model.SigningKey{}, nil, err + return false, git.SigningKey{}, nil, err } defer gitRepo.Close() } commit, err := gitRepo.GetCommit(headCommit) if err != nil { - return false, asymkey_model.SigningKey{}, nil, err + return false, git.SigningKey{}, nil, err } verification := ParseCommitWithSignature(ctx, commit) if !verification.Verified { - return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{headSigned} + return false, git.SigningKey{}, nil, &ErrWontSign{headSigned} } case commitsSigned: if gitRepo == nil { gitRepo, err = git.OpenRepository(ctx, tmpBasePath) if err != nil { - return false, asymkey_model.SigningKey{}, nil, err + return false, git.SigningKey{}, nil, err } defer gitRepo.Close() } commit, err := gitRepo.GetCommit(headCommit) if err != nil { - return false, asymkey_model.SigningKey{}, nil, err + return false, git.SigningKey{}, nil, err } verification := ParseCommitWithSignature(ctx, commit) if !verification.Verified { - return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{commitsSigned} + return false, git.SigningKey{}, nil, &ErrWontSign{commitsSigned} } // need to work out merge-base mergeBaseCommit, _, err := gitRepo.GetMergeBase("", baseCommit, headCommit) if err != nil { - return false, asymkey_model.SigningKey{}, nil, err + return false, git.SigningKey{}, nil, err } commitList, err := commit.CommitsBeforeUntil(mergeBaseCommit) if err != nil { - return false, asymkey_model.SigningKey{}, nil, err + return false, git.SigningKey{}, nil, err } for _, commit := range commitList { verification := ParseCommitWithSignature(ctx, commit) if !verification.Verified { - return false, asymkey_model.SigningKey{}, nil, &ErrWontSign{commitsSigned} + return false, git.SigningKey{}, nil, &ErrWontSign{commitsSigned} } } } diff --git a/services/context/repo.go b/services/context/repo.go index 3521b7cd7bebf..1a5951c2ccaa8 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -14,7 +14,6 @@ import ( "path" "strings" - asymkey_model "code.gitea.io/gitea/models/asymkey" "code.gitea.io/gitea/models/db" git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" @@ -102,7 +101,7 @@ type CanCommitToBranchResults struct { UserCanPush bool RequireSigned bool WillSign bool - SigningKey asymkey_model.SigningKey + SigningKey git.SigningKey WontSignReason string } diff --git a/services/pull/merge_prepare.go b/services/pull/merge_prepare.go index 093ea0ed379b0..e28ca49659b53 100644 --- a/services/pull/merge_prepare.go +++ b/services/pull/merge_prepare.go @@ -14,7 +14,6 @@ import ( "strings" "time" - asymkey_model "code.gitea.io/gitea/models/asymkey" issues_model "code.gitea.io/gitea/models/issues" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" @@ -28,7 +27,7 @@ type mergeContext struct { doer *user_model.User sig *git.Signature committer *git.Signature - signKey asymkey_model.SigningKey // empty for no-sign, non-empty to sign + signKey git.SigningKey // empty for no-sign, non-empty to sign env []string } diff --git a/services/repository/files/temp_repo.go b/services/repository/files/temp_repo.go index 2f7a1c8b3d89b..c0f21731372f8 100644 --- a/services/repository/files/temp_repo.go +++ b/services/repository/files/temp_repo.go @@ -14,7 +14,6 @@ import ( "strings" "time" - asymkey_model "code.gitea.io/gitea/models/asymkey" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" @@ -294,7 +293,7 @@ func (t *TemporaryUploadRepository) CommitTree(ctx context.Context, opts *Commit } var sign bool - var key asymkey_model.SigningKey + 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) From 785934613b957d1146db1e99dcde562ce6dee9d3 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Sat, 3 May 2025 10:44:48 +0200 Subject: [PATCH 6/8] Cleanup CommitTreeOpts --- modules/git/repo_tree.go | 11 +++++------ services/wiki/wiki.go | 6 ++---- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/modules/git/repo_tree.go b/modules/git/repo_tree.go index 064b1317f07ad..0d25b9d3a1835 100644 --- a/modules/git/repo_tree.go +++ b/modules/git/repo_tree.go @@ -15,8 +15,7 @@ import ( type CommitTreeOpts struct { Parents []string Message string - KeyID string - KeyFormat string + Key SigningKey NoGPGSign bool AlwaysSign bool } @@ -44,11 +43,11 @@ func (repo *Repository) CommitTree(author, committer *Signature, tree *Tree, opt _, _ = messageBytes.WriteString(opts.Message) _, _ = messageBytes.WriteString("\n") - if opts.KeyID != "" || opts.AlwaysSign { - if opts.KeyFormat != "" { - cmd.AddConfig("gpg.format", opts.KeyFormat) + if opts.Key.KeyID != "" || opts.AlwaysSign { + if opts.Key.Format != "" { + cmd.AddConfig("gpg.format", opts.Key.Format) } - cmd.AddOptionFormat("-S%s", opts.KeyID) + cmd.AddOptionFormat("-S%s", opts.Key.KeyID) } if opts.NoGPGSign { diff --git a/services/wiki/wiki.go b/services/wiki/wiki.go index b42c896740aa2..6f028c881239c 100644 --- a/services/wiki/wiki.go +++ b/services/wiki/wiki.go @@ -194,8 +194,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model sign, signingKey, signer, _ := asymkey_service.SignWikiCommit(ctx, repo, doer) if sign { - commitTreeOpts.KeyID = signingKey.KeyID - commitTreeOpts.KeyFormat = signingKey.Format + commitTreeOpts.Key = signingKey if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel { committer = signer } @@ -317,8 +316,7 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model sign, signingKey, signer, _ := asymkey_service.SignWikiCommit(ctx, repo, doer) if sign { - commitTreeOpts.KeyID = signingKey.KeyID - commitTreeOpts.KeyFormat = signingKey.Format + commitTreeOpts.Key = signingKey if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel { committer = signer } From 3c363c5cf7ec3a462d883e91d33ab9a483f7c910 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Sat, 3 May 2025 10:48:20 +0200 Subject: [PATCH 7/8] add missing docs --- modules/git/key.go | 3 ++- modules/setting/repository.go | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/git/key.go b/modules/git/key.go index 1653c9e49793e..a6ea543ef1c5b 100644 --- a/modules/git/key.go +++ b/modules/git/key.go @@ -3,8 +3,9 @@ package git +// Based on https://git-scm.com/docs/git-config#Documentation/git-config.txt-gpgformat const ( - // KeyTypeOpenPGP is the key type for GPG keys + // KeyTypeOpenPGP is the key type for GPG keys, expected default of git cli KeyTypeOpenPGP = "openpgp" // KeyTypeSSH is the key type for SSH keys KeyTypeSSH = "ssh" diff --git a/modules/setting/repository.go b/modules/setting/repository.go index 7cb427f71e281..bf0ae923d4459 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -255,7 +255,7 @@ var ( SigningKey: "default", SigningName: "", SigningEmail: "", - SigningFormat: "openpgp", + SigningFormat: "openpgp", // git.KeyTypeOpenPGP InitialCommit: []string{"always"}, CRUDActions: []string{"pubkey", "twofa", "parentsigned"}, Merges: []string{"pubkey", "twofa", "basesigned", "commitssigned"}, From 3e992a0cd369951a5464b5ee544ceacd63ed9206 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Sat, 3 May 2025 13:27:59 +0200 Subject: [PATCH 8/8] add missing endpoint --- routers/api/v1/api.go | 1 + 1 file changed, 1 insertion(+) diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 3b1447dc68a09..8ec7d4f6c3c98 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1426,6 +1426,7 @@ func Routes() *web.Router { Get(repo.GetFileContentsGet). Post(bind(api.GetFilesOptions{}), repo.GetFileContentsPost) // POST method requires "write" permission, so we also support "GET" method above m.Get("/signing-key.gpg", misc.SigningKey) + m.Get("/signing-key.pub", misc.SigningKeySSH) m.Group("/topics", func() { m.Combo("").Get(repo.ListTopics). Put(reqToken(), reqAdmin(), bind(api.RepoTopicOptions{}), repo.UpdateTopics)