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
|
||||
;; 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 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
|
||||
;DEFAULT_TRUST_MODEL = collaborator
|
||||
|
@ -1223,6 +1230,13 @@ 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.
|
||||
;; 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
|
||||
brokenArgs []string
|
||||
cmd *exec.Cmd // for debug purpose only
|
||||
configArgs []string
|
||||
}
|
||||
|
||||
func logArgSanitize(arg string) string {
|
||||
|
@ -196,6 +197,16 @@ func (c *Command) AddDashesAndList(list ...string) *Command {
|
|||
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
|
||||
// In most cases, it shouldn't be used. Use NewCommand().AddXxx() function instead
|
||||
func ToTrustedCmdArgs(args []string) TrustedCmdArgs {
|
||||
|
@ -321,7 +332,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(c.configArgs, c.args...)...)
|
||||
c.cmd = cmd // for debug purpose only
|
||||
if opts.Env == nil {
|
||||
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
|
||||
Name string
|
||||
PublicKeyContent string
|
||||
Format string
|
||||
}
|
||||
|
||||
const prettyLogFormat = `--pretty=format:%H`
|
||||
|
|
|
@ -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 == SigningKeyFormatSSH {
|
||||
content, err := os.ReadFile(gpgSettings.KeyID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read SSH public key file: %s, %w", gpgSettings.KeyID, err)
|
||||
}
|
||||
gpgSettings.PublicKeyContent = string(content)
|
||||
return nil
|
||||
}
|
||||
content, stderr, err := process.GetManager().Exec(
|
||||
"gpg -a --export",
|
||||
"gpg", "-a", "--export", gpgSettings.KeyID)
|
||||
|
@ -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", "--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})
|
||||
gpgSettings.Email = strings.TrimSpace(defaultEmail)
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ import (
|
|||
type CommitTreeOpts struct {
|
||||
Parents []string
|
||||
Message string
|
||||
KeyID string
|
||||
Key *SigningKey
|
||||
NoGPGSign bool
|
||||
AlwaysSign bool
|
||||
}
|
||||
|
@ -43,8 +43,13 @@ func (repo *Repository) CommitTree(author, committer *Signature, tree *Tree, opt
|
|||
_, _ = messageBytes.WriteString(opts.Message)
|
||||
_, _ = messageBytes.WriteString("\n")
|
||||
|
||||
if opts.KeyID != "" || opts.AlwaysSign {
|
||||
cmd.AddOptionFormat("-S%s", opts.KeyID)
|
||||
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 {
|
||||
|
|
|
@ -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,20 +244,24 @@ 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: "",
|
||||
SigningEmail: "",
|
||||
SigningFormat: "openpgp", // git.SigningKeyFormatOpenPGP
|
||||
InitialCommit: []string{"always"},
|
||||
CRUDActions: []string{"pubkey", "twofa", "parentsigned"},
|
||||
Merges: []string{"pubkey", "twofa", "basesigned", "commitssigned"},
|
||||
Wiki: []string{"never"},
|
||||
DefaultTrustModel: "collaborator",
|
||||
TrustedSSHKeys: []string{},
|
||||
},
|
||||
}
|
||||
RepoRootPath string
|
||||
|
|
|
@ -971,7 +971,8 @@ 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)
|
||||
m.Post("/markdown/raw", reqToken(), misc.MarkdownRaw)
|
||||
|
@ -1427,7 +1428,8 @@ 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).
|
||||
Put(reqToken(), reqAdmin(), bind(api.RepoTopicOptions{}), repo.UpdateTopics)
|
||||
|
|
|
@ -4,14 +4,35 @@
|
|||
package misc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
||||
"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
|
||||
|
@ -44,19 +65,42 @@ 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, err := asymkey_service.PublicSigningKey(ctx, path)
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(err)
|
||||
return
|
||||
}
|
||||
_, err = ctx.Write([]byte(content))
|
||||
if err != nil {
|
||||
ctx.APIErrorInternal(fmt.Errorf("Error writing key content %w", err))
|
||||
}
|
||||
getSigningKey(ctx, git.SigningKeyFormatOpenPGP)
|
||||
}
|
||||
|
||||
// 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
|
||||
// ---
|
||||
// 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
|
||||
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)
|
||||
|
||||
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["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"] = signing != nil
|
||||
ctx.Data["SigningSettings"] = setting.Repository.Signing
|
||||
ctx.Data["IsRepoIndexerEnabled"] = setting.Indexer.RepoIndexerEnabled
|
||||
|
||||
|
|
|
@ -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,7 +407,7 @@ 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
|
||||
}
|
||||
|
@ -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{
|
||||
CommittingUser: committer,
|
||||
CommittingUser: committerUser,
|
||||
Verified: false,
|
||||
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 (
|
||||
"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) (*git.SigningKey, *git.Signature) {
|
||||
if setting.Repository.Signing.SigningKey == "none" {
|
||||
return "", nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
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})
|
||||
sign, valid := git.ParseBool(strings.TrimSpace(value))
|
||||
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})
|
||||
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),
|
||||
|
||||
if strings.TrimSpace(signingKey) == "" {
|
||||
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{
|
||||
Name: setting.Repository.Signing.SigningName,
|
||||
Email: setting.Repository.Signing.SigningEmail,
|
||||
if setting.Repository.Signing.SigningKey == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
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
|
||||
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)
|
||||
if signingKey == "" {
|
||||
return "", nil
|
||||
if signingKey == nil {
|
||||
return "", "", nil
|
||||
}
|
||||
if signingKey.Format == git.SigningKeyFormatSSH {
|
||||
content, err := os.ReadFile(signingKey.KeyID)
|
||||
if err != nil {
|
||||
log.Error("Unable to read SSH public key file in %s: %s, %v", repoPath, signingKey, err)
|
||||
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, *git.SigningKey, *git.Signature, error) {
|
||||
rules := signingModeFromStrings(setting.Repository.Signing.InitialCommit)
|
||||
signingKey, sig := SigningKey(ctx, repoPath)
|
||||
if signingKey == "" {
|
||||
return false, "", nil, &ErrWontSign{noKey}
|
||||
if signingKey == nil {
|
||||
return false, nil, nil, &ErrWontSign{noKey}
|
||||
}
|
||||
|
||||
Loop:
|
||||
for _, rule := range rules {
|
||||
switch rule {
|
||||
case never:
|
||||
return false, "", nil, &ErrWontSign{never}
|
||||
return false, nil, nil, &ErrWontSign{never}
|
||||
case always:
|
||||
break Loop
|
||||
case pubkey:
|
||||
|
@ -150,18 +175,18 @@ Loop:
|
|||
IncludeSubKeys: true,
|
||||
})
|
||||
if err != nil {
|
||||
return false, "", nil, err
|
||||
return false, nil, nil, err
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
return false, "", 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, "", nil, err
|
||||
return false, nil, nil, err
|
||||
}
|
||||
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
|
||||
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()
|
||||
rules := signingModeFromStrings(setting.Repository.Signing.Wiki)
|
||||
signingKey, sig := SigningKey(ctx, repoWikiPath)
|
||||
if signingKey == "" {
|
||||
return false, "", nil, &ErrWontSign{noKey}
|
||||
if signingKey == nil {
|
||||
return false, nil, nil, &ErrWontSign{noKey}
|
||||
}
|
||||
|
||||
Loop:
|
||||
for _, rule := range rules {
|
||||
switch rule {
|
||||
case never:
|
||||
return false, "", nil, &ErrWontSign{never}
|
||||
return false, nil, nil, &ErrWontSign{never}
|
||||
case always:
|
||||
break Loop
|
||||
case pubkey:
|
||||
|
@ -190,35 +215,35 @@ Loop:
|
|||
IncludeSubKeys: true,
|
||||
})
|
||||
if err != nil {
|
||||
return false, "", nil, err
|
||||
return false, nil, nil, err
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
return false, "", 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, "", nil, err
|
||||
return false, nil, nil, err
|
||||
}
|
||||
if twofaModel == nil {
|
||||
return false, "", nil, &ErrWontSign{twofa}
|
||||
return false, nil, nil, &ErrWontSign{twofa}
|
||||
}
|
||||
case parentSigned:
|
||||
gitRepo, err := gitrepo.OpenRepository(ctx, repo.WikiStorageRepo())
|
||||
if err != nil {
|
||||
return false, "", nil, err
|
||||
return false, nil, nil, err
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
commit, err := gitRepo.GetCommit("HEAD")
|
||||
if err != nil {
|
||||
return false, "", nil, err
|
||||
return false, nil, nil, err
|
||||
}
|
||||
if commit.Signature == nil {
|
||||
return false, "", nil, &ErrWontSign{parentSigned}
|
||||
return false, nil, nil, &ErrWontSign{parentSigned}
|
||||
}
|
||||
verification := ParseCommitWithSignature(ctx, commit)
|
||||
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
|
||||
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)
|
||||
signingKey, sig := SigningKey(ctx, repoPath)
|
||||
if signingKey == "" {
|
||||
return false, "", nil, &ErrWontSign{noKey}
|
||||
if signingKey == nil {
|
||||
return false, nil, nil, &ErrWontSign{noKey}
|
||||
}
|
||||
|
||||
Loop:
|
||||
for _, rule := range rules {
|
||||
switch rule {
|
||||
case never:
|
||||
return false, "", nil, &ErrWontSign{never}
|
||||
return false, nil, nil, &ErrWontSign{never}
|
||||
case always:
|
||||
break Loop
|
||||
case pubkey:
|
||||
|
@ -246,35 +271,35 @@ Loop:
|
|||
IncludeSubKeys: true,
|
||||
})
|
||||
if err != nil {
|
||||
return false, "", nil, err
|
||||
return false, nil, nil, err
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
return false, "", 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, "", nil, err
|
||||
return false, nil, nil, err
|
||||
}
|
||||
if twofaModel == nil {
|
||||
return false, "", nil, &ErrWontSign{twofa}
|
||||
return false, nil, nil, &ErrWontSign{twofa}
|
||||
}
|
||||
case parentSigned:
|
||||
gitRepo, err := git.OpenRepository(ctx, tmpBasePath)
|
||||
if err != nil {
|
||||
return false, "", nil, err
|
||||
return false, nil, nil, err
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
commit, err := gitRepo.GetCommit(parentCommit)
|
||||
if err != nil {
|
||||
return false, "", nil, err
|
||||
return false, nil, nil, err
|
||||
}
|
||||
if commit.Signature == nil {
|
||||
return false, "", nil, &ErrWontSign{parentSigned}
|
||||
return false, nil, nil, &ErrWontSign{parentSigned}
|
||||
}
|
||||
verification := ParseCommitWithSignature(ctx, commit)
|
||||
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
|
||||
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 {
|
||||
log.Error("Unable to get Base Repo for pull request")
|
||||
return false, "", nil, err
|
||||
return false, nil, nil, err
|
||||
}
|
||||
repo := pr.BaseRepo
|
||||
|
||||
signingKey, signer := SigningKey(ctx, repo.RepoPath())
|
||||
if signingKey == "" {
|
||||
return false, "", nil, &ErrWontSign{noKey}
|
||||
if signingKey == nil {
|
||||
return false, nil, nil, &ErrWontSign{noKey}
|
||||
}
|
||||
rules := signingModeFromStrings(setting.Repository.Signing.Merges)
|
||||
|
||||
|
@ -302,7 +327,7 @@ Loop:
|
|||
for _, rule := range rules {
|
||||
switch rule {
|
||||
case never:
|
||||
return false, "", nil, &ErrWontSign{never}
|
||||
return false, nil, nil, &ErrWontSign{never}
|
||||
case always:
|
||||
break Loop
|
||||
case pubkey:
|
||||
|
@ -311,91 +336,91 @@ Loop:
|
|||
IncludeSubKeys: true,
|
||||
})
|
||||
if err != nil {
|
||||
return false, "", nil, err
|
||||
return false, nil, nil, err
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
return false, "", 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, "", nil, err
|
||||
return false, nil, nil, err
|
||||
}
|
||||
if twofaModel == nil {
|
||||
return false, "", 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, "", nil, err
|
||||
return false, nil, nil, err
|
||||
}
|
||||
if protectedBranch == nil {
|
||||
return false, "", nil, &ErrWontSign{approved}
|
||||
return false, nil, nil, &ErrWontSign{approved}
|
||||
}
|
||||
if issues_model.GetGrantedApprovalsCount(ctx, protectedBranch, pr) < 1 {
|
||||
return false, "", 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, "", nil, err
|
||||
return false, nil, nil, err
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
}
|
||||
commit, err := gitRepo.GetCommit(baseCommit)
|
||||
if err != nil {
|
||||
return false, "", nil, err
|
||||
return false, nil, nil, err
|
||||
}
|
||||
verification := ParseCommitWithSignature(ctx, commit)
|
||||
if !verification.Verified {
|
||||
return false, "", 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, "", nil, err
|
||||
return false, nil, nil, err
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
}
|
||||
commit, err := gitRepo.GetCommit(headCommit)
|
||||
if err != nil {
|
||||
return false, "", nil, err
|
||||
return false, nil, nil, err
|
||||
}
|
||||
verification := ParseCommitWithSignature(ctx, commit)
|
||||
if !verification.Verified {
|
||||
return false, "", 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, "", nil, err
|
||||
return false, nil, nil, err
|
||||
}
|
||||
defer gitRepo.Close()
|
||||
}
|
||||
commit, err := gitRepo.GetCommit(headCommit)
|
||||
if err != nil {
|
||||
return false, "", nil, err
|
||||
return false, nil, nil, err
|
||||
}
|
||||
verification := ParseCommitWithSignature(ctx, commit)
|
||||
if !verification.Verified {
|
||||
return false, "", 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, "", nil, err
|
||||
return false, nil, nil, err
|
||||
}
|
||||
commitList, err := commit.CommitsBeforeUntil(mergeBaseCommit)
|
||||
if err != nil {
|
||||
return false, "", nil, err
|
||||
return false, nil, nil, err
|
||||
}
|
||||
for _, commit := range commitList {
|
||||
verification := ParseCommitWithSignature(ctx, commit)
|
||||
if !verification.Verified {
|
||||
return false, "", nil, &ErrWontSign{commitsSigned}
|
||||
return false, nil, nil, &ErrWontSign{commitsSigned}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -101,7 +101,7 @@ type CanCommitToBranchResults struct {
|
|||
UserCanPush bool
|
||||
RequireSigned bool
|
||||
WillSign bool
|
||||
SigningKey string
|
||||
SigningKey *git.SigningKey
|
||||
WontSignReason string
|
||||
}
|
||||
|
||||
|
|
|
@ -432,10 +432,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 == nil {
|
||||
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())
|
||||
|
|
|
@ -27,7 +27,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 *git.SigningKey
|
||||
env []string
|
||||
}
|
||||
|
||||
|
@ -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.signKeyID = keyID
|
||||
mergeCtx.signKey = key
|
||||
if pr.BaseRepo.GetTrustModel() == repo_model.CommitterTrustModel || pr.BaseRepo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
|
||||
mergeCtx.committer = signer
|
||||
}
|
||||
|
|
|
@ -71,10 +71,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 == nil {
|
||||
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())
|
||||
|
|
|
@ -293,15 +293,18 @@ func (t *TemporaryUploadRepository) CommitTree(ctx context.Context, opts *Commit
|
|||
}
|
||||
|
||||
var sign bool
|
||||
var keyID string
|
||||
var key *git.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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
if sign {
|
||||
commitTreeOpts.KeyID = signingKey
|
||||
commitTreeOpts.Key = signingKey
|
||||
if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
|
||||
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)
|
||||
if sign {
|
||||
commitTreeOpts.KeyID = signingKey
|
||||
commitTreeOpts.Key = signingKey
|
||||
if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
|
||||
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": {
|
||||
"get": {
|
||||
"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}": {
|
||||
"get": {
|
||||
"produces": [
|
||||
|
|
|
@ -4,7 +4,10 @@
|
|||
package integration
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
|
@ -23,6 +26,7 @@ import (
|
|||
"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) {
|
||||
|
@ -42,6 +46,37 @@ 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 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"
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: username})
|
||||
baseAPITestContext := NewAPITestContext(t, username, "repo1")
|
Loading…
Reference in New Issue