mirror of https://github.com/go-gitea/gitea.git
Improve instance wide ssh commit signing (#34341)
* Signed SSH commits can look in the UI like on GitHub, just like gpg keys today in Gitea * SSH format can be added in gitea config * SSH Signing worked before with DEFAULT_TRUST_MODEL=committer `TRUSTED_SSH_KEYS` can be a list of additional ssh public key contents to trust for every user of this instance Closes #34329 Related #31392 --------- Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: techknowlogick <techknowlogick@gitea.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>pull/34689/head^2
parent
fbc3796f9e
commit
c9505a26b9
|
@ -1186,17 +1186,24 @@ 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:
|
||||||
|
;; - 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
|
;; run in the context of the RUN_USER
|
||||||
;; Switch to none to stop signing completely
|
;; Switch to none to stop signing completely
|
||||||
;SIGNING_KEY = default
|
;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
|
;; 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.)
|
;; by setting the SIGNING_KEY ID to the correct ID.)
|
||||||
;SIGNING_NAME =
|
;SIGNING_NAME =
|
||||||
;SIGNING_EMAIL =
|
;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
|
;; Sets the default trust model for repositories. Options are: collaborator, committer, collaboratorcommitter
|
||||||
;DEFAULT_TRUST_MODEL = collaborator
|
;DEFAULT_TRUST_MODEL = collaborator
|
||||||
|
@ -1223,6 +1230,13 @@ LEVEL = Info
|
||||||
;; - commitssigned: require that all the commits in the head branch are signed.
|
;; - commitssigned: require that all the commits in the head branch are signed.
|
||||||
;; - approved: only sign when merging an approved pr to a protected branch
|
;; - approved: only sign when merging an approved pr to a protected branch
|
||||||
;MERGES = pubkey, twofa, basesigned, commitssigned
|
;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.
|
||||||
|
;; 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-<algorithm> <key>". or "ssh-<algorithm> <key1>, ssh-<algorithm> <key2>".
|
||||||
|
;TRUSTED_SSH_KEYS =
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
|
@ -47,6 +47,7 @@ type Command struct {
|
||||||
globalArgsLength int
|
globalArgsLength int
|
||||||
brokenArgs []string
|
brokenArgs []string
|
||||||
cmd *exec.Cmd // for debug purpose only
|
cmd *exec.Cmd // for debug purpose only
|
||||||
|
configArgs []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func logArgSanitize(arg string) string {
|
func logArgSanitize(arg string) string {
|
||||||
|
@ -196,6 +197,16 @@ func (c *Command) AddDashesAndList(list ...string) *Command {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
// ToTrustedCmdArgs converts a list of strings (trusted as argument) to TrustedCmdArgs
|
// ToTrustedCmdArgs converts a list of strings (trusted as argument) to TrustedCmdArgs
|
||||||
// In most cases, it shouldn't be used. Use NewCommand().AddXxx() function instead
|
// In most cases, it shouldn't be used. Use NewCommand().AddXxx() function instead
|
||||||
func ToTrustedCmdArgs(args []string) TrustedCmdArgs {
|
func ToTrustedCmdArgs(args []string) TrustedCmdArgs {
|
||||||
|
@ -321,7 +332,7 @@ func (c *Command) run(ctx context.Context, skip int, opts *RunOpts) error {
|
||||||
|
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, c.prog, c.args...)
|
cmd := exec.CommandContext(ctx, c.prog, append(c.configArgs, c.args...)...)
|
||||||
c.cmd = cmd // for debug purpose only
|
c.cmd = cmd // for debug purpose only
|
||||||
if opts.Env == nil {
|
if opts.Env == nil {
|
||||||
cmd.Env = os.Environ()
|
cmd.Env = os.Environ()
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package git
|
||||||
|
|
||||||
|
// Based on https://git-scm.com/docs/git-config#Documentation/git-config.txt-gpgformat
|
||||||
|
const (
|
||||||
|
SigningKeyFormatOpenPGP = "openpgp" // for GPG keys, the expected default of git cli
|
||||||
|
SigningKeyFormatSSH = "ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SigningKey struct {
|
||||||
|
KeyID string
|
||||||
|
Format string
|
||||||
|
}
|
|
@ -28,6 +28,7 @@ type GPGSettings struct {
|
||||||
Email string
|
Email string
|
||||||
Name string
|
Name string
|
||||||
PublicKeyContent string
|
PublicKeyContent string
|
||||||
|
Format string
|
||||||
}
|
}
|
||||||
|
|
||||||
const prettyLogFormat = `--pretty=format:%H`
|
const prettyLogFormat = `--pretty=format:%H`
|
||||||
|
|
|
@ -6,6 +6,7 @@ package git
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/process"
|
"code.gitea.io/gitea/modules/process"
|
||||||
|
@ -13,6 +14,14 @@ import (
|
||||||
|
|
||||||
// LoadPublicKeyContent will load the key from gpg
|
// LoadPublicKeyContent will load the key from gpg
|
||||||
func (gpgSettings *GPGSettings) LoadPublicKeyContent() error {
|
func (gpgSettings *GPGSettings) LoadPublicKeyContent() error {
|
||||||
|
if gpgSettings.Format == SigningKeyFormatSSH {
|
||||||
|
content, err := os.ReadFile(gpgSettings.KeyID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to read SSH public key file: %s, %w", gpgSettings.KeyID, err)
|
||||||
|
}
|
||||||
|
gpgSettings.PublicKeyContent = string(content)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
content, stderr, err := process.GetManager().Exec(
|
content, stderr, err := process.GetManager().Exec(
|
||||||
"gpg -a --export",
|
"gpg -a --export",
|
||||||
"gpg", "-a", "--export", gpgSettings.KeyID)
|
"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})
|
signingKey, _, _ := NewCommand("config", "--get", "user.signingkey").RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path})
|
||||||
gpgSettings.KeyID = strings.TrimSpace(signingKey)
|
gpgSettings.KeyID = strings.TrimSpace(signingKey)
|
||||||
|
|
||||||
|
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})
|
defaultEmail, _, _ := NewCommand("config", "--get", "user.email").RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path})
|
||||||
gpgSettings.Email = strings.TrimSpace(defaultEmail)
|
gpgSettings.Email = strings.TrimSpace(defaultEmail)
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ import (
|
||||||
type CommitTreeOpts struct {
|
type CommitTreeOpts struct {
|
||||||
Parents []string
|
Parents []string
|
||||||
Message string
|
Message string
|
||||||
KeyID string
|
Key *SigningKey
|
||||||
NoGPGSign bool
|
NoGPGSign bool
|
||||||
AlwaysSign bool
|
AlwaysSign bool
|
||||||
}
|
}
|
||||||
|
@ -43,8 +43,13 @@ func (repo *Repository) CommitTree(author, committer *Signature, tree *Tree, opt
|
||||||
_, _ = messageBytes.WriteString(opts.Message)
|
_, _ = messageBytes.WriteString(opts.Message)
|
||||||
_, _ = messageBytes.WriteString("\n")
|
_, _ = messageBytes.WriteString("\n")
|
||||||
|
|
||||||
if opts.KeyID != "" || opts.AlwaysSign {
|
if opts.Key != nil {
|
||||||
cmd.AddOptionFormat("-S%s", opts.KeyID)
|
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 {
|
if opts.NoGPGSign {
|
||||||
|
|
|
@ -100,11 +100,13 @@ var (
|
||||||
SigningKey string
|
SigningKey string
|
||||||
SigningName string
|
SigningName string
|
||||||
SigningEmail string
|
SigningEmail string
|
||||||
|
SigningFormat string
|
||||||
InitialCommit []string
|
InitialCommit []string
|
||||||
CRUDActions []string `ini:"CRUD_ACTIONS"`
|
CRUDActions []string `ini:"CRUD_ACTIONS"`
|
||||||
Merges []string
|
Merges []string
|
||||||
Wiki []string
|
Wiki []string
|
||||||
DefaultTrustModel string
|
DefaultTrustModel string
|
||||||
|
TrustedSSHKeys []string `ini:"TRUSTED_SSH_KEYS"`
|
||||||
} `ini:"repository.signing"`
|
} `ini:"repository.signing"`
|
||||||
}{
|
}{
|
||||||
DetectedCharsetsOrder: []string{
|
DetectedCharsetsOrder: []string{
|
||||||
|
@ -242,20 +244,24 @@ var (
|
||||||
SigningKey string
|
SigningKey string
|
||||||
SigningName string
|
SigningName string
|
||||||
SigningEmail string
|
SigningEmail string
|
||||||
|
SigningFormat string
|
||||||
InitialCommit []string
|
InitialCommit []string
|
||||||
CRUDActions []string `ini:"CRUD_ACTIONS"`
|
CRUDActions []string `ini:"CRUD_ACTIONS"`
|
||||||
Merges []string
|
Merges []string
|
||||||
Wiki []string
|
Wiki []string
|
||||||
DefaultTrustModel string
|
DefaultTrustModel string
|
||||||
|
TrustedSSHKeys []string `ini:"TRUSTED_SSH_KEYS"`
|
||||||
}{
|
}{
|
||||||
SigningKey: "default",
|
SigningKey: "default",
|
||||||
SigningName: "",
|
SigningName: "",
|
||||||
SigningEmail: "",
|
SigningEmail: "",
|
||||||
|
SigningFormat: "openpgp", // git.SigningKeyFormatOpenPGP
|
||||||
InitialCommit: []string{"always"},
|
InitialCommit: []string{"always"},
|
||||||
CRUDActions: []string{"pubkey", "twofa", "parentsigned"},
|
CRUDActions: []string{"pubkey", "twofa", "parentsigned"},
|
||||||
Merges: []string{"pubkey", "twofa", "basesigned", "commitssigned"},
|
Merges: []string{"pubkey", "twofa", "basesigned", "commitssigned"},
|
||||||
Wiki: []string{"never"},
|
Wiki: []string{"never"},
|
||||||
DefaultTrustModel: "collaborator",
|
DefaultTrustModel: "collaborator",
|
||||||
|
TrustedSSHKeys: []string{},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
RepoRootPath string
|
RepoRootPath string
|
||||||
|
|
|
@ -971,7 +971,8 @@ func Routes() *web.Router {
|
||||||
// Misc (public accessible)
|
// Misc (public accessible)
|
||||||
m.Group("", func() {
|
m.Group("", func() {
|
||||||
m.Get("/version", misc.Version)
|
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("/markup", reqToken(), bind(api.MarkupOption{}), misc.Markup)
|
||||||
m.Post("/markdown", reqToken(), bind(api.MarkdownOption{}), misc.Markdown)
|
m.Post("/markdown", reqToken(), bind(api.MarkdownOption{}), misc.Markdown)
|
||||||
m.Post("/markdown/raw", reqToken(), misc.MarkdownRaw)
|
m.Post("/markdown/raw", reqToken(), misc.MarkdownRaw)
|
||||||
|
@ -1427,7 +1428,8 @@ func Routes() *web.Router {
|
||||||
m.Combo("/file-contents", reqRepoReader(unit.TypeCode), context.ReferencesGitRepo()).
|
m.Combo("/file-contents", reqRepoReader(unit.TypeCode), context.ReferencesGitRepo()).
|
||||||
Get(repo.GetFileContentsGet).
|
Get(repo.GetFileContentsGet).
|
||||||
Post(bind(api.GetFilesOptions{}), repo.GetFileContentsPost) // POST method requires "write" permission, so we also support "GET" method above
|
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.Group("/topics", func() {
|
||||||
m.Combo("").Get(repo.ListTopics).
|
m.Combo("").Get(repo.ListTopics).
|
||||||
Put(reqToken(), reqAdmin(), bind(api.RepoTopicOptions{}), repo.UpdateTopics)
|
Put(reqToken(), reqAdmin(), bind(api.RepoTopicOptions{}), repo.UpdateTopics)
|
||||||
|
|
|
@ -4,14 +4,35 @@
|
||||||
package misc
|
package misc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"code.gitea.io/gitea/modules/git"
|
||||||
|
|
||||||
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
||||||
"code.gitea.io/gitea/services/context"
|
"code.gitea.io/gitea/services/context"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SigningKey returns the public key of the default signing key if it exists
|
func getSigningKey(ctx *context.APIContext, expectedFormat string) {
|
||||||
func SigningKey(ctx *context.APIContext) {
|
// 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
|
// swagger:operation GET /signing-key.gpg miscellaneous getSigningKey
|
||||||
// ---
|
// ---
|
||||||
// summary: Get default signing-key.gpg
|
// summary: Get default signing-key.gpg
|
||||||
|
@ -44,19 +65,42 @@ func SigningKey(ctx *context.APIContext) {
|
||||||
// description: "GPG armored public key"
|
// description: "GPG armored public key"
|
||||||
// schema:
|
// schema:
|
||||||
// type: string
|
// type: string
|
||||||
|
getSigningKey(ctx, git.SigningKeyFormatOpenPGP)
|
||||||
path := ""
|
}
|
||||||
if ctx.Repo != nil && ctx.Repo.Repository != nil {
|
|
||||||
path = ctx.Repo.Repository.RepoPath()
|
// 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
|
||||||
content, err := asymkey_service.PublicSigningKey(ctx, path)
|
// ---
|
||||||
if err != nil {
|
// summary: Get default signing-key.pub
|
||||||
ctx.APIErrorInternal(err)
|
// produces:
|
||||||
return
|
// - text/plain
|
||||||
}
|
// responses:
|
||||||
_, err = ctx.Write([]byte(content))
|
// "200":
|
||||||
if err != nil {
|
// description: "ssh public key"
|
||||||
ctx.APIErrorInternal(fmt.Errorf("Error writing key content %w", err))
|
// 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
|
||||||
|
getSigningKey(ctx, git.SigningKeyFormatSSH)
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,7 +62,7 @@ func SettingsCtxData(ctx *context.Context) {
|
||||||
ctx.Data["CanConvertFork"] = ctx.Repo.Repository.IsFork && ctx.Doer.CanCreateRepoIn(ctx.Repo.Repository.Owner)
|
ctx.Data["CanConvertFork"] = ctx.Repo.Repository.IsFork && ctx.Doer.CanCreateRepoIn(ctx.Repo.Repository.Owner)
|
||||||
|
|
||||||
signing, _ := asymkey_service.SigningKey(ctx, ctx.Repo.Repository.RepoPath())
|
signing, _ := asymkey_service.SigningKey(ctx, ctx.Repo.Repository.RepoPath())
|
||||||
ctx.Data["SigningKeyAvailable"] = len(signing) > 0
|
ctx.Data["SigningKeyAvailable"] = signing != nil
|
||||||
ctx.Data["SigningSettings"] = setting.Repository.Signing
|
ctx.Data["SigningSettings"] = setting.Repository.Signing
|
||||||
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
|
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
|
||||||
|
|
||||||
|
@ -105,7 +105,7 @@ func SettingsPost(ctx *context.Context) {
|
||||||
ctx.Data["MinimumMirrorInterval"] = setting.Mirror.MinInterval
|
ctx.Data["MinimumMirrorInterval"] = setting.Mirror.MinInterval
|
||||||
|
|
||||||
signing, _ := asymkey_service.SigningKey(ctx, ctx.Repo.Repository.RepoPath())
|
signing, _ := asymkey_service.SigningKey(ctx, ctx.Repo.Repository.RepoPath())
|
||||||
ctx.Data["SigningKeyAvailable"] = len(signing) > 0
|
ctx.Data["SigningKeyAvailable"] = signing != nil
|
||||||
ctx.Data["SigningSettings"] = setting.Repository.Signing
|
ctx.Data["SigningSettings"] = setting.Repository.Signing
|
||||||
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
|
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ package asymkey
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
||||||
|
@ -359,24 +360,39 @@ func VerifyWithGPGSettings(ctx context.Context, gpgSettings *git.GPGSettings, si
|
||||||
return nil
|
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.
|
// 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
|
// 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{
|
keys, err := db.Find[asymkey_model.PublicKey](ctx, asymkey_model.FindPublicKeyOptions{
|
||||||
OwnerID: committer.ID,
|
OwnerID: committerUser.ID,
|
||||||
NotKeytype: asymkey_model.KeyTypePrincipal,
|
NotKeytype: asymkey_model.KeyTypePrincipal,
|
||||||
})
|
})
|
||||||
if err != nil { // Skipping failed to get ssh keys of user
|
if err != nil { // Skipping failed to get ssh keys of user
|
||||||
log.Error("ListPublicKeys: %v", err)
|
log.Error("ListPublicKeys: %v", err)
|
||||||
return &asymkey_model.CommitVerification{
|
return &asymkey_model.CommitVerification{
|
||||||
CommittingUser: committer,
|
CommittingUser: committerUser,
|
||||||
Verified: false,
|
Verified: false,
|
||||||
Reason: "gpg.error.failed_retrieval_gpg_keys",
|
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 {
|
if err != nil {
|
||||||
log.Error("GetEmailAddresses: %v", err)
|
log.Error("GetEmailAddresses: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -391,7 +407,7 @@ func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committer *
|
||||||
|
|
||||||
for _, k := range keys {
|
for _, k := range keys {
|
||||||
if k.Verified && activated {
|
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 {
|
if commitVerification != nil {
|
||||||
return commitVerification
|
return commitVerification
|
||||||
}
|
}
|
||||||
|
@ -399,8 +415,45 @@ func ParseCommitWithSSHSignature(ctx context.Context, c *git.Commit, committer *
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
Email: setting.Repository.Signing.SigningEmail,
|
||||||
|
}
|
||||||
|
commitVerification := verifySSHCommitVerificationByInstanceKey(c, committerUser, signerUser, c.Committer.Email, k)
|
||||||
|
if commitVerification != nil && commitVerification.Verified {
|
||||||
|
return commitVerification
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try the configured instance-wide SSH public key
|
||||||
|
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,
|
||||||
|
Name: setting.Repository.Signing.SigningName,
|
||||||
|
Email: setting.Repository.Signing.SigningEmail,
|
||||||
|
Format: setting.Repository.Signing.SigningFormat,
|
||||||
|
}
|
||||||
|
signerUser := &user_model.User{
|
||||||
|
Name: gpgSettings.Name,
|
||||||
|
Email: gpgSettings.Email,
|
||||||
|
}
|
||||||
|
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{
|
return &asymkey_model.CommitVerification{
|
||||||
CommittingUser: committer,
|
CommittingUser: committerUser,
|
||||||
Verified: false,
|
Verified: false,
|
||||||
Reason: asymkey_model.NoKeyFound,
|
Reason: asymkey_model.NoKeyFound,
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
// 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.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
|
||||||
|
author User Two <user2@example.com> 1749230009 +0200
|
||||||
|
committer User Two <user2@example.com> 1749230009 +0200
|
||||||
|
gpgsig -----BEGIN SSH SIGNATURE-----
|
||||||
|
U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgfpjiJ1VpbcT5svDW6qgB8kPujl
|
||||||
|
KK74epLnUT2hAs8T0AAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5
|
||||||
|
AAAAQDX2t2iHuuLxEWHLJetYXKsgayv3c43r0pJNfAzdLN55Q65pC5M7rG6++gT2bxcpOu
|
||||||
|
Y6EXbpLqia9sunEF3+LQY=
|
||||||
|
-----END SSH SIGNATURE-----
|
||||||
|
|
||||||
|
Initial commit with signed file
|
||||||
|
`))
|
||||||
|
require.NoError(t, err)
|
||||||
|
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.Equal(t, committingUser, ret.CommittingUser)
|
||||||
|
if assert.NotNil(t, ret.SigningUser) {
|
||||||
|
assert.Equal(t, "gitea", ret.SigningUser.Name)
|
||||||
|
assert.Equal(t, "gitea@fake.local", ret.SigningUser.Email)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ package asymkey
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
asymkey_model "code.gitea.io/gitea/models/asymkey"
|
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
|
// 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) (*git.SigningKey, *git.Signature) {
|
||||||
if setting.Repository.Signing.SigningKey == "none" {
|
if setting.Repository.Signing.SigningKey == "none" {
|
||||||
return "", nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if setting.Repository.Signing.SigningKey == "default" || setting.Repository.Signing.SigningKey == "" {
|
if setting.Repository.Signing.SigningKey == "default" || setting.Repository.Signing.SigningKey == "" {
|
||||||
|
@ -95,53 +96,77 @@ func SigningKey(ctx context.Context, repoPath string) (string, *git.Signature) {
|
||||||
value, _, _ := git.NewCommand("config", "--get", "commit.gpgsign").RunStdString(ctx, &git.RunOpts{Dir: repoPath})
|
value, _, _ := git.NewCommand("config", "--get", "commit.gpgsign").RunStdString(ctx, &git.RunOpts{Dir: repoPath})
|
||||||
sign, valid := git.ParseBool(strings.TrimSpace(value))
|
sign, valid := git.ParseBool(strings.TrimSpace(value))
|
||||||
if !sign || !valid {
|
if !sign || !valid {
|
||||||
return "", nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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})
|
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})
|
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})
|
signingEmail, _, _ := git.NewCommand("config", "--get", "user.email").RunStdString(ctx, &git.RunOpts{Dir: repoPath})
|
||||||
return strings.TrimSpace(signingKey), &git.Signature{
|
|
||||||
Name: strings.TrimSpace(signingName),
|
if strings.TrimSpace(signingKey) == "" {
|
||||||
Email: strings.TrimSpace(signingEmail),
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return &git.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{
|
if setting.Repository.Signing.SigningKey == "" {
|
||||||
Name: setting.Repository.Signing.SigningName,
|
return nil, nil
|
||||||
Email: setting.Repository.Signing.SigningEmail,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return &git.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
|
// 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) (content, format string, err error) {
|
||||||
signingKey, _ := SigningKey(ctx, repoPath)
|
signingKey, _ := SigningKey(ctx, repoPath)
|
||||||
if signingKey == "" {
|
if signingKey == nil {
|
||||||
return "", nil
|
return "", "", nil
|
||||||
|
}
|
||||||
|
if signingKey.Format == git.SigningKeyFormatSSH {
|
||||||
|
content, err := os.ReadFile(signingKey.KeyID)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Unable to read SSH public key file in %s: %s, %v", repoPath, signingKey, err)
|
||||||
|
return "", signingKey.Format, err
|
||||||
|
}
|
||||||
|
return string(content), signingKey.Format, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
content, stderr, err := process.GetManager().ExecDir(ctx, -1, repoPath,
|
content, stderr, err := process.GetManager().ExecDir(ctx, -1, repoPath,
|
||||||
"gpg --export -a", "gpg", "--export", "-a", signingKey)
|
"gpg --export -a", "gpg", "--export", "-a", signingKey.KeyID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Unable to get default signing key in %s: %s, %s, %v", repoPath, signingKey, stderr, err)
|
log.Error("Unable to get default signing key 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
|
// 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, *git.SigningKey, *git.Signature, error) {
|
||||||
rules := signingModeFromStrings(setting.Repository.Signing.InitialCommit)
|
rules := signingModeFromStrings(setting.Repository.Signing.InitialCommit)
|
||||||
signingKey, sig := SigningKey(ctx, repoPath)
|
signingKey, sig := SigningKey(ctx, repoPath)
|
||||||
if signingKey == "" {
|
if signingKey == nil {
|
||||||
return false, "", nil, &ErrWontSign{noKey}
|
return false, nil, nil, &ErrWontSign{noKey}
|
||||||
}
|
}
|
||||||
|
|
||||||
Loop:
|
Loop:
|
||||||
for _, rule := range rules {
|
for _, rule := range rules {
|
||||||
switch rule {
|
switch rule {
|
||||||
case never:
|
case never:
|
||||||
return false, "", nil, &ErrWontSign{never}
|
return false, nil, nil, &ErrWontSign{never}
|
||||||
case always:
|
case always:
|
||||||
break Loop
|
break Loop
|
||||||
case pubkey:
|
case pubkey:
|
||||||
|
@ -150,18 +175,18 @@ Loop:
|
||||||
IncludeSubKeys: true,
|
IncludeSubKeys: true,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, "", nil, err
|
return false, nil, nil, err
|
||||||
}
|
}
|
||||||
if len(keys) == 0 {
|
if len(keys) == 0 {
|
||||||
return false, "", nil, &ErrWontSign{pubkey}
|
return false, nil, nil, &ErrWontSign{pubkey}
|
||||||
}
|
}
|
||||||
case twofa:
|
case twofa:
|
||||||
twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID)
|
twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID)
|
||||||
if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
|
if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
|
||||||
return false, "", nil, err
|
return false, nil, nil, err
|
||||||
}
|
}
|
||||||
if twofaModel == nil {
|
if twofaModel == nil {
|
||||||
return false, "", nil, &ErrWontSign{twofa}
|
return false, nil, nil, &ErrWontSign{twofa}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -169,19 +194,19 @@ Loop:
|
||||||
}
|
}
|
||||||
|
|
||||||
// SignWikiCommit determines if we should sign the commits to this repository wiki
|
// 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, *git.SigningKey, *git.Signature, error) {
|
||||||
repoWikiPath := repo.WikiPath()
|
repoWikiPath := repo.WikiPath()
|
||||||
rules := signingModeFromStrings(setting.Repository.Signing.Wiki)
|
rules := signingModeFromStrings(setting.Repository.Signing.Wiki)
|
||||||
signingKey, sig := SigningKey(ctx, repoWikiPath)
|
signingKey, sig := SigningKey(ctx, repoWikiPath)
|
||||||
if signingKey == "" {
|
if signingKey == nil {
|
||||||
return false, "", nil, &ErrWontSign{noKey}
|
return false, nil, nil, &ErrWontSign{noKey}
|
||||||
}
|
}
|
||||||
|
|
||||||
Loop:
|
Loop:
|
||||||
for _, rule := range rules {
|
for _, rule := range rules {
|
||||||
switch rule {
|
switch rule {
|
||||||
case never:
|
case never:
|
||||||
return false, "", nil, &ErrWontSign{never}
|
return false, nil, nil, &ErrWontSign{never}
|
||||||
case always:
|
case always:
|
||||||
break Loop
|
break Loop
|
||||||
case pubkey:
|
case pubkey:
|
||||||
|
@ -190,35 +215,35 @@ Loop:
|
||||||
IncludeSubKeys: true,
|
IncludeSubKeys: true,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, "", nil, err
|
return false, nil, nil, err
|
||||||
}
|
}
|
||||||
if len(keys) == 0 {
|
if len(keys) == 0 {
|
||||||
return false, "", nil, &ErrWontSign{pubkey}
|
return false, nil, nil, &ErrWontSign{pubkey}
|
||||||
}
|
}
|
||||||
case twofa:
|
case twofa:
|
||||||
twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID)
|
twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID)
|
||||||
if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
|
if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
|
||||||
return false, "", nil, err
|
return false, nil, nil, err
|
||||||
}
|
}
|
||||||
if twofaModel == nil {
|
if twofaModel == nil {
|
||||||
return false, "", nil, &ErrWontSign{twofa}
|
return false, nil, nil, &ErrWontSign{twofa}
|
||||||
}
|
}
|
||||||
case parentSigned:
|
case parentSigned:
|
||||||
gitRepo, err := gitrepo.OpenRepository(ctx, repo.WikiStorageRepo())
|
gitRepo, err := gitrepo.OpenRepository(ctx, repo.WikiStorageRepo())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, "", nil, err
|
return false, nil, nil, err
|
||||||
}
|
}
|
||||||
defer gitRepo.Close()
|
defer gitRepo.Close()
|
||||||
commit, err := gitRepo.GetCommit("HEAD")
|
commit, err := gitRepo.GetCommit("HEAD")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, "", nil, err
|
return false, nil, nil, err
|
||||||
}
|
}
|
||||||
if commit.Signature == nil {
|
if commit.Signature == nil {
|
||||||
return false, "", nil, &ErrWontSign{parentSigned}
|
return false, nil, nil, &ErrWontSign{parentSigned}
|
||||||
}
|
}
|
||||||
verification := ParseCommitWithSignature(ctx, commit)
|
verification := ParseCommitWithSignature(ctx, commit)
|
||||||
if !verification.Verified {
|
if !verification.Verified {
|
||||||
return false, "", nil, &ErrWontSign{parentSigned}
|
return false, nil, nil, &ErrWontSign{parentSigned}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -226,18 +251,18 @@ Loop:
|
||||||
}
|
}
|
||||||
|
|
||||||
// SignCRUDAction determines if we should sign a CRUD commit to this repository
|
// 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, *git.SigningKey, *git.Signature, error) {
|
||||||
rules := signingModeFromStrings(setting.Repository.Signing.CRUDActions)
|
rules := signingModeFromStrings(setting.Repository.Signing.CRUDActions)
|
||||||
signingKey, sig := SigningKey(ctx, repoPath)
|
signingKey, sig := SigningKey(ctx, repoPath)
|
||||||
if signingKey == "" {
|
if signingKey == nil {
|
||||||
return false, "", nil, &ErrWontSign{noKey}
|
return false, nil, nil, &ErrWontSign{noKey}
|
||||||
}
|
}
|
||||||
|
|
||||||
Loop:
|
Loop:
|
||||||
for _, rule := range rules {
|
for _, rule := range rules {
|
||||||
switch rule {
|
switch rule {
|
||||||
case never:
|
case never:
|
||||||
return false, "", nil, &ErrWontSign{never}
|
return false, nil, nil, &ErrWontSign{never}
|
||||||
case always:
|
case always:
|
||||||
break Loop
|
break Loop
|
||||||
case pubkey:
|
case pubkey:
|
||||||
|
@ -246,35 +271,35 @@ Loop:
|
||||||
IncludeSubKeys: true,
|
IncludeSubKeys: true,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, "", nil, err
|
return false, nil, nil, err
|
||||||
}
|
}
|
||||||
if len(keys) == 0 {
|
if len(keys) == 0 {
|
||||||
return false, "", nil, &ErrWontSign{pubkey}
|
return false, nil, nil, &ErrWontSign{pubkey}
|
||||||
}
|
}
|
||||||
case twofa:
|
case twofa:
|
||||||
twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID)
|
twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID)
|
||||||
if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
|
if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
|
||||||
return false, "", nil, err
|
return false, nil, nil, err
|
||||||
}
|
}
|
||||||
if twofaModel == nil {
|
if twofaModel == nil {
|
||||||
return false, "", nil, &ErrWontSign{twofa}
|
return false, nil, nil, &ErrWontSign{twofa}
|
||||||
}
|
}
|
||||||
case parentSigned:
|
case parentSigned:
|
||||||
gitRepo, err := git.OpenRepository(ctx, tmpBasePath)
|
gitRepo, err := git.OpenRepository(ctx, tmpBasePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, "", nil, err
|
return false, nil, nil, err
|
||||||
}
|
}
|
||||||
defer gitRepo.Close()
|
defer gitRepo.Close()
|
||||||
commit, err := gitRepo.GetCommit(parentCommit)
|
commit, err := gitRepo.GetCommit(parentCommit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, "", nil, err
|
return false, nil, nil, err
|
||||||
}
|
}
|
||||||
if commit.Signature == nil {
|
if commit.Signature == nil {
|
||||||
return false, "", nil, &ErrWontSign{parentSigned}
|
return false, nil, nil, &ErrWontSign{parentSigned}
|
||||||
}
|
}
|
||||||
verification := ParseCommitWithSignature(ctx, commit)
|
verification := ParseCommitWithSignature(ctx, commit)
|
||||||
if !verification.Verified {
|
if !verification.Verified {
|
||||||
return false, "", nil, &ErrWontSign{parentSigned}
|
return false, nil, nil, &ErrWontSign{parentSigned}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -282,16 +307,16 @@ Loop:
|
||||||
}
|
}
|
||||||
|
|
||||||
// SignMerge determines if we should sign a PR merge commit to the base repository
|
// 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, *git.SigningKey, *git.Signature, error) {
|
||||||
if err := pr.LoadBaseRepo(ctx); err != nil {
|
if err := pr.LoadBaseRepo(ctx); err != nil {
|
||||||
log.Error("Unable to get Base Repo for pull request")
|
log.Error("Unable to get Base Repo for pull request")
|
||||||
return false, "", nil, err
|
return false, nil, nil, err
|
||||||
}
|
}
|
||||||
repo := pr.BaseRepo
|
repo := pr.BaseRepo
|
||||||
|
|
||||||
signingKey, signer := SigningKey(ctx, repo.RepoPath())
|
signingKey, signer := SigningKey(ctx, repo.RepoPath())
|
||||||
if signingKey == "" {
|
if signingKey == nil {
|
||||||
return false, "", nil, &ErrWontSign{noKey}
|
return false, nil, nil, &ErrWontSign{noKey}
|
||||||
}
|
}
|
||||||
rules := signingModeFromStrings(setting.Repository.Signing.Merges)
|
rules := signingModeFromStrings(setting.Repository.Signing.Merges)
|
||||||
|
|
||||||
|
@ -302,7 +327,7 @@ Loop:
|
||||||
for _, rule := range rules {
|
for _, rule := range rules {
|
||||||
switch rule {
|
switch rule {
|
||||||
case never:
|
case never:
|
||||||
return false, "", nil, &ErrWontSign{never}
|
return false, nil, nil, &ErrWontSign{never}
|
||||||
case always:
|
case always:
|
||||||
break Loop
|
break Loop
|
||||||
case pubkey:
|
case pubkey:
|
||||||
|
@ -311,91 +336,91 @@ Loop:
|
||||||
IncludeSubKeys: true,
|
IncludeSubKeys: true,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, "", nil, err
|
return false, nil, nil, err
|
||||||
}
|
}
|
||||||
if len(keys) == 0 {
|
if len(keys) == 0 {
|
||||||
return false, "", nil, &ErrWontSign{pubkey}
|
return false, nil, nil, &ErrWontSign{pubkey}
|
||||||
}
|
}
|
||||||
case twofa:
|
case twofa:
|
||||||
twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID)
|
twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID)
|
||||||
if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
|
if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
|
||||||
return false, "", nil, err
|
return false, nil, nil, err
|
||||||
}
|
}
|
||||||
if twofaModel == nil {
|
if twofaModel == nil {
|
||||||
return false, "", nil, &ErrWontSign{twofa}
|
return false, nil, nil, &ErrWontSign{twofa}
|
||||||
}
|
}
|
||||||
case approved:
|
case approved:
|
||||||
protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, pr.BaseBranch)
|
protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, pr.BaseBranch)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, "", nil, err
|
return false, nil, nil, err
|
||||||
}
|
}
|
||||||
if protectedBranch == nil {
|
if protectedBranch == nil {
|
||||||
return false, "", nil, &ErrWontSign{approved}
|
return false, nil, nil, &ErrWontSign{approved}
|
||||||
}
|
}
|
||||||
if issues_model.GetGrantedApprovalsCount(ctx, protectedBranch, pr) < 1 {
|
if issues_model.GetGrantedApprovalsCount(ctx, protectedBranch, pr) < 1 {
|
||||||
return false, "", nil, &ErrWontSign{approved}
|
return false, nil, nil, &ErrWontSign{approved}
|
||||||
}
|
}
|
||||||
case baseSigned:
|
case baseSigned:
|
||||||
if gitRepo == nil {
|
if gitRepo == nil {
|
||||||
gitRepo, err = git.OpenRepository(ctx, tmpBasePath)
|
gitRepo, err = git.OpenRepository(ctx, tmpBasePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, "", nil, err
|
return false, nil, nil, err
|
||||||
}
|
}
|
||||||
defer gitRepo.Close()
|
defer gitRepo.Close()
|
||||||
}
|
}
|
||||||
commit, err := gitRepo.GetCommit(baseCommit)
|
commit, err := gitRepo.GetCommit(baseCommit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, "", nil, err
|
return false, nil, nil, err
|
||||||
}
|
}
|
||||||
verification := ParseCommitWithSignature(ctx, commit)
|
verification := ParseCommitWithSignature(ctx, commit)
|
||||||
if !verification.Verified {
|
if !verification.Verified {
|
||||||
return false, "", nil, &ErrWontSign{baseSigned}
|
return false, nil, nil, &ErrWontSign{baseSigned}
|
||||||
}
|
}
|
||||||
case headSigned:
|
case headSigned:
|
||||||
if gitRepo == nil {
|
if gitRepo == nil {
|
||||||
gitRepo, err = git.OpenRepository(ctx, tmpBasePath)
|
gitRepo, err = git.OpenRepository(ctx, tmpBasePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, "", nil, err
|
return false, nil, nil, err
|
||||||
}
|
}
|
||||||
defer gitRepo.Close()
|
defer gitRepo.Close()
|
||||||
}
|
}
|
||||||
commit, err := gitRepo.GetCommit(headCommit)
|
commit, err := gitRepo.GetCommit(headCommit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, "", nil, err
|
return false, nil, nil, err
|
||||||
}
|
}
|
||||||
verification := ParseCommitWithSignature(ctx, commit)
|
verification := ParseCommitWithSignature(ctx, commit)
|
||||||
if !verification.Verified {
|
if !verification.Verified {
|
||||||
return false, "", nil, &ErrWontSign{headSigned}
|
return false, nil, nil, &ErrWontSign{headSigned}
|
||||||
}
|
}
|
||||||
case commitsSigned:
|
case commitsSigned:
|
||||||
if gitRepo == nil {
|
if gitRepo == nil {
|
||||||
gitRepo, err = git.OpenRepository(ctx, tmpBasePath)
|
gitRepo, err = git.OpenRepository(ctx, tmpBasePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, "", nil, err
|
return false, nil, nil, err
|
||||||
}
|
}
|
||||||
defer gitRepo.Close()
|
defer gitRepo.Close()
|
||||||
}
|
}
|
||||||
commit, err := gitRepo.GetCommit(headCommit)
|
commit, err := gitRepo.GetCommit(headCommit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, "", nil, err
|
return false, nil, nil, err
|
||||||
}
|
}
|
||||||
verification := ParseCommitWithSignature(ctx, commit)
|
verification := ParseCommitWithSignature(ctx, commit)
|
||||||
if !verification.Verified {
|
if !verification.Verified {
|
||||||
return false, "", nil, &ErrWontSign{commitsSigned}
|
return false, nil, nil, &ErrWontSign{commitsSigned}
|
||||||
}
|
}
|
||||||
// need to work out merge-base
|
// need to work out merge-base
|
||||||
mergeBaseCommit, _, err := gitRepo.GetMergeBase("", baseCommit, headCommit)
|
mergeBaseCommit, _, err := gitRepo.GetMergeBase("", baseCommit, headCommit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, "", nil, err
|
return false, nil, nil, err
|
||||||
}
|
}
|
||||||
commitList, err := commit.CommitsBeforeUntil(mergeBaseCommit)
|
commitList, err := commit.CommitsBeforeUntil(mergeBaseCommit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, "", nil, err
|
return false, nil, nil, err
|
||||||
}
|
}
|
||||||
for _, commit := range commitList {
|
for _, commit := range commitList {
|
||||||
verification := ParseCommitWithSignature(ctx, commit)
|
verification := ParseCommitWithSignature(ctx, commit)
|
||||||
if !verification.Verified {
|
if !verification.Verified {
|
||||||
return false, "", nil, &ErrWontSign{commitsSigned}
|
return false, nil, nil, &ErrWontSign{commitsSigned}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -101,7 +101,7 @@ type CanCommitToBranchResults struct {
|
||||||
UserCanPush bool
|
UserCanPush bool
|
||||||
RequireSigned bool
|
RequireSigned bool
|
||||||
WillSign bool
|
WillSign bool
|
||||||
SigningKey string
|
SigningKey *git.SigningKey
|
||||||
WontSignReason string
|
WontSignReason string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -432,10 +432,13 @@ func doMergeAndPush(ctx context.Context, pr *issues_model.PullRequest, doer *use
|
||||||
|
|
||||||
func commitAndSignNoAuthor(ctx *mergeContext, message string) error {
|
func commitAndSignNoAuthor(ctx *mergeContext, message string) error {
|
||||||
cmdCommit := git.NewCommand("commit").AddOptionFormat("--message=%s", message)
|
cmdCommit := git.NewCommand("commit").AddOptionFormat("--message=%s", message)
|
||||||
if ctx.signKeyID == "" {
|
if ctx.signKey == nil {
|
||||||
cmdCommit.AddArguments("--no-gpg-sign")
|
cmdCommit.AddArguments("--no-gpg-sign")
|
||||||
} else {
|
} 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 {
|
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())
|
log.Error("git commit %-v: %v\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String())
|
||||||
|
|
|
@ -27,7 +27,7 @@ type mergeContext struct {
|
||||||
doer *user_model.User
|
doer *user_model.User
|
||||||
sig *git.Signature
|
sig *git.Signature
|
||||||
committer *git.Signature
|
committer *git.Signature
|
||||||
signKeyID string // empty for no-sign, non-empty to sign
|
signKey *git.SigningKey
|
||||||
env []string
|
env []string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,9 +99,9 @@ func createTemporaryRepoForMerge(ctx context.Context, pr *issues_model.PullReque
|
||||||
mergeCtx.committer = mergeCtx.sig
|
mergeCtx.committer = mergeCtx.sig
|
||||||
|
|
||||||
// Determine if we should sign
|
// 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 {
|
if sign {
|
||||||
mergeCtx.signKeyID = keyID
|
mergeCtx.signKey = key
|
||||||
if pr.BaseRepo.GetTrustModel() == repo_model.CommitterTrustModel || pr.BaseRepo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
|
if pr.BaseRepo.GetTrustModel() == repo_model.CommitterTrustModel || pr.BaseRepo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
|
||||||
mergeCtx.committer = signer
|
mergeCtx.committer = signer
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,10 +71,13 @@ func doMergeStyleSquash(ctx *mergeContext, message string) error {
|
||||||
cmdCommit := git.NewCommand("commit").
|
cmdCommit := git.NewCommand("commit").
|
||||||
AddOptionFormat("--author='%s <%s>'", sig.Name, sig.Email).
|
AddOptionFormat("--author='%s <%s>'", sig.Name, sig.Email).
|
||||||
AddOptionFormat("--message=%s", message)
|
AddOptionFormat("--message=%s", message)
|
||||||
if ctx.signKeyID == "" {
|
if ctx.signKey == nil {
|
||||||
cmdCommit.AddArguments("--no-gpg-sign")
|
cmdCommit.AddArguments("--no-gpg-sign")
|
||||||
} else {
|
} 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 {
|
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())
|
log.Error("git commit %-v: %v\n%s\n%s", ctx.pr, err, ctx.outbuf.String(), ctx.errbuf.String())
|
||||||
|
|
|
@ -293,15 +293,18 @@ func (t *TemporaryUploadRepository) CommitTree(ctx context.Context, opts *Commit
|
||||||
}
|
}
|
||||||
|
|
||||||
var sign bool
|
var sign bool
|
||||||
var keyID string
|
var key *git.SigningKey
|
||||||
var signer *git.Signature
|
var signer *git.Signature
|
||||||
if opts.ParentCommitID != "" {
|
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 {
|
} 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 {
|
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 t.repo.GetTrustModel() == repo_model.CommitterTrustModel || t.repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
|
||||||
if committerSig.Name != authorSig.Name || committerSig.Email != authorSig.Email {
|
if committerSig.Name != authorSig.Name || committerSig.Email != authorSig.Email {
|
||||||
// Add trailers
|
// Add trailers
|
||||||
|
|
|
@ -42,9 +42,12 @@ func initRepoCommit(ctx context.Context, tmpPath string, repo *repo_model.Reposi
|
||||||
cmd := git.NewCommand("commit", "--message=Initial commit").
|
cmd := git.NewCommand("commit", "--message=Initial commit").
|
||||||
AddOptionFormat("--author='%s <%s>'", sig.Name, sig.Email)
|
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 {
|
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 {
|
if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
|
||||||
// need to set the committer to the KeyID owner
|
// need to set the committer to the KeyID owner
|
||||||
|
|
|
@ -194,7 +194,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
|
||||||
|
|
||||||
sign, signingKey, signer, _ := asymkey_service.SignWikiCommit(ctx, repo, doer)
|
sign, signingKey, signer, _ := asymkey_service.SignWikiCommit(ctx, repo, doer)
|
||||||
if sign {
|
if sign {
|
||||||
commitTreeOpts.KeyID = signingKey
|
commitTreeOpts.Key = signingKey
|
||||||
if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
|
if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
|
||||||
committer = signer
|
committer = signer
|
||||||
}
|
}
|
||||||
|
@ -316,7 +316,7 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model
|
||||||
|
|
||||||
sign, signingKey, signer, _ := asymkey_service.SignWikiCommit(ctx, repo, doer)
|
sign, signingKey, signer, _ := asymkey_service.SignWikiCommit(ctx, repo, doer)
|
||||||
if sign {
|
if sign {
|
||||||
commitTreeOpts.KeyID = signingKey
|
commitTreeOpts.Key = signingKey
|
||||||
if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
|
if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
|
||||||
committer = signer
|
committer = signer
|
||||||
}
|
}
|
||||||
|
|
|
@ -15164,6 +15164,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": {
|
"/repos/{owner}/{repo}/stargazers": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"produces": [
|
||||||
|
@ -16997,6 +17033,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}": {
|
"/teams/{id}": {
|
||||||
"get": {
|
"get": {
|
||||||
"produces": [
|
"produces": [
|
||||||
|
|
|
@ -4,7 +4,10 @@
|
||||||
package integration
|
package integration
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/ed25519"
|
||||||
|
"crypto/rand"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
@ -23,6 +26,7 @@ import (
|
||||||
"github.com/ProtonMail/go-crypto/openpgp/armor"
|
"github.com/ProtonMail/go-crypto/openpgp/armor"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGPGGit(t *testing.T) {
|
func TestGPGGit(t *testing.T) {
|
||||||
|
@ -42,6 +46,37 @@ func TestGPGGit(t *testing.T) {
|
||||||
defer test.MockVariableValue(&setting.Repository.Signing.InitialCommit, []string{"never"})()
|
defer test.MockVariableValue(&setting.Repository.Signing.InitialCommit, []string{"never"})()
|
||||||
defer test.MockVariableValue(&setting.Repository.Signing.CRUDActions, []string{"never"})()
|
defer test.MockVariableValue(&setting.Repository.Signing.CRUDActions, []string{"never"})()
|
||||||
|
|
||||||
|
testGitSigning(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSSHGit(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir() // use a temp dir to store the SSH keys
|
||||||
|
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")
|
||||||
|
|
||||||
|
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 testGitSigning(t *testing.T) {
|
||||||
username := "user2"
|
username := "user2"
|
||||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: username})
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: username})
|
||||||
baseAPITestContext := NewAPITestContext(t, username, "repo1")
|
baseAPITestContext := NewAPITestContext(t, username, "repo1")
|
Loading…
Reference in New Issue