From fc8fc5a4c092636676e3db45f9ea4549335586d7 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Fri, 2 May 2025 11:17:04 +0200 Subject: [PATCH 01/45] 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 02/45] 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 03/45] 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 04/45] 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 05/45] 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 06/45] 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 07/45] 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 08/45] 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) From a73ccb46bcf2b8b53bb7c442d5679b384be0a523 Mon Sep 17 00:00:00 2001 From: ChristopherHX Date: Sun, 11 May 2025 10:39:43 +0200 Subject: [PATCH 09/45] Update modules/git/repo_gpg.go Co-authored-by: Lunny Xiao --- modules/git/repo_gpg.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/git/repo_gpg.go b/modules/git/repo_gpg.go index e226757af4c87..6eff247f66480 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 == KeyTypeOpenPGP { + if gpgSettings.Format == KeyTypeSSH { content, err := os.ReadFile(gpgSettings.KeyID) if err != nil { return fmt.Errorf("unable to read SSH public key file: %s, %w", gpgSettings.KeyID, err) From 8007fba4b14cc40010d0b6c0f56e81bff0153921 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Sun, 11 May 2025 11:04:26 +0200 Subject: [PATCH 10/45] update app.example.ini --- custom/conf/app.example.ini | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index a7476ad1be818..13c0e6bc6dbbb 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1190,17 +1190,21 @@ LEVEL = Info ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; -;; GPG key to use to sign commits, Defaults to the default - that is the value of git config --get user.signingkey +;; GPG or SSH key to use to sign commits, Defaults to the default - that is the value of git config --get user.signingkey +;; Depending on the value of SIGNING_FORMAT this is either: +;; - the GPG key ID +;; - the path to the ssh public key "/path/to/key.pub": where "/path/to/key" is the private key, use ssh-keygen -t ed25519 to generate a new key pair without password ;; run in the context of the RUN_USER ;; Switch to none to stop signing completely ;SIGNING_KEY = default ;; -;; If a SIGNING_KEY ID is provided and is not set to default, use the provided Name and Email address as the signer. +;; If a SIGNING_KEY ID is provided and is not set to default, use the provided Name and Email address as the signer and the signing format. ;; These should match a publicized name and email address for the key. (When SIGNING_KEY is default these are set to -;; the results of git config --get user.name and git config --get user.email respectively and can only be overridden +;; the results of git config --get user.name, git config --get user.email and git config --default openpgp --get gpg.format respectively and can only be overridden ;; by setting the SIGNING_KEY ID to the correct ID.) ;SIGNING_NAME = ;SIGNING_EMAIL = +;SIGNING_FORMAT = openpgp ;; ;; Sets the default trust model for repositories. Options are: collaborator, committer, collaboratorcommitter ;DEFAULT_TRUST_MODEL = collaborator @@ -1227,6 +1231,12 @@ LEVEL = Info ;; - commitssigned: require that all the commits in the head branch are signed. ;; - approved: only sign when merging an approved pr to a protected branch ;MERGES = pubkey, twofa, basesigned, commitssigned +;; +;; Determines which additional ssh keys are trusted for all signed commits regardless of the user +;; This is useful for ssh signing key rotation. +;; Multiple keys should be comma separated. +;; E.g."ssh- ". or "ssh- , ssh- ". +;TRUSTED_SSH_KEYS = ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; From 6ae80f9d21e759498dda255f0a5e744fecec4b48 Mon Sep 17 00:00:00 2001 From: ChristopherHX Date: Sun, 11 May 2025 21:56:07 +0200 Subject: [PATCH 11/45] Update modules/git/command.go Co-authored-by: Lunny Xiao --- 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 dfc58fb3f5053..3598f978af80b 100644 --- a/modules/git/command.go +++ b/modules/git/command.go @@ -331,7 +331,7 @@ func (c *Command) run(ctx context.Context, skip int, opts *RunOpts) error { startTime := time.Now() - cmd := exec.CommandContext(ctx, c.prog, append(append([]string{}, c.configArgs...), c.args...)...) + cmd := exec.CommandContext(ctx, c.prog, append(c.configArgs, c.args...)...) c.cmd = cmd // for debug purpose only if opts.Env == nil { cmd.Env = os.Environ() From 6c0feaa420e8ee7f1c5a569a37edf6e975ff605c Mon Sep 17 00:00:00 2001 From: ChristopherHX Date: Mon, 12 May 2025 10:56:17 +0200 Subject: [PATCH 12/45] Update modules/git/command.go Co-authored-by: Lunny Xiao --- modules/git/command.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/git/command.go b/modules/git/command.go index 3598f978af80b..51c225e3b1863 100644 --- a/modules/git/command.go +++ b/modules/git/command.go @@ -201,8 +201,9 @@ func (c *Command) AddConfig(key, value string) *Command { kv := key + "=" + value if !isSafeArgumentValue(kv) { c.brokenArgs = append(c.brokenArgs, key) + } else { + c.configArgs = append(c.configArgs, "-c", kv) } - c.configArgs = append(c.configArgs, "-c", kv) return c } From 98bcc7cb1cfad261a0cb02fc6a70a5ad354b88d1 Mon Sep 17 00:00:00 2001 From: ChristopherHX Date: Mon, 12 May 2025 11:31:44 +0200 Subject: [PATCH 13/45] fix indent --- 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 51c225e3b1863..22f1d02339148 100644 --- a/modules/git/command.go +++ b/modules/git/command.go @@ -202,7 +202,7 @@ func (c *Command) AddConfig(key, value string) *Command { if !isSafeArgumentValue(kv) { c.brokenArgs = append(c.brokenArgs, key) } else { - c.configArgs = append(c.configArgs, "-c", kv) + c.configArgs = append(c.configArgs, "-c", kv) } return c } From dce379767c8d6fe3a6c5c33ee66e4a54d3d1e261 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Sun, 1 Jun 2025 01:03:06 +0200 Subject: [PATCH 14/45] reuse gpg tests and expand them to ssh --- services/asymkey/commit.go | 10 +- .../{gpg_git_test.go => gpg_ssh_git_test.go} | 97 +++++++++++++++++++ 2 files changed, 105 insertions(+), 2 deletions(-) rename tests/integration/{gpg_git_test.go => gpg_ssh_git_test.go} (78%) diff --git a/services/asymkey/commit.go b/services/asymkey/commit.go index 93cea85b37355..745517907edec 100644 --- a/services/asymkey/commit.go +++ b/services/asymkey/commit.go @@ -432,7 +432,10 @@ func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committer * Content: gpgSettings.PublicKeyContent, Fingerprint: fingerprint, HasUsed: true, - }, committer, committer, committer.Email); commitVerification != nil { + }, committer, &user_model.User{ + Name: gpgSettings.Name, + Email: gpgSettings.Email, + }, gpgSettings.Email); commitVerification != nil { if commitVerification.Reason == asymkey_model.BadSignature { defaultReason = asymkey_model.BadSignature } else { @@ -457,7 +460,10 @@ func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committer * Content: defaultGPGSettings.PublicKeyContent, Fingerprint: fingerprint, HasUsed: true, - }, committer, committer, committer.Email); commitVerification != nil { + }, committer, &user_model.User{ + Name: defaultGPGSettings.Name, + Email: defaultGPGSettings.Email, + }, defaultGPGSettings.Email); commitVerification != nil { if commitVerification.Reason == asymkey_model.BadSignature { defaultReason = asymkey_model.BadSignature } else { diff --git a/tests/integration/gpg_git_test.go b/tests/integration/gpg_ssh_git_test.go similarity index 78% rename from tests/integration/gpg_git_test.go rename to tests/integration/gpg_ssh_git_test.go index 32de200f63d66..346af588aba96 100644 --- a/tests/integration/gpg_git_test.go +++ b/tests/integration/gpg_ssh_git_test.go @@ -4,7 +4,10 @@ package integration import ( + "crypto/ed25519" + "crypto/rand" "encoding/base64" + "encoding/pem" "fmt" "net/url" "os" @@ -18,6 +21,7 @@ import ( api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/tests" + "golang.org/x/crypto/ssh" "github.com/ProtonMail/go-crypto/openpgp" "github.com/ProtonMail/go-crypto/openpgp/armor" @@ -42,6 +46,99 @@ func TestGPGGit(t *testing.T) { defer test.MockVariableValue(&setting.Repository.Signing.InitialCommit, []string{"never"})() defer test.MockVariableValue(&setting.Repository.Signing.CRUDActions, []string{"never"})() + testGitSigning(t) +} + +func TestGPGGitDefaults(t *testing.T) { + tmpDir := t.TempDir() // use a temp dir to avoid messing with the user's GPG keyring + err := os.Chmod(tmpDir, 0o700) + assert.NoError(t, err) + + t.Setenv("GNUPGHOME", tmpDir) + + // Need to create a root key + rootKeyPair, err := importTestingKey() + require.NoError(t, err, "importTestingKey") + + os.WriteFile(tmpDir+"/.gitconfig", []byte(` +[user] + name = gitea + email = gitea@fake.local + signingKey = `+rootKeyPair.PrimaryKey.KeyIdShortString()+` +[commit] + gpgsign = true +`), 0o600) + + defer test.MockVariableValue(&setting.Repository.Signing.SigningKey, "")() + defer test.MockVariableValue(&setting.Repository.Signing.InitialCommit, []string{"never"})() + defer test.MockVariableValue(&setting.Repository.Signing.CRUDActions, []string{"never"})() + defer test.MockVariableValue(&setting.Git.HomePath, tmpDir)() + + testGitSigning(t) +} + +func TestSSHGit(t *testing.T) { + tmpDir := t.TempDir() // use a temp dir to avoid messing with the user's GPG keyring + err := os.Chmod(tmpDir, 0o700) + assert.NoError(t, err) + + pub, priv, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err, "ed25519.GenerateKey") + sshPubKey, err := ssh.NewPublicKey(pub) + require.NoError(t, err, "ssh.NewPublicKey") + _, err = ssh.NewSignerFromKey(priv) + require.NoError(t, err, "ssh.NewSignerFromKey") + + os.WriteFile(tmpDir+"/id_ed25519.pub", []byte(ssh.MarshalAuthorizedKey(sshPubKey)), 0o600) + block, err := ssh.MarshalPrivateKey(priv, "") + os.WriteFile(tmpDir+"/id_ed25519", []byte(pem.EncodeToMemory(block)), 0o600) + + defer test.MockVariableValue(&setting.Repository.Signing.SigningKey, tmpDir+"/id_ed25519.pub")() + defer test.MockVariableValue(&setting.Repository.Signing.SigningName, "gitea")() + defer test.MockVariableValue(&setting.Repository.Signing.SigningEmail, "gitea@fake.local")() + defer test.MockVariableValue(&setting.Repository.Signing.SigningFormat, "ssh")() + defer test.MockVariableValue(&setting.Repository.Signing.InitialCommit, []string{"never"})() + defer test.MockVariableValue(&setting.Repository.Signing.CRUDActions, []string{"never"})() + + testGitSigning(t) +} + +func TestSSHGitDefaults(t *testing.T) { + tmpDir := t.TempDir() // use a temp dir to avoid messing with the user's GPG keyring + err := os.Chmod(tmpDir, 0o700) + assert.NoError(t, err) + + pub, priv, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err, "ed25519.GenerateKey") + sshPubKey, err := ssh.NewPublicKey(pub) + require.NoError(t, err, "ssh.NewPublicKey") + _, err = ssh.NewSignerFromKey(priv) + require.NoError(t, err, "ssh.NewSignerFromKey") + + os.WriteFile(tmpDir+"/id_ed25519.pub", []byte(ssh.MarshalAuthorizedKey(sshPubKey)), 0o600) + block, err := ssh.MarshalPrivateKey(priv, "") + os.WriteFile(tmpDir+"/id_ed25519", []byte(pem.EncodeToMemory(block)), 0o600) + os.WriteFile(tmpDir+"/.gitconfig", []byte(` +[user] + name = gitea + email = gitea@fake.local + signingKey = `+tmpDir+`/id_ed25519.pub +[gpg] + format = ssh +[commit] + gpgsign = true +`), 0o600) + + defer test.MockVariableValue(&setting.Repository.Signing.SigningKey, "")() + defer test.MockVariableValue(&setting.Repository.Signing.SigningFormat, "ssh")() + defer test.MockVariableValue(&setting.Repository.Signing.InitialCommit, []string{"never"})() + defer test.MockVariableValue(&setting.Repository.Signing.CRUDActions, []string{"never"})() + defer test.MockVariableValue(&setting.Git.HomePath, tmpDir)() + + testGitSigning(t) +} + +func testGitSigning(t *testing.T) { username := "user2" user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: username}) baseAPITestContext := NewAPITestContext(t, username, "repo1") From 48a1d6dc2a11de27251c66bafe2ede58de5572e9 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Sun, 1 Jun 2025 01:04:11 +0200 Subject: [PATCH 15/45] rename keyID to key --- services/pull/merge_prepare.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/pull/merge_prepare.go b/services/pull/merge_prepare.go index e28ca49659b53..c1b606ed2ea19 100644 --- a/services/pull/merge_prepare.go +++ b/services/pull/merge_prepare.go @@ -99,9 +99,9 @@ func createTemporaryRepoForMerge(ctx context.Context, pr *issues_model.PullReque mergeCtx.committer = mergeCtx.sig // Determine if we should sign - sign, keyID, signer, _ := asymkey_service.SignMerge(ctx, mergeCtx.pr, mergeCtx.doer, mergeCtx.tmpBasePath, "HEAD", trackingBranch) + sign, key, signer, _ := asymkey_service.SignMerge(ctx, mergeCtx.pr, mergeCtx.doer, mergeCtx.tmpBasePath, "HEAD", trackingBranch) if sign { - mergeCtx.signKey = keyID + mergeCtx.signKey = key if pr.BaseRepo.GetTrustModel() == repo_model.CommitterTrustModel || pr.BaseRepo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel { mergeCtx.committer = signer } From e749a14910ecc04fb51738bff6a0243706626f4b Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Sun, 1 Jun 2025 01:04:40 +0200 Subject: [PATCH 16/45] update comment that signKey.KeyID may be empty --- services/pull/merge_prepare.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/pull/merge_prepare.go b/services/pull/merge_prepare.go index c1b606ed2ea19..eb9aeb08eb558 100644 --- a/services/pull/merge_prepare.go +++ b/services/pull/merge_prepare.go @@ -27,7 +27,7 @@ type mergeContext struct { doer *user_model.User sig *git.Signature committer *git.Signature - signKey git.SigningKey // empty for no-sign, non-empty to sign + signKey git.SigningKey // signKey.KeyID empty for no-sign, non-empty to sign env []string } From 984557de979b875c26a452b87f29b308448c37d2 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Sun, 1 Jun 2025 01:06:30 +0200 Subject: [PATCH 17/45] improve example description --- custom/conf/app.example.ini | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 13c0e6bc6dbbb..5b3c069154eaa 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1192,8 +1192,8 @@ LEVEL = Info ;; ;; GPG or SSH key to use to sign commits, Defaults to the default - that is the value of git config --get user.signingkey ;; Depending on the value of SIGNING_FORMAT this is either: -;; - the GPG key ID -;; - the path to the ssh public key "/path/to/key.pub": where "/path/to/key" is the private key, use ssh-keygen -t ed25519 to generate a new key pair without password +;; - openpgp: the GPG key ID +;; - ssh: the path to the ssh public key "/path/to/key.pub": where "/path/to/key" is the private key, use ssh-keygen -t ed25519 to generate a new key pair without password ;; run in the context of the RUN_USER ;; Switch to none to stop signing completely ;SIGNING_KEY = default @@ -1204,6 +1204,9 @@ LEVEL = Info ;; by setting the SIGNING_KEY ID to the correct ID.) ;SIGNING_NAME = ;SIGNING_EMAIL = +;; SIGNING_FORMAT can be one of: +;; - openpgp (default): use GPG to sign commits +;; - ssh: use SSH to sign commits ;SIGNING_FORMAT = openpgp ;; ;; Sets the default trust model for repositories. Options are: collaborator, committer, collaboratorcommitter From da2c19eea5a5fc490781bb78f0e51d0db3434999 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Sun, 1 Jun 2025 01:13:53 +0200 Subject: [PATCH 18/45] format test code --- tests/integration/gpg_ssh_git_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/gpg_ssh_git_test.go b/tests/integration/gpg_ssh_git_test.go index 346af588aba96..5193a2d38aeaf 100644 --- a/tests/integration/gpg_ssh_git_test.go +++ b/tests/integration/gpg_ssh_git_test.go @@ -21,12 +21,12 @@ import ( api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/tests" - "golang.org/x/crypto/ssh" "github.com/ProtonMail/go-crypto/openpgp" "github.com/ProtonMail/go-crypto/openpgp/armor" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "golang.org/x/crypto/ssh" ) func TestGPGGit(t *testing.T) { From 39eda94a0d66f5907703dec986cb9b99c35379d8 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Sun, 1 Jun 2025 01:24:40 +0200 Subject: [PATCH 19/45] fix error handling in test --- tests/integration/gpg_ssh_git_test.go | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/integration/gpg_ssh_git_test.go b/tests/integration/gpg_ssh_git_test.go index 5193a2d38aeaf..1305741574213 100644 --- a/tests/integration/gpg_ssh_git_test.go +++ b/tests/integration/gpg_ssh_git_test.go @@ -89,9 +89,12 @@ func TestSSHGit(t *testing.T) { _, err = ssh.NewSignerFromKey(priv) require.NoError(t, err, "ssh.NewSignerFromKey") - os.WriteFile(tmpDir+"/id_ed25519.pub", []byte(ssh.MarshalAuthorizedKey(sshPubKey)), 0o600) + err = os.WriteFile(tmpDir+"/id_ed25519.pub", ssh.MarshalAuthorizedKey(sshPubKey), 0o600) + require.NoError(t, err, "os.WriteFile id_ed25519.pub") block, err := ssh.MarshalPrivateKey(priv, "") - os.WriteFile(tmpDir+"/id_ed25519", []byte(pem.EncodeToMemory(block)), 0o600) + require.NoError(t, err, "ssh.MarshalPrivateKey") + err = os.WriteFile(tmpDir+"/id_ed25519", pem.EncodeToMemory(block), 0o600) + require.NoError(t, err, "os.WriteFile id_ed25519") defer test.MockVariableValue(&setting.Repository.Signing.SigningKey, tmpDir+"/id_ed25519.pub")() defer test.MockVariableValue(&setting.Repository.Signing.SigningName, "gitea")() @@ -115,9 +118,12 @@ func TestSSHGitDefaults(t *testing.T) { _, err = ssh.NewSignerFromKey(priv) require.NoError(t, err, "ssh.NewSignerFromKey") - os.WriteFile(tmpDir+"/id_ed25519.pub", []byte(ssh.MarshalAuthorizedKey(sshPubKey)), 0o600) + err = os.WriteFile(tmpDir+"/id_ed25519.pub", ssh.MarshalAuthorizedKey(sshPubKey), 0o600) + require.NoError(t, err, "os.WriteFile id_ed25519.pub") block, err := ssh.MarshalPrivateKey(priv, "") - os.WriteFile(tmpDir+"/id_ed25519", []byte(pem.EncodeToMemory(block)), 0o600) + require.NoError(t, err, "ssh.MarshalPrivateKey") + err = os.WriteFile(tmpDir+"/id_ed25519", pem.EncodeToMemory(block), 0o600) + require.NoError(t, err, "os.WriteFile id_ed25519") os.WriteFile(tmpDir+"/.gitconfig", []byte(` [user] name = gitea From e45c6fb642d47187149999a09bb8be3802812c50 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Sun, 1 Jun 2025 01:27:02 +0200 Subject: [PATCH 20/45] handle another error in test --- tests/integration/gpg_ssh_git_test.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/integration/gpg_ssh_git_test.go b/tests/integration/gpg_ssh_git_test.go index 1305741574213..44b6e617ed180 100644 --- a/tests/integration/gpg_ssh_git_test.go +++ b/tests/integration/gpg_ssh_git_test.go @@ -60,7 +60,7 @@ func TestGPGGitDefaults(t *testing.T) { rootKeyPair, err := importTestingKey() require.NoError(t, err, "importTestingKey") - os.WriteFile(tmpDir+"/.gitconfig", []byte(` + err = os.WriteFile(tmpDir+"/.gitconfig", []byte(` [user] name = gitea email = gitea@fake.local @@ -68,6 +68,7 @@ func TestGPGGitDefaults(t *testing.T) { [commit] gpgsign = true `), 0o600) + require.NoError(t, err, "os.WriteFile .gitconfig") defer test.MockVariableValue(&setting.Repository.Signing.SigningKey, "")() defer test.MockVariableValue(&setting.Repository.Signing.InitialCommit, []string{"never"})() @@ -124,7 +125,7 @@ func TestSSHGitDefaults(t *testing.T) { require.NoError(t, err, "ssh.MarshalPrivateKey") err = os.WriteFile(tmpDir+"/id_ed25519", pem.EncodeToMemory(block), 0o600) require.NoError(t, err, "os.WriteFile id_ed25519") - os.WriteFile(tmpDir+"/.gitconfig", []byte(` + err = os.WriteFile(tmpDir+"/.gitconfig", []byte(` [user] name = gitea email = gitea@fake.local @@ -134,6 +135,7 @@ func TestSSHGitDefaults(t *testing.T) { [commit] gpgsign = true `), 0o600) + require.NoError(t, err, "os.WriteFile .gitconfig") defer test.MockVariableValue(&setting.Repository.Signing.SigningKey, "")() defer test.MockVariableValue(&setting.Repository.Signing.SigningFormat, "ssh")() From 66194b226d051a6d9bd1c738758db2ca0e8fe69b Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Sun, 1 Jun 2025 01:39:46 +0200 Subject: [PATCH 21/45] remove dead code --- tests/integration/gpg_ssh_git_test.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/integration/gpg_ssh_git_test.go b/tests/integration/gpg_ssh_git_test.go index 44b6e617ed180..97a1bbbf293ce 100644 --- a/tests/integration/gpg_ssh_git_test.go +++ b/tests/integration/gpg_ssh_git_test.go @@ -87,8 +87,6 @@ func TestSSHGit(t *testing.T) { require.NoError(t, err, "ed25519.GenerateKey") sshPubKey, err := ssh.NewPublicKey(pub) require.NoError(t, err, "ssh.NewPublicKey") - _, err = ssh.NewSignerFromKey(priv) - require.NoError(t, err, "ssh.NewSignerFromKey") err = os.WriteFile(tmpDir+"/id_ed25519.pub", ssh.MarshalAuthorizedKey(sshPubKey), 0o600) require.NoError(t, err, "os.WriteFile id_ed25519.pub") @@ -116,8 +114,6 @@ func TestSSHGitDefaults(t *testing.T) { require.NoError(t, err, "ed25519.GenerateKey") sshPubKey, err := ssh.NewPublicKey(pub) require.NoError(t, err, "ssh.NewPublicKey") - _, err = ssh.NewSignerFromKey(priv) - require.NoError(t, err, "ssh.NewSignerFromKey") err = os.WriteFile(tmpDir+"/id_ed25519.pub", ssh.MarshalAuthorizedKey(sshPubKey), 0o600) require.NoError(t, err, "os.WriteFile id_ed25519.pub") From d3af10fc5874f2d72c448eacdd294bc2661b3a4c Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Fri, 6 Jun 2025 16:09:51 +0200 Subject: [PATCH 22/45] fix error handling and add comments about what function verifies which config --- services/asymkey/commit.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/services/asymkey/commit.go b/services/asymkey/commit.go index 745517907edec..3a00127ba44ea 100644 --- a/services/asymkey/commit.go +++ b/services/asymkey/commit.go @@ -414,6 +414,7 @@ func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committer * defaultReason := asymkey_model.NoKeyFound + // Covers ssh verification for the default SSH signing key specifed in gitea config 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{ @@ -425,9 +426,9 @@ func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committer * } 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{ + } else if fingerprint, err := asymkey_model.CalcFingerprint(gpgSettings.PublicKeyContent); err != nil { + log.Error("Error calculating the fingerprint public key: %s %v", gpgSettings.KeyID, err) + } else if commitVerification := verifySSHCommitVerification(c.Signature.Signature, c.Signature.Payload, &asymkey_model.PublicKey{ Verified: true, Content: gpgSettings.PublicKeyContent, Fingerprint: fingerprint, @@ -444,18 +445,19 @@ func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committer * } } + // Covers ssh verification for the default SSH signing key specifed in the .gitconfig file in Git.HomePath setting defaultGPGSettings, err := c.GetRepositoryDefaultPublicGPGKey(false) if defaultGPGSettings.Format == git.KeyTypeSSH { if err != nil { - log.Error("Error getting default public gpg key: %v", err) + log.Error("Error getting default public ssh 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{ + } else if fingerprint, err := asymkey_model.CalcFingerprint(defaultGPGSettings.PublicKeyContent); err != nil { + log.Error("Error calculating the fingerprint public key: %s %v", defaultGPGSettings.KeyID, err) + } else if commitVerification := verifySSHCommitVerification(c.Signature.Signature, c.Signature.Payload, &asymkey_model.PublicKey{ Verified: true, Content: defaultGPGSettings.PublicKeyContent, Fingerprint: fingerprint, From c0b65efa94e4ef3e48b9b94edb3d6a324616e3e9 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Fri, 6 Jun 2025 16:15:13 +0200 Subject: [PATCH 23/45] apply suggestion do not write error to already written response --- routers/api/v1/misc/signing.go | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/routers/api/v1/misc/signing.go b/routers/api/v1/misc/signing.go index e66a1f70df9d0..ff82494f33124 100644 --- a/routers/api/v1/misc/signing.go +++ b/routers/api/v1/misc/signing.go @@ -5,7 +5,6 @@ package misc import ( "errors" - "fmt" "code.gitea.io/gitea/modules/git" asymkey_service "code.gitea.io/gitea/services/asymkey" @@ -58,13 +57,10 @@ func SigningKey(ctx *context.APIContext) { return } if format != git.KeyTypeOpenPGP { - ctx.APIErrorNotFound(errors.New("SSH keys are used for signing, not GPG")) + ctx.APIErrorNotFound("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)) - } + _, _ = ctx.Write([]byte(content)) } // SigningKey returns the public key of the default signing key if it exists @@ -116,8 +112,5 @@ func SigningKeySSH(ctx *context.APIContext) { ctx.APIErrorNotFound(errors.New("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)) - } + _, _ = ctx.Write([]byte(content)) } From 64729ae1b699c93a83216fb7ec9a23cbe5cbf749 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Fri, 6 Jun 2025 16:22:32 +0200 Subject: [PATCH 24/45] update comments --- tests/integration/gpg_ssh_git_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration/gpg_ssh_git_test.go b/tests/integration/gpg_ssh_git_test.go index 97a1bbbf293ce..4f41e58ab118e 100644 --- a/tests/integration/gpg_ssh_git_test.go +++ b/tests/integration/gpg_ssh_git_test.go @@ -50,7 +50,7 @@ func TestGPGGit(t *testing.T) { } func TestGPGGitDefaults(t *testing.T) { - tmpDir := t.TempDir() // use a temp dir to avoid messing with the user's GPG keyring + tmpDir := t.TempDir() // use a temp dir to avoid messing with the user's GPG keyring and git config err := os.Chmod(tmpDir, 0o700) assert.NoError(t, err) @@ -79,7 +79,7 @@ func TestGPGGitDefaults(t *testing.T) { } func TestSSHGit(t *testing.T) { - tmpDir := t.TempDir() // use a temp dir to avoid messing with the user's GPG keyring + tmpDir := t.TempDir() // use a temp dir to store the SSH keys err := os.Chmod(tmpDir, 0o700) assert.NoError(t, err) @@ -106,7 +106,7 @@ func TestSSHGit(t *testing.T) { } func TestSSHGitDefaults(t *testing.T) { - tmpDir := t.TempDir() // use a temp dir to avoid messing with the user's GPG keyring + tmpDir := t.TempDir() // use a temp dir to store the SSH keys and git config err := os.Chmod(tmpDir, 0o700) assert.NoError(t, err) From 5af804af67a17e0160b142e8e1d63859bc233e09 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Fri, 6 Jun 2025 16:44:19 +0200 Subject: [PATCH 25/45] do not ignore fingerprint calc error --- services/asymkey/commit.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/services/asymkey/commit.go b/services/asymkey/commit.go index 3a00127ba44ea..e4732393c54d7 100644 --- a/services/asymkey/commit.go +++ b/services/asymkey/commit.go @@ -400,14 +400,14 @@ 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{ + if fingerprint, err := asymkey_model.CalcFingerprint(k); err != nil { + log.Error("Error calculating the fingerprint public key: %s %v", k, err) + } else if 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 { + }, committer, committer, c.Committer.Email); commitVerification != nil { return commitVerification } } From 0f413c54fc7c3bf972fd3edfd1c83832e19ea1dd Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Fri, 6 Jun 2025 20:06:51 +0200 Subject: [PATCH 26/45] Add TestTrustedSSHKeys Test via mocked repo --- .../user2/repo-test-trusted-ssh-keys.git/HEAD | 1 + .../repo-test-trusted-ssh-keys.git/config | 6 ++++++ .../description | 1 + .../info/exclude | 6 ++++++ .../9a/93ffa76e8b72bdb6431910b3a506fa2b39f42e | Bin 0 -> 55 bytes .../b7/fca3aa0d80144f9718b0486681944f9c587e33 | 4 ++++ .../b9/04b3dcaada8c26236f096fe337fd3c4cd8048a | Bin 0 -> 36 bytes .../packed-refs | 1 + .../refs/heads/main | 1 + tests/integration/gpg_ssh_git_test.go | 20 ++++++++++++++++++ 10 files changed, 40 insertions(+) create mode 100644 tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/HEAD create mode 100644 tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/config create mode 100644 tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/description create mode 100644 tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/info/exclude create mode 100644 tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/objects/9a/93ffa76e8b72bdb6431910b3a506fa2b39f42e create mode 100644 tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/objects/b7/fca3aa0d80144f9718b0486681944f9c587e33 create mode 100644 tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/objects/b9/04b3dcaada8c26236f096fe337fd3c4cd8048a create mode 100644 tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/packed-refs create mode 100644 tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/refs/heads/main diff --git a/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/HEAD b/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/HEAD new file mode 100644 index 0000000000000..b870d82622c1a --- /dev/null +++ b/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/HEAD @@ -0,0 +1 @@ +ref: refs/heads/main diff --git a/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/config b/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/config new file mode 100644 index 0000000000000..e6da231579bcc --- /dev/null +++ b/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/config @@ -0,0 +1,6 @@ +[core] + repositoryformatversion = 0 + filemode = true + bare = true + ignorecase = true + precomposeunicode = true diff --git a/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/description b/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/description new file mode 100644 index 0000000000000..498b267a8c781 --- /dev/null +++ b/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/description @@ -0,0 +1 @@ +Unnamed repository; edit this file 'description' to name the repository. diff --git a/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/info/exclude b/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/info/exclude new file mode 100644 index 0000000000000..a5196d1be8fb5 --- /dev/null +++ b/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/info/exclude @@ -0,0 +1,6 @@ +# git ls-files --others --exclude-from=.git/info/exclude +# Lines that start with '#' are comments. +# For a project mostly in C, the following would be a good set of +# exclude patterns (uncomment them if you want to use them): +# *.[oa] +# *~ diff --git a/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/objects/9a/93ffa76e8b72bdb6431910b3a506fa2b39f42e b/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/objects/9a/93ffa76e8b72bdb6431910b3a506fa2b39f42e new file mode 100644 index 0000000000000000000000000000000000000000..bcf0704cd0cbba115cb93c01e583249df6ed4f46 GIT binary patch literal 55 zcmV-70LcG%0V^p=O;s?qU@$Z=Ff%bxD9%jJOHI)$sVHIC$+G#*s#`s3%K4o6kInzu N_}pOW0sv$I5hQpJ8C(DW literal 0 HcmV?d00001 diff --git a/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/objects/b7/fca3aa0d80144f9718b0486681944f9c587e33 b/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/objects/b7/fca3aa0d80144f9718b0486681944f9c587e33 new file mode 100644 index 0000000000000..6d64c797ca642 --- /dev/null +++ b/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/objects/b7/fca3aa0d80144f9718b0486681944f9c587e33 @@ -0,0 +1,4 @@ +x��[o�0��ٿb�QW�� ��� $�@��˛�`�ͥ���&�k_z���|ҙ�NZ�Ϫ�55��� +�f�pl9L��ۤ�k�� + ۙ u3�H$��PVײ�VŸ�s䟼��.��<��1]B1�. 0�u���t���r��*��^�`2X��� +'���S��� c\L�^�n�E�2Z?���X�S��L��x�Iʭ��_�/�hxzl����1��.1'�r�'�)>M�x��&&Aw-����dL�M��b�ぅ��KC��m]��4��f��׹x��I+�g,�n��Y�Ҷ��Z8�� rN�k����v�It���[�������}�0��2PxQ�|Uo�9@��E�!S�D?J�� \ No newline at end of file diff --git a/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/objects/b9/04b3dcaada8c26236f096fe337fd3c4cd8048a b/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/objects/b9/04b3dcaada8c26236f096fe337fd3c4cd8048a new file mode 100644 index 0000000000000000000000000000000000000000..ee5dcf6ba398b5e76d1df5e90ee6d0ba63ddfcbe GIT binary patch literal 36 ucmV+<0Nej~0ZYosPf{>4VhG8|ELH%bM1|ta^t{v*g|y6^R6PKbBMHGIZxA~G literal 0 HcmV?d00001 diff --git a/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/packed-refs b/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/packed-refs new file mode 100644 index 0000000000000..250f187384963 --- /dev/null +++ b/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/packed-refs @@ -0,0 +1 @@ +# pack-refs with: peeled fully-peeled sorted diff --git a/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/refs/heads/main b/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/refs/heads/main new file mode 100644 index 0000000000000..8b4ec6bcd860c --- /dev/null +++ b/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/refs/heads/main @@ -0,0 +1 @@ +b7fca3aa0d80144f9718b0486681944f9c587e33 diff --git a/tests/integration/gpg_ssh_git_test.go b/tests/integration/gpg_ssh_git_test.go index 4f41e58ab118e..84f5e072cd339 100644 --- a/tests/integration/gpg_ssh_git_test.go +++ b/tests/integration/gpg_ssh_git_test.go @@ -366,3 +366,23 @@ func importTestingKey() (*openpgp.Entity, error) { // There should only be one entity in this file. return keyring[0], nil } + +func TestTrustedSSHKeys(t *testing.T) { + defer tests.PrepareTestEnv(t)() + defer test.MockVariableValue(&setting.Repository.Signing.TrustedSSHKeys, []string{"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH6Y4idVaW3E+bLw1uqoAfJD7o5Siu+HqS51E9oQLPE9"})() + + username := "user2" + testCtx := NewAPITestContext(t, username, "repo-test-trusted-ssh-keys", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + t.Run("CheckMainBranchSignedVerified", doAPIGetBranch(testCtx, "main", func(t *testing.T, branch api.Branch) { + require.NotNil(t, branch.Commit, "no commit provided with branch! %v", branch) + require.NotNil(t, branch.Commit.Verification, "no verification provided with branch commit! %v", branch.Commit) + require.True(t, branch.Commit.Verification.Verified) + })) + + setting.Repository.Signing.TrustedSSHKeys = []string{} + t.Run("CheckMainBranchSignedUnverified", doAPIGetBranch(testCtx, "main", func(t *testing.T, branch api.Branch) { + require.NotNil(t, branch.Commit, "no commit provided with branch! %v", branch) + require.NotNil(t, branch.Commit.Verification, "no verification provided with branch commit! %v", branch.Commit) + require.False(t, branch.Commit.Verification.Verified) + })) +} From d8e123aecbc298123cc966b656ebe3f42cf184d2 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Fri, 6 Jun 2025 20:11:54 +0200 Subject: [PATCH 27/45] fix typo --- services/asymkey/commit.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/asymkey/commit.go b/services/asymkey/commit.go index e4732393c54d7..d648571b70e98 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 - // Covers ssh verification for the default SSH signing key specifed in gitea config + // Covers ssh verification for the default SSH signing key specified in gitea config 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{ @@ -445,7 +445,7 @@ func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committer * } } - // Covers ssh verification for the default SSH signing key specifed in the .gitconfig file in Git.HomePath setting + // Covers ssh verification for the default SSH signing key specified in the .gitconfig file in Git.HomePath setting defaultGPGSettings, err := c.GetRepositoryDefaultPublicGPGKey(false) if defaultGPGSettings.Format == git.KeyTypeSSH { if err != nil { From 6f18abc7e9d1745ae6975447242d62a5f9b6ee28 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Fri, 6 Jun 2025 20:15:32 +0200 Subject: [PATCH 28/45] fix invalid APIErrorNotFound usage --- routers/api/v1/misc/signing.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/routers/api/v1/misc/signing.go b/routers/api/v1/misc/signing.go index ff82494f33124..e2779fc966a64 100644 --- a/routers/api/v1/misc/signing.go +++ b/routers/api/v1/misc/signing.go @@ -4,8 +4,6 @@ package misc import ( - "errors" - "code.gitea.io/gitea/modules/git" asymkey_service "code.gitea.io/gitea/services/asymkey" "code.gitea.io/gitea/services/context" @@ -109,7 +107,7 @@ func SigningKeySSH(ctx *context.APIContext) { return } if format != git.KeyTypeSSH { - ctx.APIErrorNotFound(errors.New("GPG keys are used for signing, not SSH")) + ctx.APIErrorNotFound("GPG keys are used for signing, not SSH") return } _, _ = ctx.Write([]byte(content)) From 3da66460fe0f83874d77fcc801f5b5f2216a69f9 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Fri, 6 Jun 2025 20:55:37 +0200 Subject: [PATCH 29/45] add missing fixtures --- models/fixtures/branch.yml | 12 ++++++++++++ models/fixtures/repo_unit.yml | 7 +++++++ models/fixtures/repository.yml | 31 +++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/models/fixtures/branch.yml b/models/fixtures/branch.yml index 6536e1dda7b84..32ea21ebf69fc 100644 --- a/models/fixtures/branch.yml +++ b/models/fixtures/branch.yml @@ -201,3 +201,15 @@ is_deleted: false deleted_by_id: 0 deleted_unix: 0 + +- + id: 25 + repo_id: 63 + name: 'main' + commit_id: 'b7fca3aa0d80144f9718b0486681944f9c587e33' + commit_message: "Initial commit with signed file" + commit_time: 1602935385 + pusher_id: 2 + is_deleted: false + deleted_by_id: 0 + deleted_unix: 0 diff --git a/models/fixtures/repo_unit.yml b/models/fixtures/repo_unit.yml index f6b6252da1f88..59ca712885f49 100644 --- a/models/fixtures/repo_unit.yml +++ b/models/fixtures/repo_unit.yml @@ -733,3 +733,10 @@ type: 3 config: "{\"IgnoreWhitespaceConflicts\":false,\"AllowMerge\":true,\"AllowRebase\":true,\"AllowRebaseMerge\":true,\"AllowSquash\":true}" created_unix: 946684810 + +- + id: 111 + repo_id: 63 + type: 1 + config: "{}" + created_unix: 946684810 diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml index 552a78cbd2773..8a618e0a72d60 100644 --- a/models/fixtures/repository.yml +++ b/models/fixtures/repository.yml @@ -1786,3 +1786,34 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false + +- + id: 63 + owner_id: 2 + owner_name: user2 + lower_name: repo-test-trusted-ssh-keys + name: repo-test-trusted-ssh-keys + default_branch: main + num_watches: 4 + num_stars: 0 + num_forks: 0 + num_issues: 2 + num_closed_issues: 1 + num_pulls: 3 + num_closed_pulls: 0 + num_milestones: 3 + num_closed_milestones: 1 + num_projects: 1 + num_closed_projects: 0 + is_private: false + is_empty: false + is_archived: false + is_mirror: false + status: 0 + is_fork: false + fork_id: 0 + is_template: false + template_id: 0 + size: 0 + is_fsck_enabled: true + close_issues_via_commit_in_any_branch: false From 9c8837bd995e0fbffa1814499c6226daee86599a Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Fri, 6 Jun 2025 21:09:44 +0200 Subject: [PATCH 30/45] Use pointer to git.SigningKey --- modules/git/repo_tree.go | 6 +- routers/web/repo/setting/setting.go | 4 +- services/asymkey/sign.go | 135 +++++++++++++------------ services/context/repo.go | 2 +- services/pull/merge.go | 2 +- services/pull/merge_prepare.go | 2 +- services/pull/merge_squash.go | 2 +- services/repository/files/temp_repo.go | 2 +- 8 files changed, 83 insertions(+), 72 deletions(-) diff --git a/modules/git/repo_tree.go b/modules/git/repo_tree.go index 0d25b9d3a1835..309a73d759544 100644 --- a/modules/git/repo_tree.go +++ b/modules/git/repo_tree.go @@ -15,7 +15,7 @@ import ( type CommitTreeOpts struct { Parents []string Message string - Key SigningKey + Key *SigningKey NoGPGSign bool AlwaysSign bool } @@ -43,11 +43,13 @@ func (repo *Repository) CommitTree(author, committer *Signature, tree *Tree, opt _, _ = messageBytes.WriteString(opts.Message) _, _ = messageBytes.WriteString("\n") - if opts.Key.KeyID != "" || opts.AlwaysSign { + if opts.Key != nil { if opts.Key.Format != "" { cmd.AddConfig("gpg.format", opts.Key.Format) } cmd.AddOptionFormat("-S%s", opts.Key.KeyID) + } else if opts.AlwaysSign { + cmd.AddOptionFormat("-S") } if opts.NoGPGSign { diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index bebb4867bc4c3..6602685e94888 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.KeyID) > 0 + ctx.Data["SigningKeyAvailable"] = signing != nil 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.KeyID) > 0 + ctx.Data["SigningKeyAvailable"] = signing != nil ctx.Data["SigningSettings"] = setting.Repository.Signing ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled diff --git a/services/asymkey/sign.go b/services/asymkey/sign.go index 90ce63c33b40e..93e5c691a180a 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) (git.SigningKey, *git.Signature) { +func SigningKey(ctx context.Context, repoPath string) (*git.SigningKey, *git.Signature) { if setting.Repository.Signing.SigningKey == "none" { - return git.SigningKey{}, nil + return nil, nil } if setting.Repository.Signing.SigningKey == "default" || setting.Repository.Signing.SigningKey == "" { @@ -96,14 +96,19 @@ func SigningKey(ctx context.Context, repoPath string) (git.SigningKey, *git.Sign value, _, _ := git.NewCommand("config", "--get", "commit.gpgsign").RunStdString(ctx, &git.RunOpts{Dir: repoPath}) sign, valid := git.ParseBool(strings.TrimSpace(value)) if !sign || !valid { - return git.SigningKey{}, nil + return nil, nil } 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 git.SigningKey{ + + if strings.TrimSpace(signingKey) == "" { + return nil, nil + } + + return &git.SigningKey{ KeyID: strings.TrimSpace(signingKey), Format: strings.TrimSpace(format), }, &git.Signature{ @@ -112,7 +117,11 @@ func SigningKey(ctx context.Context, repoPath string) (git.SigningKey, *git.Sign } } - return git.SigningKey{ + if setting.Repository.Signing.SigningKey == "" { + return nil, nil + } + + return &git.SigningKey{ KeyID: setting.Repository.Signing.SigningKey, Format: setting.Repository.Signing.SigningFormat, }, &git.Signature{ @@ -124,8 +133,8 @@ func SigningKey(ctx context.Context, repoPath string) (git.SigningKey, *git.Sign // PublicSigningKey gets the public signing key within a provided repository directory func PublicSigningKey(ctx context.Context, repoPath string) (string, string, error) { signingKey, _ := SigningKey(ctx, repoPath) - if signingKey.KeyID == "" { - return "", signingKey.Format, nil + if signingKey == nil { + return "", "", nil } if signingKey.Format == git.KeyTypeSSH { content, err := os.ReadFile(signingKey.KeyID) @@ -146,18 +155,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, git.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, git.SigningKey{}, nil, &ErrWontSign{noKey} + if signingKey == nil { + return false, nil, nil, &ErrWontSign{noKey} } Loop: for _, rule := range rules { switch rule { case never: - return false, git.SigningKey{}, nil, &ErrWontSign{never} + return false, nil, nil, &ErrWontSign{never} case always: break Loop case pubkey: @@ -166,18 +175,18 @@ Loop: IncludeSubKeys: true, }) if err != nil { - return false, git.SigningKey{}, nil, err + return false, nil, nil, err } if len(keys) == 0 { - return false, git.SigningKey{}, nil, &ErrWontSign{pubkey} + return false, nil, nil, &ErrWontSign{pubkey} } case twofa: twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID) if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) { - return false, git.SigningKey{}, nil, err + return false, nil, nil, err } if twofaModel == nil { - return false, git.SigningKey{}, nil, &ErrWontSign{twofa} + return false, nil, nil, &ErrWontSign{twofa} } } } @@ -185,19 +194,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, git.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, git.SigningKey{}, nil, &ErrWontSign{noKey} + if signingKey == nil { + return false, nil, nil, &ErrWontSign{noKey} } Loop: for _, rule := range rules { switch rule { case never: - return false, git.SigningKey{}, nil, &ErrWontSign{never} + return false, nil, nil, &ErrWontSign{never} case always: break Loop case pubkey: @@ -206,35 +215,35 @@ Loop: IncludeSubKeys: true, }) if err != nil { - return false, git.SigningKey{}, nil, err + return false, nil, nil, err } if len(keys) == 0 { - return false, git.SigningKey{}, nil, &ErrWontSign{pubkey} + return false, nil, nil, &ErrWontSign{pubkey} } case twofa: twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID) if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) { - return false, git.SigningKey{}, nil, err + return false, nil, nil, err } if twofaModel == nil { - return false, git.SigningKey{}, nil, &ErrWontSign{twofa} + return false, nil, nil, &ErrWontSign{twofa} } case parentSigned: gitRepo, err := gitrepo.OpenRepository(ctx, repo.WikiStorageRepo()) if err != nil { - return false, git.SigningKey{}, nil, err + return false, nil, nil, err } defer gitRepo.Close() commit, err := gitRepo.GetCommit("HEAD") if err != nil { - return false, git.SigningKey{}, nil, err + return false, nil, nil, err } if commit.Signature == nil { - return false, git.SigningKey{}, nil, &ErrWontSign{parentSigned} + return false, nil, nil, &ErrWontSign{parentSigned} } verification := ParseCommitWithSignature(ctx, commit) if !verification.Verified { - return false, git.SigningKey{}, nil, &ErrWontSign{parentSigned} + return false, nil, nil, &ErrWontSign{parentSigned} } } } @@ -242,18 +251,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, git.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, git.SigningKey{}, nil, &ErrWontSign{noKey} + if signingKey == nil { + return false, nil, nil, &ErrWontSign{noKey} } Loop: for _, rule := range rules { switch rule { case never: - return false, git.SigningKey{}, nil, &ErrWontSign{never} + return false, nil, nil, &ErrWontSign{never} case always: break Loop case pubkey: @@ -262,35 +271,35 @@ Loop: IncludeSubKeys: true, }) if err != nil { - return false, git.SigningKey{}, nil, err + return false, nil, nil, err } if len(keys) == 0 { - return false, git.SigningKey{}, nil, &ErrWontSign{pubkey} + return false, nil, nil, &ErrWontSign{pubkey} } case twofa: twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID) if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) { - return false, git.SigningKey{}, nil, err + return false, nil, nil, err } if twofaModel == nil { - return false, git.SigningKey{}, nil, &ErrWontSign{twofa} + return false, nil, nil, &ErrWontSign{twofa} } case parentSigned: gitRepo, err := git.OpenRepository(ctx, tmpBasePath) if err != nil { - return false, git.SigningKey{}, nil, err + return false, nil, nil, err } defer gitRepo.Close() commit, err := gitRepo.GetCommit(parentCommit) if err != nil { - return false, git.SigningKey{}, nil, err + return false, nil, nil, err } if commit.Signature == nil { - return false, git.SigningKey{}, nil, &ErrWontSign{parentSigned} + return false, nil, nil, &ErrWontSign{parentSigned} } verification := ParseCommitWithSignature(ctx, commit) if !verification.Verified { - return false, git.SigningKey{}, nil, &ErrWontSign{parentSigned} + return false, nil, nil, &ErrWontSign{parentSigned} } } } @@ -298,16 +307,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, git.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, git.SigningKey{}, nil, err + return false, nil, nil, err } repo := pr.BaseRepo signingKey, signer := SigningKey(ctx, repo.RepoPath()) - if signingKey.KeyID == "" { - return false, git.SigningKey{}, nil, &ErrWontSign{noKey} + if signingKey == nil { + return false, nil, nil, &ErrWontSign{noKey} } rules := signingModeFromStrings(setting.Repository.Signing.Merges) @@ -318,7 +327,7 @@ Loop: for _, rule := range rules { switch rule { case never: - return false, git.SigningKey{}, nil, &ErrWontSign{never} + return false, nil, nil, &ErrWontSign{never} case always: break Loop case pubkey: @@ -327,91 +336,91 @@ Loop: IncludeSubKeys: true, }) if err != nil { - return false, git.SigningKey{}, nil, err + return false, nil, nil, err } if len(keys) == 0 { - return false, git.SigningKey{}, nil, &ErrWontSign{pubkey} + return false, nil, nil, &ErrWontSign{pubkey} } case twofa: twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID) if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) { - return false, git.SigningKey{}, nil, err + return false, nil, nil, err } if twofaModel == nil { - return false, git.SigningKey{}, nil, &ErrWontSign{twofa} + return false, nil, nil, &ErrWontSign{twofa} } case approved: protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, pr.BaseBranch) if err != nil { - return false, git.SigningKey{}, nil, err + return false, nil, nil, err } if protectedBranch == nil { - return false, git.SigningKey{}, nil, &ErrWontSign{approved} + return false, nil, nil, &ErrWontSign{approved} } if issues_model.GetGrantedApprovalsCount(ctx, protectedBranch, pr) < 1 { - return false, git.SigningKey{}, nil, &ErrWontSign{approved} + return false, nil, nil, &ErrWontSign{approved} } case baseSigned: if gitRepo == nil { gitRepo, err = git.OpenRepository(ctx, tmpBasePath) if err != nil { - return false, git.SigningKey{}, nil, err + return false, nil, nil, err } defer gitRepo.Close() } commit, err := gitRepo.GetCommit(baseCommit) if err != nil { - return false, git.SigningKey{}, nil, err + return false, nil, nil, err } verification := ParseCommitWithSignature(ctx, commit) if !verification.Verified { - return false, git.SigningKey{}, nil, &ErrWontSign{baseSigned} + return false, nil, nil, &ErrWontSign{baseSigned} } case headSigned: if gitRepo == nil { gitRepo, err = git.OpenRepository(ctx, tmpBasePath) if err != nil { - return false, git.SigningKey{}, nil, err + return false, nil, nil, err } defer gitRepo.Close() } commit, err := gitRepo.GetCommit(headCommit) if err != nil { - return false, git.SigningKey{}, nil, err + return false, nil, nil, err } verification := ParseCommitWithSignature(ctx, commit) if !verification.Verified { - return false, git.SigningKey{}, nil, &ErrWontSign{headSigned} + return false, nil, nil, &ErrWontSign{headSigned} } case commitsSigned: if gitRepo == nil { gitRepo, err = git.OpenRepository(ctx, tmpBasePath) if err != nil { - return false, git.SigningKey{}, nil, err + return false, nil, nil, err } defer gitRepo.Close() } commit, err := gitRepo.GetCommit(headCommit) if err != nil { - return false, git.SigningKey{}, nil, err + return false, nil, nil, err } verification := ParseCommitWithSignature(ctx, commit) if !verification.Verified { - return false, git.SigningKey{}, nil, &ErrWontSign{commitsSigned} + return false, nil, nil, &ErrWontSign{commitsSigned} } // need to work out merge-base mergeBaseCommit, _, err := gitRepo.GetMergeBase("", baseCommit, headCommit) if err != nil { - return false, git.SigningKey{}, nil, err + return false, nil, nil, err } commitList, err := commit.CommitsBeforeUntil(mergeBaseCommit) if err != nil { - return false, git.SigningKey{}, nil, err + return false, nil, nil, err } for _, commit := range commitList { verification := ParseCommitWithSignature(ctx, commit) if !verification.Verified { - return false, git.SigningKey{}, nil, &ErrWontSign{commitsSigned} + return false, nil, nil, &ErrWontSign{commitsSigned} } } } diff --git a/services/context/repo.go b/services/context/repo.go index f7f3b66c0dafd..dafa398dc851b 100644 --- a/services/context/repo.go +++ b/services/context/repo.go @@ -101,7 +101,7 @@ type CanCommitToBranchResults struct { UserCanPush bool RequireSigned bool WillSign bool - SigningKey git.SigningKey + SigningKey *git.SigningKey WontSignReason string } diff --git a/services/pull/merge.go b/services/pull/merge.go index 42b1871a9f44f..06e00664dc676 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -396,7 +396,7 @@ 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.signKey.KeyID == "" { + if ctx.signKey == nil { cmdCommit.AddArguments("--no-gpg-sign") } else { if ctx.signKey.Format != "" { diff --git a/services/pull/merge_prepare.go b/services/pull/merge_prepare.go index eb9aeb08eb558..31a1e137344e9 100644 --- a/services/pull/merge_prepare.go +++ b/services/pull/merge_prepare.go @@ -27,7 +27,7 @@ type mergeContext struct { doer *user_model.User sig *git.Signature committer *git.Signature - signKey git.SigningKey // signKey.KeyID empty for no-sign, non-empty to sign + signKey *git.SigningKey env []string } diff --git a/services/pull/merge_squash.go b/services/pull/merge_squash.go index 6de319ca2e444..bc9bdcebe5689 100644 --- a/services/pull/merge_squash.go +++ b/services/pull/merge_squash.go @@ -74,7 +74,7 @@ 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.signKey.KeyID == "" { + if ctx.signKey == nil { cmdCommit.AddArguments("--no-gpg-sign") } else { if ctx.signKey.Format != "" { diff --git a/services/repository/files/temp_repo.go b/services/repository/files/temp_repo.go index c0f21731372f8..1cf30edc7b548 100644 --- a/services/repository/files/temp_repo.go +++ b/services/repository/files/temp_repo.go @@ -293,7 +293,7 @@ func (t *TemporaryUploadRepository) CommitTree(ctx context.Context, opts *Commit } var sign bool - var key git.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 6f6ddd04b0dbd1e25f316464c7db3da15651bb98 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Fri, 6 Jun 2025 21:12:13 +0200 Subject: [PATCH 31/45] fix fixture --- models/fixtures/repository.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml index 8a618e0a72d60..2adc2342e3054 100644 --- a/models/fixtures/repository.yml +++ b/models/fixtures/repository.yml @@ -1794,16 +1794,16 @@ lower_name: repo-test-trusted-ssh-keys name: repo-test-trusted-ssh-keys default_branch: main - num_watches: 4 + num_watches: 0 num_stars: 0 num_forks: 0 - num_issues: 2 - num_closed_issues: 1 - num_pulls: 3 + num_issues: 0 + num_closed_issues: 0 + num_pulls: 0 num_closed_pulls: 0 - num_milestones: 3 - num_closed_milestones: 1 - num_projects: 1 + num_milestones: 0 + num_closed_milestones: 0 + num_projects: 0 num_closed_projects: 0 is_private: false is_empty: false From 59eed16514e686364badd5473425435f5d439f8d Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Fri, 6 Jun 2025 23:02:01 +0200 Subject: [PATCH 32/45] . --- models/fixtures/user.yml | 2 +- tests/integration/api_repo_test.go | 6 +++--- tests/integration/oauth_test.go | 4 ++++ 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml index 976a236011cc9..b3bece5589c9b 100644 --- a/models/fixtures/user.yml +++ b/models/fixtures/user.yml @@ -67,7 +67,7 @@ num_followers: 2 num_following: 1 num_stars: 2 - num_repos: 14 + num_repos: 15 num_teams: 0 num_members: 0 visibility: 0 diff --git a/tests/integration/api_repo_test.go b/tests/integration/api_repo_test.go index 672c2a2c8bf9b..df8de083ca087 100644 --- a/tests/integration/api_repo_test.go +++ b/tests/integration/api_repo_test.go @@ -94,9 +94,9 @@ func TestAPISearchRepo(t *testing.T) { }{ { name: "RepositoriesMax50", requestURL: "/api/v1/repos/search?limit=50&private=false", expectedResults: expectedResults{ - nil: {count: 36}, - user: {count: 36}, - user2: {count: 36}, + nil: {count: 37}, + user: {count: 37}, + user2: {count: 37}, }, }, { diff --git a/tests/integration/oauth_test.go b/tests/integration/oauth_test.go index d2228bae7992a..50c15021ee938 100644 --- a/tests/integration/oauth_test.go +++ b/tests/integration/oauth_test.go @@ -691,6 +691,10 @@ func TestOAuth_GrantScopesReadRepositoryFailOrganization(t *testing.T) { FullRepoName: "user2/commitsonpr", Private: false, }, + { + FullRepoName: "user2/repo-test-trusted-ssh-keys", + Private: false, + }, } assert.Equal(t, reposExpected, reposCaptured) From 82e7e2d267ba440513ffbc3e6bdae34712b3f321 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Fri, 6 Jun 2025 23:25:07 +0200 Subject: [PATCH 33/45] fix more tests --- models/repo/repo_list_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/models/repo/repo_list_test.go b/models/repo/repo_list_test.go index ca6007f6c7882..6b1bb39b85410 100644 --- a/models/repo/repo_list_test.go +++ b/models/repo/repo_list_test.go @@ -138,27 +138,27 @@ func getTestCases() []struct { { name: "AllPublic/PublicRepositoriesOfUserIncludingCollaborative", opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, AllPublic: true, Template: optional.Some(false)}, - count: 34, + count: 35, }, { name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborative", opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true, AllLimited: true, Template: optional.Some(false)}, - count: 39, + count: 40, }, { name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborativeByName", opts: &repo_model.SearchRepoOptions{Keyword: "test", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true}, - count: 15, + count: 16, }, { name: "AllPublic/PublicAndPrivateRepositoriesOfUser2IncludingCollaborativeByName", opts: &repo_model.SearchRepoOptions{Keyword: "test", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Private: true, AllPublic: true}, - count: 13, + count: 14, }, { name: "AllPublic/PublicRepositoriesOfOrganization", opts: &repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, AllPublic: true, Collaborate: optional.Some(false), Template: optional.Some(false)}, - count: 34, + count: 35, }, { name: "AllTemplates", From 452f58bab1ef6e48776db38984aad33d9468588a Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Fri, 6 Jun 2025 23:47:00 +0200 Subject: [PATCH 34/45] finally fix another test --- models/fixtures/branch.yml | 2 +- models/fixtures/repo_unit.yml | 34 +++++++++++++++++++ models/fixtures/repository.yml | 2 +- .../user2/repo-test-trusted-ssh-keys.git/HEAD | 2 +- .../refs/heads/{main => master} | 0 5 files changed, 37 insertions(+), 3 deletions(-) rename tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/refs/heads/{main => master} (100%) diff --git a/models/fixtures/branch.yml b/models/fixtures/branch.yml index 32ea21ebf69fc..c27ac71bf42d3 100644 --- a/models/fixtures/branch.yml +++ b/models/fixtures/branch.yml @@ -205,7 +205,7 @@ - id: 25 repo_id: 63 - name: 'main' + name: 'master' commit_id: 'b7fca3aa0d80144f9718b0486681944f9c587e33' commit_message: "Initial commit with signed file" commit_time: 1602935385 diff --git a/models/fixtures/repo_unit.yml b/models/fixtures/repo_unit.yml index 59ca712885f49..cd8c3a973fd96 100644 --- a/models/fixtures/repo_unit.yml +++ b/models/fixtures/repo_unit.yml @@ -737,6 +737,40 @@ - id: 111 repo_id: 63 + type: 4 + config: "{}" + created_unix: 946684810 + +- + id: 112 + repo_id: 63 + type: 5 + config: "{}" + created_unix: 946684810 + +- + id: 113 + repo_id: 63 type: 1 config: "{}" created_unix: 946684810 + +- + id: 114 + repo_id: 63 + type: 2 + config: "{}" + created_unix: 946684810 + +- + id: 115 + repo_id: 63 + type: 8 + created_unix: 946684810 + +- + id: 116 + repo_id: 63 + type: 3 + config: "{}" + created_unix: 946684810 diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml index 2adc2342e3054..93048e44d0a8d 100644 --- a/models/fixtures/repository.yml +++ b/models/fixtures/repository.yml @@ -1793,7 +1793,7 @@ owner_name: user2 lower_name: repo-test-trusted-ssh-keys name: repo-test-trusted-ssh-keys - default_branch: main + default_branch: master num_watches: 0 num_stars: 0 num_forks: 0 diff --git a/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/HEAD b/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/HEAD index b870d82622c1a..cb089cd89a7d7 100644 --- a/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/HEAD +++ b/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/HEAD @@ -1 +1 @@ -ref: refs/heads/main +ref: refs/heads/master diff --git a/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/refs/heads/main b/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/refs/heads/master similarity index 100% rename from tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/refs/heads/main rename to tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/refs/heads/master From 494921a78b029ebd9033b9ae7bfd71a8dd9cdfd8 Mon Sep 17 00:00:00 2001 From: ChristopherHX Date: Sat, 7 Jun 2025 00:01:02 +0200 Subject: [PATCH 35/45] fix my own test to use master branch --- tests/integration/gpg_ssh_git_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/gpg_ssh_git_test.go b/tests/integration/gpg_ssh_git_test.go index 84f5e072cd339..8d964c7b2da6f 100644 --- a/tests/integration/gpg_ssh_git_test.go +++ b/tests/integration/gpg_ssh_git_test.go @@ -373,14 +373,14 @@ func TestTrustedSSHKeys(t *testing.T) { username := "user2" testCtx := NewAPITestContext(t, username, "repo-test-trusted-ssh-keys", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) - t.Run("CheckMainBranchSignedVerified", doAPIGetBranch(testCtx, "main", func(t *testing.T, branch api.Branch) { + t.Run("CheckMasterBranchSignedVerified", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) { require.NotNil(t, branch.Commit, "no commit provided with branch! %v", branch) require.NotNil(t, branch.Commit.Verification, "no verification provided with branch commit! %v", branch.Commit) require.True(t, branch.Commit.Verification.Verified) })) setting.Repository.Signing.TrustedSSHKeys = []string{} - t.Run("CheckMainBranchSignedUnverified", doAPIGetBranch(testCtx, "main", func(t *testing.T, branch api.Branch) { + t.Run("CheckMasterBranchSignedUnverified", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) { require.NotNil(t, branch.Commit, "no commit provided with branch! %v", branch) require.NotNil(t, branch.Commit.Verification, "no verification provided with branch commit! %v", branch.Commit) require.False(t, branch.Commit.Verification.Verified) From 3824905e1ea00a32897bc79cb3a671a405be2a77 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Sat, 7 Jun 2025 17:01:38 +0200 Subject: [PATCH 36/45] remove verify of GetRepositoryDefaultPublicGPGKey for ssh and test for gpg --- services/asymkey/commit.go | 30 ------------ tests/integration/gpg_ssh_git_test.go | 66 --------------------------- 2 files changed, 96 deletions(-) diff --git a/services/asymkey/commit.go b/services/asymkey/commit.go index d648571b70e98..04dbf09d4a320 100644 --- a/services/asymkey/commit.go +++ b/services/asymkey/commit.go @@ -445,36 +445,6 @@ func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committer * } } - // Covers ssh verification for the default SSH signing key specified in the .gitconfig file in Git.HomePath setting - defaultGPGSettings, err := c.GetRepositoryDefaultPublicGPGKey(false) - if defaultGPGSettings.Format == git.KeyTypeSSH { - if err != nil { - log.Error("Error getting default public ssh 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) - } else if fingerprint, err := asymkey_model.CalcFingerprint(defaultGPGSettings.PublicKeyContent); err != nil { - log.Error("Error calculating the fingerprint public key: %s %v", defaultGPGSettings.KeyID, err) - } else if commitVerification := verifySSHCommitVerification(c.Signature.Signature, c.Signature.Payload, &asymkey_model.PublicKey{ - Verified: true, - Content: defaultGPGSettings.PublicKeyContent, - Fingerprint: fingerprint, - HasUsed: true, - }, committer, &user_model.User{ - Name: defaultGPGSettings.Name, - Email: defaultGPGSettings.Email, - }, defaultGPGSettings.Email); commitVerification != nil { - if commitVerification.Reason == asymkey_model.BadSignature { - defaultReason = asymkey_model.BadSignature - } else { - return commitVerification - } - } - } - } - return &asymkey_model.CommitVerification{ CommittingUser: committer, Verified: false, diff --git a/tests/integration/gpg_ssh_git_test.go b/tests/integration/gpg_ssh_git_test.go index 8d964c7b2da6f..a367720795395 100644 --- a/tests/integration/gpg_ssh_git_test.go +++ b/tests/integration/gpg_ssh_git_test.go @@ -49,35 +49,6 @@ func TestGPGGit(t *testing.T) { testGitSigning(t) } -func TestGPGGitDefaults(t *testing.T) { - tmpDir := t.TempDir() // use a temp dir to avoid messing with the user's GPG keyring and git config - err := os.Chmod(tmpDir, 0o700) - assert.NoError(t, err) - - t.Setenv("GNUPGHOME", tmpDir) - - // Need to create a root key - rootKeyPair, err := importTestingKey() - require.NoError(t, err, "importTestingKey") - - err = os.WriteFile(tmpDir+"/.gitconfig", []byte(` -[user] - name = gitea - email = gitea@fake.local - signingKey = `+rootKeyPair.PrimaryKey.KeyIdShortString()+` -[commit] - gpgsign = true -`), 0o600) - require.NoError(t, err, "os.WriteFile .gitconfig") - - defer test.MockVariableValue(&setting.Repository.Signing.SigningKey, "")() - defer test.MockVariableValue(&setting.Repository.Signing.InitialCommit, []string{"never"})() - defer test.MockVariableValue(&setting.Repository.Signing.CRUDActions, []string{"never"})() - defer test.MockVariableValue(&setting.Git.HomePath, tmpDir)() - - testGitSigning(t) -} - func TestSSHGit(t *testing.T) { tmpDir := t.TempDir() // use a temp dir to store the SSH keys err := os.Chmod(tmpDir, 0o700) @@ -105,43 +76,6 @@ func TestSSHGit(t *testing.T) { testGitSigning(t) } -func TestSSHGitDefaults(t *testing.T) { - tmpDir := t.TempDir() // use a temp dir to store the SSH keys and git config - err := os.Chmod(tmpDir, 0o700) - assert.NoError(t, err) - - pub, priv, err := ed25519.GenerateKey(rand.Reader) - require.NoError(t, err, "ed25519.GenerateKey") - sshPubKey, err := ssh.NewPublicKey(pub) - require.NoError(t, err, "ssh.NewPublicKey") - - err = os.WriteFile(tmpDir+"/id_ed25519.pub", ssh.MarshalAuthorizedKey(sshPubKey), 0o600) - require.NoError(t, err, "os.WriteFile id_ed25519.pub") - block, err := ssh.MarshalPrivateKey(priv, "") - require.NoError(t, err, "ssh.MarshalPrivateKey") - err = os.WriteFile(tmpDir+"/id_ed25519", pem.EncodeToMemory(block), 0o600) - require.NoError(t, err, "os.WriteFile id_ed25519") - err = os.WriteFile(tmpDir+"/.gitconfig", []byte(` -[user] - name = gitea - email = gitea@fake.local - signingKey = `+tmpDir+`/id_ed25519.pub -[gpg] - format = ssh -[commit] - gpgsign = true -`), 0o600) - require.NoError(t, err, "os.WriteFile .gitconfig") - - defer test.MockVariableValue(&setting.Repository.Signing.SigningKey, "")() - defer test.MockVariableValue(&setting.Repository.Signing.SigningFormat, "ssh")() - defer test.MockVariableValue(&setting.Repository.Signing.InitialCommit, []string{"never"})() - defer test.MockVariableValue(&setting.Repository.Signing.CRUDActions, []string{"never"})() - defer test.MockVariableValue(&setting.Git.HomePath, tmpDir)() - - testGitSigning(t) -} - func testGitSigning(t *testing.T) { username := "user2" user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: username}) From c771cd698a3c57f7688ca7eaf8751d1cf6723aa8 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Wed, 11 Jun 2025 02:43:09 +0800 Subject: [PATCH 37/45] refactor ParseCommitWithSSHSignature --- models/fixtures/branch.yml | 12 --- models/fixtures/repo_unit.yml | 41 ---------- models/fixtures/repository.yml | 31 -------- models/fixtures/user.yml | 2 +- models/repo/repo_list_test.go | 10 +-- services/asymkey/commit.go | 75 +++++++++--------- services/asymkey/commit_test.go | 44 ++++++++++ .../user2/repo-test-trusted-ssh-keys.git/HEAD | 1 - .../repo-test-trusted-ssh-keys.git/config | 6 -- .../description | 1 - .../info/exclude | 6 -- .../9a/93ffa76e8b72bdb6431910b3a506fa2b39f42e | Bin 55 -> 0 bytes .../b7/fca3aa0d80144f9718b0486681944f9c587e33 | 4 - .../b9/04b3dcaada8c26236f096fe337fd3c4cd8048a | Bin 36 -> 0 bytes .../packed-refs | 1 - .../refs/heads/master | 1 - tests/integration/api_repo_test.go | 6 +- tests/integration/gpg_ssh_git_test.go | 20 ----- tests/integration/oauth_test.go | 4 - 19 files changed, 92 insertions(+), 173 deletions(-) create mode 100644 services/asymkey/commit_test.go delete mode 100644 tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/HEAD delete mode 100644 tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/config delete mode 100644 tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/description delete mode 100644 tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/info/exclude delete mode 100644 tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/objects/9a/93ffa76e8b72bdb6431910b3a506fa2b39f42e delete mode 100644 tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/objects/b7/fca3aa0d80144f9718b0486681944f9c587e33 delete mode 100644 tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/objects/b9/04b3dcaada8c26236f096fe337fd3c4cd8048a delete mode 100644 tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/packed-refs delete mode 100644 tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/refs/heads/master diff --git a/models/fixtures/branch.yml b/models/fixtures/branch.yml index c27ac71bf42d3..6536e1dda7b84 100644 --- a/models/fixtures/branch.yml +++ b/models/fixtures/branch.yml @@ -201,15 +201,3 @@ is_deleted: false deleted_by_id: 0 deleted_unix: 0 - -- - id: 25 - repo_id: 63 - name: 'master' - commit_id: 'b7fca3aa0d80144f9718b0486681944f9c587e33' - commit_message: "Initial commit with signed file" - commit_time: 1602935385 - pusher_id: 2 - is_deleted: false - deleted_by_id: 0 - deleted_unix: 0 diff --git a/models/fixtures/repo_unit.yml b/models/fixtures/repo_unit.yml index cd8c3a973fd96..f6b6252da1f88 100644 --- a/models/fixtures/repo_unit.yml +++ b/models/fixtures/repo_unit.yml @@ -733,44 +733,3 @@ type: 3 config: "{\"IgnoreWhitespaceConflicts\":false,\"AllowMerge\":true,\"AllowRebase\":true,\"AllowRebaseMerge\":true,\"AllowSquash\":true}" created_unix: 946684810 - -- - id: 111 - repo_id: 63 - type: 4 - config: "{}" - created_unix: 946684810 - -- - id: 112 - repo_id: 63 - type: 5 - config: "{}" - created_unix: 946684810 - -- - id: 113 - repo_id: 63 - type: 1 - config: "{}" - created_unix: 946684810 - -- - id: 114 - repo_id: 63 - type: 2 - config: "{}" - created_unix: 946684810 - -- - id: 115 - repo_id: 63 - type: 8 - created_unix: 946684810 - -- - id: 116 - repo_id: 63 - type: 3 - config: "{}" - created_unix: 946684810 diff --git a/models/fixtures/repository.yml b/models/fixtures/repository.yml index 93048e44d0a8d..552a78cbd2773 100644 --- a/models/fixtures/repository.yml +++ b/models/fixtures/repository.yml @@ -1786,34 +1786,3 @@ size: 0 is_fsck_enabled: true close_issues_via_commit_in_any_branch: false - -- - id: 63 - owner_id: 2 - owner_name: user2 - lower_name: repo-test-trusted-ssh-keys - name: repo-test-trusted-ssh-keys - default_branch: master - num_watches: 0 - num_stars: 0 - num_forks: 0 - num_issues: 0 - num_closed_issues: 0 - num_pulls: 0 - num_closed_pulls: 0 - num_milestones: 0 - num_closed_milestones: 0 - num_projects: 0 - num_closed_projects: 0 - is_private: false - is_empty: false - is_archived: false - is_mirror: false - status: 0 - is_fork: false - fork_id: 0 - is_template: false - template_id: 0 - size: 0 - is_fsck_enabled: true - close_issues_via_commit_in_any_branch: false diff --git a/models/fixtures/user.yml b/models/fixtures/user.yml index b3bece5589c9b..976a236011cc9 100644 --- a/models/fixtures/user.yml +++ b/models/fixtures/user.yml @@ -67,7 +67,7 @@ num_followers: 2 num_following: 1 num_stars: 2 - num_repos: 15 + num_repos: 14 num_teams: 0 num_members: 0 visibility: 0 diff --git a/models/repo/repo_list_test.go b/models/repo/repo_list_test.go index bd7fe6da4dc00..7eb76416c2012 100644 --- a/models/repo/repo_list_test.go +++ b/models/repo/repo_list_test.go @@ -138,27 +138,27 @@ func getTestCases() []struct { { name: "AllPublic/PublicRepositoriesOfUserIncludingCollaborative", opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, AllPublic: true, Template: optional.Some(false)}, - count: 35, + count: 34, }, { name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborative", opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true, AllLimited: true, Template: optional.Some(false)}, - count: 40, + count: 39, }, { name: "AllPublic/PublicAndPrivateRepositoriesOfUserIncludingCollaborativeByName", opts: repo_model.SearchRepoOptions{Keyword: "test", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 15, Private: true, AllPublic: true}, - count: 16, + count: 15, }, { name: "AllPublic/PublicAndPrivateRepositoriesOfUser2IncludingCollaborativeByName", opts: repo_model.SearchRepoOptions{Keyword: "test", ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 18, Private: true, AllPublic: true}, - count: 14, + count: 13, }, { name: "AllPublic/PublicRepositoriesOfOrganization", opts: repo_model.SearchRepoOptions{ListOptions: db.ListOptions{Page: 1, PageSize: 10}, OwnerID: 17, AllPublic: true, Collaborate: optional.Some(false), Template: optional.Some(false)}, - count: 35, + count: 34, }, { name: "AllTemplates", diff --git a/services/asymkey/commit.go b/services/asymkey/commit.go index 04dbf09d4a320..ae25d427063a8 100644 --- a/services/asymkey/commit.go +++ b/services/asymkey/commit.go @@ -6,6 +6,7 @@ package asymkey import ( "context" "fmt" + "slices" "strings" asymkey_model "code.gitea.io/gitea/models/asymkey" @@ -359,24 +360,39 @@ func VerifyWithGPGSettings(ctx context.Context, gpgSettings *git.GPGSettings, si return nil } +func verifySSHCommitVerificationByInstanceKey(c *git.Commit, committerUser, signerUser *user_model.User, committerGitEmail, publicKeyContent string) *asymkey_model.CommitVerification { + fingerprint, err := asymkey_model.CalcFingerprint(publicKeyContent) + if err != nil { + log.Error("Error calculating the fingerprint public key %q, err: %v", publicKeyContent, err) + return nil + } + sshPubKey := &asymkey_model.PublicKey{ + Verified: true, + Content: publicKeyContent, + Fingerprint: fingerprint, + HasUsed: true, + } + return verifySSHCommitVerification(c.Signature.Signature, c.Signature.Payload, sshPubKey, committerUser, signerUser, committerGitEmail) +} + // ParseCommitWithSSHSignature check if signature is good against keystore. -func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committer *user_model.User) *asymkey_model.CommitVerification { +func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committerUser *user_model.User) *asymkey_model.CommitVerification { // Now try to associate the signature with the committer, if present - if committer.ID != 0 { + if committerUser.ID != 0 { keys, err := db.Find[asymkey_model.PublicKey](ctx, asymkey_model.FindPublicKeyOptions{ - OwnerID: committer.ID, + OwnerID: committerUser.ID, NotKeytype: asymkey_model.KeyTypePrincipal, }) if err != nil { // Skipping failed to get ssh keys of user log.Error("ListPublicKeys: %v", err) return &asymkey_model.CommitVerification{ - CommittingUser: committer, + CommittingUser: committerUser, Verified: false, Reason: "gpg.error.failed_retrieval_gpg_keys", } } - committerEmailAddresses, err := cache.GetWithContextCache(ctx, cachegroup.UserEmailAddresses, committer.ID, user_model.GetEmailAddresses) + committerEmailAddresses, err := cache.GetWithContextCache(ctx, cachegroup.UserEmailAddresses, committerUser.ID, user_model.GetEmailAddresses) if err != nil { log.Error("GetEmailAddresses: %v", err) } @@ -391,32 +407,26 @@ func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committer * for _, k := range keys { if k.Verified && activated { - commitVerification := verifySSHCommitVerification(c.Signature.Signature, c.Signature.Payload, k, committer, committer, c.Committer.Email) + commitVerification := verifySSHCommitVerification(c.Signature.Signature, c.Signature.Payload, k, committerUser, committerUser, c.Committer.Email) if commitVerification != nil { return commitVerification } } } } - // Trust more than one key for every User + + // Try the pre-set trusted keys (for key-rotation purpose) for _, k := range setting.Repository.Signing.TrustedSSHKeys { - if fingerprint, err := asymkey_model.CalcFingerprint(k); err != nil { - log.Error("Error calculating the fingerprint public key: %s %v", k, err) - } else if commitVerification := verifySSHCommitVerification(c.Signature.Signature, c.Signature.Payload, &asymkey_model.PublicKey{ - Verified: true, - Content: k, - Fingerprint: fingerprint, - HasUsed: true, - }, committer, committer, c.Committer.Email); commitVerification != nil { + // FIXME: why here uses "commiterUser" as "signerUser" but below don't? why here uses "c.Committer.Email" but below uses "gpgSettings.Email"? + signerUser := committerUser + commitVerification := verifySSHCommitVerificationByInstanceKey(c, committerUser, signerUser, c.Committer.Email, k) + if commitVerification != nil && commitVerification.Verified { return commitVerification } } - defaultReason := asymkey_model.NoKeyFound - - // Covers ssh verification for the default SSH signing key specified in gitea config - 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 + // Try the configured instance-wide SSH public key + if setting.Repository.Signing.SigningFormat == git.KeyTypeSSH && !slices.Contains([]string{"", "default", "none"}, setting.Repository.Signing.SigningKey) { gpgSettings := git.GPGSettings{ Sign: true, KeyID: setting.Repository.Signing.SigningKey, @@ -424,31 +434,24 @@ func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committer * 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) - } else if fingerprint, err := asymkey_model.CalcFingerprint(gpgSettings.PublicKeyContent); err != nil { - log.Error("Error calculating the fingerprint public key: %s %v", gpgSettings.KeyID, err) - } else if commitVerification := verifySSHCommitVerification(c.Signature.Signature, c.Signature.Payload, &asymkey_model.PublicKey{ - Verified: true, - Content: gpgSettings.PublicKeyContent, - Fingerprint: fingerprint, - HasUsed: true, - }, committer, &user_model.User{ + signerUser := &user_model.User{ Name: gpgSettings.Name, Email: gpgSettings.Email, - }, gpgSettings.Email); commitVerification != nil { - if commitVerification.Reason == asymkey_model.BadSignature { - defaultReason = asymkey_model.BadSignature - } else { + } + if err := gpgSettings.LoadPublicKeyContent(); err != nil { + log.Error("Error getting instance-wide SSH signing key %q, err: %v", gpgSettings.KeyID, err) + } else { + commitVerification := verifySSHCommitVerificationByInstanceKey(c, committerUser, signerUser, gpgSettings.Email, gpgSettings.PublicKeyContent) + if commitVerification != nil && commitVerification.Verified { return commitVerification } } } return &asymkey_model.CommitVerification{ - CommittingUser: committer, + CommittingUser: committerUser, Verified: false, - Reason: defaultReason, + Reason: asymkey_model.NoKeyFound, } } diff --git a/services/asymkey/commit_test.go b/services/asymkey/commit_test.go new file mode 100644 index 0000000000000..985598ced4906 --- /dev/null +++ b/services/asymkey/commit_test.go @@ -0,0 +1,44 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package asymkey + +import ( + "strings" + "testing" + + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/test" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParseCommitWithSSHSignature(t *testing.T) { + // Here we only test the TrustedSSHKeys. The complete signing test is in tests/integration/gpg_ssh_git_test.go + t.Run("TrustedSSHKey", func(t *testing.T) { + defer test.MockVariableValue(&setting.Repository.Signing.TrustedSSHKeys, []string{"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH6Y4idVaW3E+bLw1uqoAfJD7o5Siu+HqS51E9oQLPE9"})() + + commit, err := git.CommitFromReader(nil, git.Sha1ObjectFormat.EmptyObjectID(), strings.NewReader(`tree 9a93ffa76e8b72bdb6431910b3a506fa2b39f42e +author User Two 1749230009 +0200 +committer User Two 1749230009 +0200 +gpgsig -----BEGIN SSH SIGNATURE----- + U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgfpjiJ1VpbcT5svDW6qgB8kPujl + KK74epLnUT2hAs8T0AAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 + AAAAQDX2t2iHuuLxEWHLJetYXKsgayv3c43r0pJNfAzdLN55Q65pC5M7rG6++gT2bxcpOu + Y6EXbpLqia9sunEF3+LQY= + -----END SSH SIGNATURE----- + +Initial commit with signed file +`)) + require.NoError(t, err) + ret := ParseCommitWithSSHSignature(t.Context(), commit, &user_model.User{Name: "foo", Email: "foo@bar.com"}) + require.NotNil(t, ret) + assert.True(t, ret.Verified) + assert.False(t, ret.Warning) + assert.NotNil(t, ret.CommittingUser) // FIXME: test the CommittingUser and SigningUser correctly + assert.NotNil(t, ret.SigningUser) // FIXME: test the CommittingUser and SigningUser correctly + }) +} diff --git a/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/HEAD b/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/HEAD deleted file mode 100644 index cb089cd89a7d7..0000000000000 --- a/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/HEAD +++ /dev/null @@ -1 +0,0 @@ -ref: refs/heads/master diff --git a/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/config b/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/config deleted file mode 100644 index e6da231579bcc..0000000000000 --- a/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/config +++ /dev/null @@ -1,6 +0,0 @@ -[core] - repositoryformatversion = 0 - filemode = true - bare = true - ignorecase = true - precomposeunicode = true diff --git a/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/description b/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/description deleted file mode 100644 index 498b267a8c781..0000000000000 --- a/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/description +++ /dev/null @@ -1 +0,0 @@ -Unnamed repository; edit this file 'description' to name the repository. diff --git a/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/info/exclude b/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/info/exclude deleted file mode 100644 index a5196d1be8fb5..0000000000000 --- a/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/info/exclude +++ /dev/null @@ -1,6 +0,0 @@ -# git ls-files --others --exclude-from=.git/info/exclude -# Lines that start with '#' are comments. -# For a project mostly in C, the following would be a good set of -# exclude patterns (uncomment them if you want to use them): -# *.[oa] -# *~ diff --git a/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/objects/9a/93ffa76e8b72bdb6431910b3a506fa2b39f42e b/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/objects/9a/93ffa76e8b72bdb6431910b3a506fa2b39f42e deleted file mode 100644 index bcf0704cd0cbba115cb93c01e583249df6ed4f46..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 55 zcmV-70LcG%0V^p=O;s?qU@$Z=Ff%bxD9%jJOHI)$sVHIC$+G#*s#`s3%K4o6kInzu N_}pOW0sv$I5hQpJ8C(DW diff --git a/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/objects/b7/fca3aa0d80144f9718b0486681944f9c587e33 b/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/objects/b7/fca3aa0d80144f9718b0486681944f9c587e33 deleted file mode 100644 index 6d64c797ca642..0000000000000 --- a/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/objects/b7/fca3aa0d80144f9718b0486681944f9c587e33 +++ /dev/null @@ -1,4 +0,0 @@ -x��[o�0��ٿb�QW�� ��� $�@��˛�`�ͥ���&�k_z���|ҙ�NZ�Ϫ�55��� -�f�pl9L��ۤ�k�� - ۙ u3�H$��PVײ�VŸ�s䟼��.��<��1]B1�. 0�u���t���r��*��^�`2X��� -'���S��� c\L�^�n�E�2Z?���X�S��L��x�Iʭ��_�/�hxzl����1��.1'�r�'�)>M�x��&&Aw-����dL�M��b�ぅ��KC��m]��4��f��׹x��I+�g,�n��Y�Ҷ��Z8�� rN�k����v�It���[�������}�0��2PxQ�|Uo�9@��E�!S�D?J�� \ No newline at end of file diff --git a/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/objects/b9/04b3dcaada8c26236f096fe337fd3c4cd8048a b/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/objects/b9/04b3dcaada8c26236f096fe337fd3c4cd8048a deleted file mode 100644 index ee5dcf6ba398b5e76d1df5e90ee6d0ba63ddfcbe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 36 ucmV+<0Nej~0ZYosPf{>4VhG8|ELH%bM1|ta^t{v*g|y6^R6PKbBMHGIZxA~G diff --git a/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/packed-refs b/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/packed-refs deleted file mode 100644 index 250f187384963..0000000000000 --- a/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/packed-refs +++ /dev/null @@ -1 +0,0 @@ -# pack-refs with: peeled fully-peeled sorted diff --git a/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/refs/heads/master b/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/refs/heads/master deleted file mode 100644 index 8b4ec6bcd860c..0000000000000 --- a/tests/gitea-repositories-meta/user2/repo-test-trusted-ssh-keys.git/refs/heads/master +++ /dev/null @@ -1 +0,0 @@ -b7fca3aa0d80144f9718b0486681944f9c587e33 diff --git a/tests/integration/api_repo_test.go b/tests/integration/api_repo_test.go index df8de083ca087..672c2a2c8bf9b 100644 --- a/tests/integration/api_repo_test.go +++ b/tests/integration/api_repo_test.go @@ -94,9 +94,9 @@ func TestAPISearchRepo(t *testing.T) { }{ { name: "RepositoriesMax50", requestURL: "/api/v1/repos/search?limit=50&private=false", expectedResults: expectedResults{ - nil: {count: 37}, - user: {count: 37}, - user2: {count: 37}, + nil: {count: 36}, + user: {count: 36}, + user2: {count: 36}, }, }, { diff --git a/tests/integration/gpg_ssh_git_test.go b/tests/integration/gpg_ssh_git_test.go index a367720795395..56f9f87783355 100644 --- a/tests/integration/gpg_ssh_git_test.go +++ b/tests/integration/gpg_ssh_git_test.go @@ -300,23 +300,3 @@ func importTestingKey() (*openpgp.Entity, error) { // There should only be one entity in this file. return keyring[0], nil } - -func TestTrustedSSHKeys(t *testing.T) { - defer tests.PrepareTestEnv(t)() - defer test.MockVariableValue(&setting.Repository.Signing.TrustedSSHKeys, []string{"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH6Y4idVaW3E+bLw1uqoAfJD7o5Siu+HqS51E9oQLPE9"})() - - username := "user2" - testCtx := NewAPITestContext(t, username, "repo-test-trusted-ssh-keys", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) - t.Run("CheckMasterBranchSignedVerified", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) { - require.NotNil(t, branch.Commit, "no commit provided with branch! %v", branch) - require.NotNil(t, branch.Commit.Verification, "no verification provided with branch commit! %v", branch.Commit) - require.True(t, branch.Commit.Verification.Verified) - })) - - setting.Repository.Signing.TrustedSSHKeys = []string{} - t.Run("CheckMasterBranchSignedUnverified", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) { - require.NotNil(t, branch.Commit, "no commit provided with branch! %v", branch) - require.NotNil(t, branch.Commit.Verification, "no verification provided with branch commit! %v", branch.Commit) - require.False(t, branch.Commit.Verification.Verified) - })) -} diff --git a/tests/integration/oauth_test.go b/tests/integration/oauth_test.go index 50c15021ee938..d2228bae7992a 100644 --- a/tests/integration/oauth_test.go +++ b/tests/integration/oauth_test.go @@ -691,10 +691,6 @@ func TestOAuth_GrantScopesReadRepositoryFailOrganization(t *testing.T) { FullRepoName: "user2/commitsonpr", Private: false, }, - { - FullRepoName: "user2/repo-test-trusted-ssh-keys", - Private: false, - }, } assert.Equal(t, reposExpected, reposCaptured) From 745a690b3f1bf53f3e40fe59421dfa407941ec74 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Wed, 11 Jun 2025 10:38:47 +0200 Subject: [PATCH 38/45] resolve fixme --- custom/conf/app.example.ini | 1 + services/asymkey/commit.go | 6 ++++-- services/asymkey/commit_test.go | 15 ++++++++++++--- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 3e9a9e2451621..aa2fcee765507 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1233,6 +1233,7 @@ LEVEL = Info ;; ;; Determines which additional ssh keys are trusted for all signed commits regardless of the user ;; This is useful for ssh signing key rotation. +;; Exposes the provided SIGNING_NAME and SIGNING_EMAIL as the signer, regardless of the SIGNING_FORMAT value. ;; Multiple keys should be comma separated. ;; E.g."ssh- ". or "ssh- , ssh- ". ;TRUSTED_SSH_KEYS = diff --git a/services/asymkey/commit.go b/services/asymkey/commit.go index ae25d427063a8..10bb9c8a59474 100644 --- a/services/asymkey/commit.go +++ b/services/asymkey/commit.go @@ -417,8 +417,10 @@ func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committerUs // Try the pre-set trusted keys (for key-rotation purpose) for _, k := range setting.Repository.Signing.TrustedSSHKeys { - // FIXME: why here uses "commiterUser" as "signerUser" but below don't? why here uses "c.Committer.Email" but below uses "gpgSettings.Email"? - signerUser := committerUser + signerUser := &user_model.User{ + Name: setting.Repository.Signing.SigningName, + Email: setting.Repository.Signing.SigningEmail, + } commitVerification := verifySSHCommitVerificationByInstanceKey(c, committerUser, signerUser, c.Committer.Email, k) if commitVerification != nil && commitVerification.Verified { return commitVerification diff --git a/services/asymkey/commit_test.go b/services/asymkey/commit_test.go index 985598ced4906..5ad1d014feabd 100644 --- a/services/asymkey/commit_test.go +++ b/services/asymkey/commit_test.go @@ -19,6 +19,8 @@ import ( func TestParseCommitWithSSHSignature(t *testing.T) { // Here we only test the TrustedSSHKeys. The complete signing test is in tests/integration/gpg_ssh_git_test.go t.Run("TrustedSSHKey", func(t *testing.T) { + defer test.MockVariableValue(&setting.Repository.Signing.SigningName, "gitea")() + defer test.MockVariableValue(&setting.Repository.Signing.SigningEmail, "gitea@fake.local")() defer test.MockVariableValue(&setting.Repository.Signing.TrustedSSHKeys, []string{"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH6Y4idVaW3E+bLw1uqoAfJD7o5Siu+HqS51E9oQLPE9"})() commit, err := git.CommitFromReader(nil, git.Sha1ObjectFormat.EmptyObjectID(), strings.NewReader(`tree 9a93ffa76e8b72bdb6431910b3a506fa2b39f42e @@ -34,11 +36,18 @@ gpgsig -----BEGIN SSH SIGNATURE----- Initial commit with signed file `)) require.NoError(t, err) - ret := ParseCommitWithSSHSignature(t.Context(), commit, &user_model.User{Name: "foo", Email: "foo@bar.com"}) + committingUser := &user_model.User{ + ID: 2, + Name: "User Two", + Email: "user2@example.com", + } + ret := ParseCommitWithSSHSignature(t.Context(), commit, committingUser) require.NotNil(t, ret) assert.True(t, ret.Verified) assert.False(t, ret.Warning) - assert.NotNil(t, ret.CommittingUser) // FIXME: test the CommittingUser and SigningUser correctly - assert.NotNil(t, ret.SigningUser) // FIXME: test the CommittingUser and SigningUser correctly + assert.Equal(t, ret.CommittingUser, committingUser) + assert.NotNil(t, ret.SigningUser) + assert.Equal(t, ret.SigningUser.Name, "gitea") + assert.Equal(t, ret.SigningUser.Email, "gitea@fake.local") }) } From f3710a34e27f2f4f06902a9c67c9e34ab9ea5dd1 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Wed, 11 Jun 2025 10:54:55 +0200 Subject: [PATCH 39/45] fix test api usage --- services/asymkey/commit_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/services/asymkey/commit_test.go b/services/asymkey/commit_test.go index 5ad1d014feabd..56358ca4cd975 100644 --- a/services/asymkey/commit_test.go +++ b/services/asymkey/commit_test.go @@ -45,9 +45,9 @@ Initial commit with signed file require.NotNil(t, ret) assert.True(t, ret.Verified) assert.False(t, ret.Warning) - assert.Equal(t, ret.CommittingUser, committingUser) + assert.Equal(t, committingUser, ret.CommittingUser) assert.NotNil(t, ret.SigningUser) - assert.Equal(t, ret.SigningUser.Name, "gitea") - assert.Equal(t, ret.SigningUser.Email, "gitea@fake.local") + assert.Equal(t, "gitea", ret.SigningUser.Name) + assert.Equal(t, "gitea@fake.local", ret.SigningUser.Email) }) } From 6b67a5440a18a8960292430465ea68c07d7be94d Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Wed, 11 Jun 2025 10:56:29 +0200 Subject: [PATCH 40/45] do not crash the test if signinguser is nil --- services/asymkey/commit_test.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/services/asymkey/commit_test.go b/services/asymkey/commit_test.go index 56358ca4cd975..0438209a6192a 100644 --- a/services/asymkey/commit_test.go +++ b/services/asymkey/commit_test.go @@ -46,8 +46,9 @@ Initial commit with signed file assert.True(t, ret.Verified) assert.False(t, ret.Warning) assert.Equal(t, committingUser, ret.CommittingUser) - assert.NotNil(t, ret.SigningUser) - assert.Equal(t, "gitea", ret.SigningUser.Name) - assert.Equal(t, "gitea@fake.local", ret.SigningUser.Email) + if assert.NotNil(t, ret.SigningUser) { + assert.Equal(t, "gitea", ret.SigningUser.Name) + assert.Equal(t, "gitea@fake.local", ret.SigningUser.Email) + } }) } From 6d119a0fea404fea95d47ad1d5faa034cbe45caf Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Wed, 11 Jun 2025 17:48:10 +0800 Subject: [PATCH 41/45] merge duplicate code and improve err handling --- routers/api/v1/api.go | 4 +-- routers/api/v1/misc/signing.go | 62 +++++++++++++++------------------- services/asymkey/sign.go | 2 +- 3 files changed, 30 insertions(+), 38 deletions(-) diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 1fbca181d24c8..2c138ad61a091 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -971,7 +971,7 @@ func Routes() *web.Router { // Misc (public accessible) m.Group("", func() { m.Get("/version", misc.Version) - m.Get("/signing-key.gpg", misc.SigningKey) + m.Get("/signing-key.gpg", misc.SigningKeyGPG) 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) @@ -1428,7 +1428,7 @@ func Routes() *web.Router { m.Combo("/file-contents", reqRepoReader(unit.TypeCode), context.ReferencesGitRepo()). 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.gpg", misc.SigningKeyGPG) m.Get("/signing-key.pub", misc.SigningKeySSH) m.Group("/topics", func() { m.Combo("").Get(repo.ListTopics). diff --git a/routers/api/v1/misc/signing.go b/routers/api/v1/misc/signing.go index e2779fc966a64..749d59a822a01 100644 --- a/routers/api/v1/misc/signing.go +++ b/routers/api/v1/misc/signing.go @@ -9,8 +9,30 @@ import ( "code.gitea.io/gitea/services/context" ) -// SigningKey returns the public key of the default signing key if it exists -func SigningKey(ctx *context.APIContext) { +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) + if err != nil { + ctx.APIErrorInternal(err) + return + } + if format == "" { + ctx.APIErrorNotFound("no signing key") + return + } else if format != expectedFormat { + ctx.APIErrorNotFound("signing key format is " + format) + return + } + _, _ = ctx.Write([]byte(content)) +} + +// SigningKeyGPG returns the public key of the default signing key if it exists +func SigningKeyGPG(ctx *context.APIContext) { // swagger:operation GET /signing-key.gpg miscellaneous getSigningKey // --- // summary: Get default signing-key.gpg @@ -43,25 +65,10 @@ func SigningKey(ctx *context.APIContext) { // description: "GPG armored 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 != git.KeyTypeOpenPGP { - ctx.APIErrorNotFound("SSH keys are used for signing, not GPG") - return - } - _, _ = ctx.Write([]byte(content)) + getSigningKey(ctx, git.KeyTypeOpenPGP) } -// SigningKey returns the public key of the default signing key if it exists +// SigningKeySSH 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 // --- @@ -95,20 +102,5 @@ func SigningKeySSH(ctx *context.APIContext) { // 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 != git.KeyTypeSSH { - ctx.APIErrorNotFound("GPG keys are used for signing, not SSH") - return - } - _, _ = ctx.Write([]byte(content)) + getSigningKey(ctx, git.KeyTypeSSH) } diff --git a/services/asymkey/sign.go b/services/asymkey/sign.go index 93e5c691a180a..a9cb88dc3aa50 100644 --- a/services/asymkey/sign.go +++ b/services/asymkey/sign.go @@ -131,7 +131,7 @@ func SigningKey(ctx context.Context, repoPath string) (*git.SigningKey, *git.Sig } // PublicSigningKey gets the public signing key within a provided repository directory -func PublicSigningKey(ctx context.Context, repoPath string) (string, string, error) { +func PublicSigningKey(ctx context.Context, repoPath string) (content string, format string, err error) { signingKey, _ := SigningKey(ctx, repoPath) if signingKey == nil { return "", "", nil From 7655f5d6200bc45ff20d6dbf34cb7022789b08d6 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Wed, 11 Jun 2025 17:50:52 +0800 Subject: [PATCH 42/45] rename consts to match SigningKey.Format --- modules/git/key.go | 6 ++---- modules/git/repo_gpg.go | 4 ++-- routers/api/v1/misc/signing.go | 4 ++-- services/asymkey/commit.go | 2 +- services/asymkey/sign.go | 4 ++-- 5 files changed, 9 insertions(+), 11 deletions(-) diff --git a/modules/git/key.go b/modules/git/key.go index a6ea543ef1c5b..2513c048b7cbd 100644 --- a/modules/git/key.go +++ b/modules/git/key.go @@ -5,10 +5,8 @@ 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, expected default of git cli - KeyTypeOpenPGP = "openpgp" - // KeyTypeSSH is the key type for SSH keys - KeyTypeSSH = "ssh" + SigningKeyFormatOpenPGP = "openpgp" // for GPG keys, the expected default of git cli + SigningKeyFormatSSH = "ssh" ) type SigningKey struct { diff --git a/modules/git/repo_gpg.go b/modules/git/repo_gpg.go index 6eff247f66480..0021a7bda7bf1 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 == KeyTypeSSH { + 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) @@ -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", "--default", KeyTypeOpenPGP, "--get", "gpg.format").RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path}) + format, _, _ := NewCommand("config", "--default", SigningKeyFormatOpenPGP, "--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/routers/api/v1/misc/signing.go b/routers/api/v1/misc/signing.go index 749d59a822a01..db70e04b8f627 100644 --- a/routers/api/v1/misc/signing.go +++ b/routers/api/v1/misc/signing.go @@ -65,7 +65,7 @@ func SigningKeyGPG(ctx *context.APIContext) { // description: "GPG armored public key" // schema: // type: string - getSigningKey(ctx, git.KeyTypeOpenPGP) + getSigningKey(ctx, git.SigningKeyFormatOpenPGP) } // SigningKeySSH returns the public key of the default signing key if it exists @@ -102,5 +102,5 @@ func SigningKeySSH(ctx *context.APIContext) { // description: "ssh public key" // schema: // type: string - getSigningKey(ctx, git.KeyTypeSSH) + getSigningKey(ctx, git.SigningKeyFormatSSH) } diff --git a/services/asymkey/commit.go b/services/asymkey/commit.go index 10bb9c8a59474..94ea8b50a3003 100644 --- a/services/asymkey/commit.go +++ b/services/asymkey/commit.go @@ -428,7 +428,7 @@ func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committerUs } // Try the configured instance-wide SSH public key - if setting.Repository.Signing.SigningFormat == git.KeyTypeSSH && !slices.Contains([]string{"", "default", "none"}, setting.Repository.Signing.SigningKey) { + if setting.Repository.Signing.SigningFormat == git.SigningKeyFormatSSH && !slices.Contains([]string{"", "default", "none"}, setting.Repository.Signing.SigningKey) { gpgSettings := git.GPGSettings{ Sign: true, KeyID: setting.Repository.Signing.SigningKey, diff --git a/services/asymkey/sign.go b/services/asymkey/sign.go index a9cb88dc3aa50..d80ca76abbe9f 100644 --- a/services/asymkey/sign.go +++ b/services/asymkey/sign.go @@ -99,7 +99,7 @@ func SigningKey(ctx context.Context, repoPath string) (*git.SigningKey, *git.Sig return nil, nil } - format, _, _ := git.NewCommand("config", "--default", git.KeyTypeOpenPGP, "--get", "gpg.format").RunStdString(ctx, &git.RunOpts{Dir: repoPath}) + format, _, _ := git.NewCommand("config", "--default", git.SigningKeyFormatOpenPGP, "--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}) @@ -136,7 +136,7 @@ func PublicSigningKey(ctx context.Context, repoPath string) (content string, for if signingKey == nil { return "", "", nil } - if signingKey.Format == git.KeyTypeSSH { + 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) From b3997e3e689982cb56e0b1dacbd0ef6e89b62321 Mon Sep 17 00:00:00 2001 From: wxiaoguang Date: Wed, 11 Jun 2025 17:55:48 +0800 Subject: [PATCH 43/45] add some comments --- services/asymkey/commit.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/services/asymkey/commit.go b/services/asymkey/commit.go index 94ea8b50a3003..148f51fd10e18 100644 --- a/services/asymkey/commit.go +++ b/services/asymkey/commit.go @@ -416,6 +416,8 @@ func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committerUs } // Try the pre-set trusted keys (for key-rotation purpose) + // At the moment, we still use the SigningName&SigningEmail for the rotated keys. + // Maybe in the future we can extend the key format to "ssh-xxx .... old-user@example.com" to support different signer emails. for _, k := range setting.Repository.Signing.TrustedSSHKeys { signerUser := &user_model.User{ Name: setting.Repository.Signing.SigningName, From 1f4ff813bc607f4e452e7c279923f8f2a7f3307d Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Wed, 11 Jun 2025 11:59:29 +0200 Subject: [PATCH 44/45] fix lint --- services/asymkey/sign.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/asymkey/sign.go b/services/asymkey/sign.go index d80ca76abbe9f..f94462ea4630f 100644 --- a/services/asymkey/sign.go +++ b/services/asymkey/sign.go @@ -131,7 +131,7 @@ func SigningKey(ctx context.Context, repoPath string) (*git.SigningKey, *git.Sig } // PublicSigningKey gets the public signing key within a provided repository directory -func PublicSigningKey(ctx context.Context, repoPath string) (content string, format string, err error) { +func PublicSigningKey(ctx context.Context, repoPath string) (content, format string, err error) { signingKey, _ := SigningKey(ctx, repoPath) if signingKey == nil { return "", "", nil From 70ae40f58fbe29f3c19bf9567e46f047eef14f38 Mon Sep 17 00:00:00 2001 From: Christopher Homberger Date: Wed, 11 Jun 2025 11:59:50 +0200 Subject: [PATCH 45/45] Update default setting comment --- modules/setting/repository.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/setting/repository.go b/modules/setting/repository.go index bf0ae923d4459..318cf4110855c 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -255,7 +255,7 @@ var ( SigningKey: "default", SigningName: "", SigningEmail: "", - SigningFormat: "openpgp", // git.KeyTypeOpenPGP + SigningFormat: "openpgp", // git.SigningKeyFormatOpenPGP InitialCommit: []string{"always"}, CRUDActions: []string{"pubkey", "twofa", "parentsigned"}, Merges: []string{"pubkey", "twofa", "basesigned", "commitssigned"},