diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index 79793ad47f..11c5d5eb0a 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1163,9 +1163,13 @@ LEVEL = Info ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; +;; Signing format that Forgejo should use, openpgp uses GPG and ssh uses OpenSSH. +;FORMAT = openpgp +;; ;; GPG key to use to sign commits, Defaults to the default - that is the value of git config --get user.signingkey ;; run in the context of the RUN_USER -;; Switch to none to stop signing completely +;; Switch to none to stop signing completely. +;; If `FORMAT` is set to **ssh** this should be set to an absolute path to an public OpenSSH key. ;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. diff --git a/models/asymkey/gpg_key_object_verification.go b/models/asymkey/gpg_key_object_verification.go index 407a29c221..ae2ee3661d 100644 --- a/models/asymkey/gpg_key_object_verification.go +++ b/models/asymkey/gpg_key_object_verification.go @@ -201,7 +201,7 @@ func ParseObjectWithSignature(ctx context.Context, c *GitObject) *ObjectVerifica } } - if setting.Repository.Signing.SigningKey != "" && setting.Repository.Signing.SigningKey != "default" && setting.Repository.Signing.SigningKey != "none" { + if setting.Repository.Signing.Format == "openpgp" && setting.Repository.Signing.SigningKey != "" && setting.Repository.Signing.SigningKey != "default" && setting.Repository.Signing.SigningKey != "none" { // OK we should try the default key gpgSettings := git.GPGSettings{ Sign: true, diff --git a/models/asymkey/ssh_key_object_verification.go b/models/asymkey/ssh_key_object_verification.go index e0476fe5a8..5d933d35f9 100644 --- a/models/asymkey/ssh_key_object_verification.go +++ b/models/asymkey/ssh_key_object_verification.go @@ -12,8 +12,10 @@ import ( "forgejo.org/models/db" user_model "forgejo.org/models/user" "forgejo.org/modules/log" + "forgejo.org/modules/setting" "github.com/42wim/sshsig" + "golang.org/x/crypto/ssh" ) // ParseObjectWithSSHSignature check if signature is good against keystore. @@ -62,6 +64,22 @@ func ParseObjectWithSSHSignature(ctx context.Context, c *GitObject, committer *u } } + // If the SSH instance key is set, try to verify it with that key. + if setting.SSHInstanceKey != nil { + instanceSSHKey := &PublicKey{ + Content: string(ssh.MarshalAuthorizedKey(setting.SSHInstanceKey)), + Fingerprint: ssh.FingerprintSHA256(setting.SSHInstanceKey), + } + instanceUser := &user_model.User{ + Name: setting.Repository.Signing.SigningName, + Email: setting.Repository.Signing.SigningEmail, + } + commitVerification := verifySSHObjectVerification(c.Signature.Signature, c.Signature.Payload, instanceSSHKey, committer, instanceUser, setting.Repository.Signing.SigningEmail) + if commitVerification != nil { + return commitVerification + } + } + return &ObjectVerification{ CommittingUser: committer, Verified: false, diff --git a/models/asymkey/ssh_key_object_verification_test.go b/models/asymkey/ssh_key_object_verification_test.go index 5d1b7edc27..4bfd79d56e 100644 --- a/models/asymkey/ssh_key_object_verification_test.go +++ b/models/asymkey/ssh_key_object_verification_test.go @@ -4,6 +4,7 @@ package asymkey import ( + "os" "testing" "forgejo.org/models/db" @@ -15,6 +16,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "golang.org/x/crypto/ssh" ) func TestParseCommitWithSSHSignature(t *testing.T) { @@ -150,4 +152,43 @@ muPLbvEduU+Ze/1Ol1pgk= assert.Equal(t, "user2 / SHA256:TKfwbZMR7e9OnlV2l1prfah1TXH8CmqR0PvFEXVCXA4", commitVerification.Reason) assert.Equal(t, sshKey, commitVerification.SigningSSHKey) }) + + t.Run("Instance key", func(t *testing.T) { + pubKeyContent, err := os.ReadFile("../../tests/integration/ssh-signing-key.pub") + require.NoError(t, err) + pubKey, _, _, _, err := ssh.ParseAuthorizedKey(pubKeyContent) + require.NoError(t, err) + + defer test.MockVariableValue(&setting.Repository.Signing.SigningName, "UwU")() + defer test.MockVariableValue(&setting.Repository.Signing.SigningEmail, "fox@example.com")() + defer test.MockVariableValue(&setting.SSHInstanceKey, pubKey)() + + gitCommit := &git.Commit{ + Committer: &git.Signature{ + Email: "fox@example.com", + }, + Signature: &git.ObjectSignature{ + Payload: `tree f96f1a4f1a51dc42e2983592f503980b60b8849c +parent 93f84db542dd8c6e952c8130bc2fcbe2e299b8b4 +author OwO 1738961379 +0100 +committer UwU 1738961379 +0100 + +Fox +`, + Signature: `-----BEGIN SSH SIGNATURE----- +U1NIU0lHAAAAAQAAADMAAAALc3NoLWVkMjU1MTkAAAAgV5ELwZ8XJe2LLR/UTuEu/vsFdb +t7ry0W8hyzz/b1iocAAAADZ2l0AAAAAAAAAAZzaGE1MTIAAABTAAAAC3NzaC1lZDI1NTE5 +AAAAQCnyMRkWVVNoZxZkvi/ZoknUhs4LNBmEwZs9e9214WIt+mhKfc6BiHoE2qeluR2McD +Y5RzHnA8Ke9wXddEePCQE= +-----END SSH SIGNATURE----- +`, + }, + } + + o := commitToGitObject(gitCommit) + commitVerification := ParseObjectWithSSHSignature(db.DefaultContext, &o, user2) + assert.True(t, commitVerification.Verified) + assert.Equal(t, "UwU / SHA256:QttK41r/zMUeAW71b5UgVSb8xGFF/DlZJ6TyADW+uoI", commitVerification.Reason) + assert.Equal(t, "SHA256:QttK41r/zMUeAW71b5UgVSb8xGFF/DlZJ6TyADW+uoI", commitVerification.SigningSSHKey.Fingerprint) + }) } diff --git a/modules/git/git.go b/modules/git/git.go index c7d5a31b31..30d41933b2 100644 --- a/modules/git/git.go +++ b/modules/git/git.go @@ -278,6 +278,49 @@ func syncGitConfig() (err error) { return err } + switch setting.Repository.Signing.Format { + case "ssh": + // First do a git version check. + if CheckGitVersionAtLeast("2.34.0") != nil { + return errors.New("ssh signing requires Git >= 2.34.0") + } + + // Get the ssh-keygen binary that Git will use. + // This can be overriden in app.ini in [git.config] section, so we must + // query this information. + sshKeygenPath, err := configGet("gpg.ssh.program") + if err != nil { + return err + } + // git is very stubborn and does not give a default value, so we must do + // this ourselves. + if len(sshKeygenPath) == 0 { + // Default value of git, very unlikely to change. + // https://github.com/git/git/blob/5b97a56fa0e7d580dc8865b73107407c9b3f0eff/gpg-interface.c#L116 + sshKeygenPath = "ssh-keygen" + } + + // Although there's a version requirement of 8.2p1, there's no cross-version + // method to get the version of ssh-keygen. Therefore we do a simple binary + // presence check and hope for the best. + if _, err := exec.LookPath(sshKeygenPath); err != nil { + if errors.Is(err, exec.ErrNotFound) { + return errors.New("git signing requires a ssh-keygen binary") + } + return err + } + + if err := configSet("gpg.format", "ssh"); err != nil { + return err + } + // openpgp is already the default value, so in the case of a non SSH format + // set the value to openpgp. + default: + if err := configSet("gpg.format", "openpgp"); err != nil { + return err + } + } + // By default partial clones are disabled, enable them from git v2.22 if !setting.Git.DisablePartialClone && CheckGitVersionAtLeast("2.22") == nil { if err = configSet("uploadpack.allowfilter", "true"); err != nil { @@ -324,6 +367,15 @@ func CheckGitVersionEqual(equal string) error { return nil } +func configGet(key string) (string, error) { + stdout, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil) + if err != nil && !IsErrorExitCode(err, 1) { + return "", fmt.Errorf("failed to get git config %s, err: %w", key, err) + } + + return strings.TrimSpace(stdout), nil +} + func configSet(key, value string) error { stdout, _, err := NewCommand(DefaultContext, "config", "--global", "--get").AddDynamicArguments(key).RunStdString(nil) if err != nil && !IsErrorExitCode(err, 1) { diff --git a/modules/git/git_test.go b/modules/git/git_test.go index bb07367e3b..a3faf85f16 100644 --- a/modules/git/git_test.go +++ b/modules/git/git_test.go @@ -11,8 +11,10 @@ import ( "testing" "forgejo.org/modules/setting" + "forgejo.org/modules/test" "forgejo.org/modules/util" + "github.com/hashicorp/go-version" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -94,3 +96,57 @@ func TestSyncConfig(t *testing.T) { assert.True(t, gitConfigContains("[sync-test]")) assert.True(t, gitConfigContains("cfg-key-a = CfgValA")) } + +func TestSyncConfigGPGFormat(t *testing.T) { + defer test.MockProtect(&setting.GitConfig)() + + t.Run("No format", func(t *testing.T) { + defer test.MockVariableValue(&setting.Repository.Signing.Format, "")() + require.NoError(t, syncGitConfig()) + assert.True(t, gitConfigContains("[gpg]")) + assert.True(t, gitConfigContains("format = openpgp")) + }) + + t.Run("SSH format", func(t *testing.T) { + r, err := os.OpenRoot(t.TempDir()) + require.NoError(t, err) + f, err := r.OpenFile("ssh-keygen", os.O_CREATE|os.O_TRUNC, 0o700) + require.NoError(t, f.Close()) + require.NoError(t, err) + t.Setenv("PATH", r.Name()) + defer test.MockVariableValue(&setting.Repository.Signing.Format, "ssh")() + + require.NoError(t, syncGitConfig()) + assert.True(t, gitConfigContains("[gpg]")) + assert.True(t, gitConfigContains("format = ssh")) + + t.Run("Old version", func(t *testing.T) { + oldVersion, err := version.NewVersion("2.33.0") + require.NoError(t, err) + defer test.MockVariableValue(&gitVersion, oldVersion)() + require.ErrorContains(t, syncGitConfig(), "ssh signing requires Git >= 2.34.0") + }) + + t.Run("No ssh-keygen binary", func(t *testing.T) { + require.NoError(t, r.Remove("ssh-keygen")) + require.ErrorContains(t, syncGitConfig(), "git signing requires a ssh-keygen binary") + }) + + t.Run("Dynamic ssh-keygen binary location", func(t *testing.T) { + f, err := r.OpenFile("ssh-keygen-2", os.O_CREATE|os.O_TRUNC, 0o700) + require.NoError(t, f.Close()) + require.NoError(t, err) + defer test.MockVariableValue(&setting.GitConfig.Options, map[string]string{ + "gpg.ssh.program": "ssh-keygen-2", + })() + require.NoError(t, syncGitConfig()) + }) + }) + + t.Run("OpenPGP format", func(t *testing.T) { + defer test.MockVariableValue(&setting.Repository.Signing.Format, "openpgp")() + require.NoError(t, syncGitConfig()) + assert.True(t, gitConfigContains("[gpg]")) + assert.True(t, gitConfigContains("format = openpgp")) + }) +} diff --git a/modules/setting/repository.go b/modules/setting/repository.go index 9efd674f8b..a770740f86 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -4,12 +4,15 @@ package setting import ( + "os" "os/exec" "path" "path/filepath" "strings" "forgejo.org/modules/log" + + "golang.org/x/crypto/ssh" ) // enumerates all the policy repository creating @@ -26,6 +29,8 @@ var MaxUserCardsPerPage = 36 // MaxForksPerPage sets maximum amount of forks shown per page var MaxForksPerPage = 40 +var SSHInstanceKey ssh.PublicKey + // Repository settings var ( Repository = struct { @@ -109,6 +114,7 @@ var ( SigningKey string SigningName string SigningEmail string + Format string InitialCommit []string CRUDActions []string `ini:"CRUD_ACTIONS"` Merges []string @@ -262,6 +268,7 @@ var ( SigningKey string SigningName string SigningEmail string + Format string InitialCommit []string CRUDActions []string `ini:"CRUD_ACTIONS"` Merges []string @@ -271,6 +278,7 @@ var ( SigningKey: "default", SigningName: "", SigningEmail: "", + Format: "openpgp", InitialCommit: []string{"always"}, CRUDActions: []string{"pubkey", "twofa", "parentsigned"}, Merges: []string{"pubkey", "twofa", "basesigned", "commitssigned"}, @@ -376,4 +384,15 @@ func loadRepositoryFrom(rootCfg ConfigProvider) { log.Fatal("loadRepoArchiveFrom: %v", err) } Repository.EnableFlags = sec.Key("ENABLE_FLAGS").MustBool() + + if Repository.Signing.Format == "ssh" && Repository.Signing.SigningKey != "none" && Repository.Signing.SigningKey != "" { + sshPublicKey, err := os.ReadFile(Repository.Signing.SigningKey) + if err != nil { + log.Fatal("Could not read repository signing key in %q: %v", Repository.Signing.SigningKey, err) + } + SSHInstanceKey, _, _, _, err = ssh.ParseAuthorizedKey(sshPublicKey) + if err != nil { + log.Fatal("Could not parse the SSH signing key %q: %v", sshPublicKey, err) + } + } } diff --git a/modules/setting/repository_test.go b/modules/setting/repository_test.go new file mode 100644 index 0000000000..d36e739f6b --- /dev/null +++ b/modules/setting/repository_test.go @@ -0,0 +1,59 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package setting + +import ( + "fmt" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/crypto/ssh" +) + +func TestSSHInstanceKey(t *testing.T) { + sshSigningKeyPath, err := filepath.Abs("../../tests/integration/ssh-signing-key.pub") + require.NoError(t, err) + + t.Run("None value", func(t *testing.T) { + cfg, err := NewConfigProviderFromData(` +[repository.signing] +FORMAT = ssh +SIGNING_KEY = none +`) + require.NoError(t, err) + + loadRepositoryFrom(cfg) + + assert.Nil(t, SSHInstanceKey) + }) + + t.Run("No value", func(t *testing.T) { + cfg, err := NewConfigProviderFromData(` +[repository.signing] +FORMAT = ssh +`) + require.NoError(t, err) + + loadRepositoryFrom(cfg) + + assert.Nil(t, SSHInstanceKey) + }) + t.Run("Normal", func(t *testing.T) { + iniStr := fmt.Sprintf(` +[repository.signing] +FORMAT = ssh +SIGNING_KEY = %s +`, sshSigningKeyPath) + cfg, err := NewConfigProviderFromData(iniStr) + require.NoError(t, err) + + loadRepositoryFrom(cfg) + + assert.NotNil(t, SSHInstanceKey) + assert.Equal(t, "ssh-ed25519", SSHInstanceKey.Type()) + assert.EqualValues(t, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFeRC8GfFyXtiy0f1E7hLv77BXW7e68tFvIcs8/29YqH\n", ssh.MarshalAuthorizedKey(SSHInstanceKey)) + }) +} diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 1c6f260b23..d638e79a60 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -865,6 +865,7 @@ func Routes() *web.Route { m.Group("", func() { m.Get("/version", misc.Version) m.Get("/signing-key.gpg", misc.SigningKey) + m.Get("/signing-key.ssh", misc.SSHSigningKey) m.Post("/markup", reqToken(), bind(api.MarkupOption{}), misc.Markup) m.Post("/markdown", reqToken(), bind(api.MarkdownOption{}), misc.Markdown) m.Post("/markdown/raw", reqToken(), misc.MarkdownRaw) diff --git a/routers/api/v1/misc/signing.go b/routers/api/v1/misc/signing.go index 945df6068f..9f829b8443 100644 --- a/routers/api/v1/misc/signing.go +++ b/routers/api/v1/misc/signing.go @@ -7,8 +7,11 @@ import ( "fmt" "net/http" + "forgejo.org/modules/setting" asymkey_service "forgejo.org/services/asymkey" "forgejo.org/services/context" + + "golang.org/x/crypto/ssh" ) // SigningKey returns the public key of the default signing key if it exists @@ -61,3 +64,29 @@ func SigningKey(ctx *context.APIContext) { ctx.Error(http.StatusInternalServerError, "gpg export", fmt.Errorf("Error writing key content %w", err)) } } + +// SSHSigningKey returns the public SSH key of the default signing key if it exists +func SSHSigningKey(ctx *context.APIContext) { + // swagger:operation GET /signing-key.ssh miscellaneous getSSHSigningKey + // --- + // summary: Get default signing-key.ssh + // produces: + // - text/plain + // responses: + // "200": + // description: "SSH public key in OpenSSH authorized key format" + // schema: + // type: string + // "404": + // "$ref": "#/responses/notFound" + + if setting.SSHInstanceKey == nil { + ctx.NotFound() + return + } + + _, err := ctx.Write(ssh.MarshalAuthorizedKey(setting.SSHInstanceKey)) + if err != nil { + ctx.Error(http.StatusInternalServerError, "ssh export", err) + } +} diff --git a/services/asymkey/sign.go b/services/asymkey/sign.go index 0030523b22..592396769d 100644 --- a/services/asymkey/sign.go +++ b/services/asymkey/sign.go @@ -90,6 +90,13 @@ func SigningKey(ctx context.Context, repoPath string) (string, *git.Signature) { return "", nil } + if setting.Repository.Signing.Format == "ssh" { + return setting.Repository.Signing.SigningKey, &git.Signature{ + Name: setting.Repository.Signing.SigningName, + Email: setting.Repository.Signing.SigningEmail, + } + } + if setting.Repository.Signing.SigningKey == "default" || setting.Repository.Signing.SigningKey == "" { // Can ignore the error here as it means that commit.gpgsign is not set value, _, _ := git.NewCommand(ctx, "config", "--get", "commit.gpgsign").RunStdString(&git.RunOpts{Dir: repoPath}) diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 97529248e9..21679881b2 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -17315,6 +17315,29 @@ } } }, + "/signing-key.ssh": { + "get": { + "produces": [ + "text/plain" + ], + "tags": [ + "miscellaneous" + ], + "summary": "Get default signing-key.ssh", + "operationId": "getSSHSigningKey", + "responses": { + "200": { + "description": "SSH public key in OpenSSH authorized key format", + "schema": { + "type": "string" + } + }, + "404": { + "$ref": "#/responses/notFound" + } + } + } + }, "/teams/{id}": { "get": { "produces": [ diff --git a/tests/integration/api_misc_test.go b/tests/integration/api_misc_test.go new file mode 100644 index 0000000000..2687a9bb31 --- /dev/null +++ b/tests/integration/api_misc_test.go @@ -0,0 +1,38 @@ +// Copyright 2025 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: GPL-3.0-or-later + +package integration + +import ( + "net/http" + "testing" + + "forgejo.org/modules/setting" + "forgejo.org/modules/test" + "forgejo.org/tests" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/crypto/ssh" +) + +func TestAPISSHSigningKey(t *testing.T) { + defer tests.PrepareTestEnv(t)() + + t.Run("No signing key", func(t *testing.T) { + defer test.MockVariableValue(&setting.SSHInstanceKey, nil)() + defer tests.PrintCurrentTest(t)() + + MakeRequest(t, NewRequest(t, "GET", "/api/v1/signing-key.ssh"), http.StatusNotFound) + }) + t.Run("With signing key", func(t *testing.T) { + publicKey := "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFeRC8GfFyXtiy0f1E7hLv77BXW7e68tFvIcs8/29YqH\n" + pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(publicKey)) + require.NoError(t, err) + defer test.MockVariableValue(&setting.SSHInstanceKey, pubKey)() + defer tests.PrintCurrentTest(t)() + + resp := MakeRequest(t, NewRequest(t, "GET", "/api/v1/signing-key.ssh"), http.StatusOK) + assert.Equal(t, publicKey, resp.Body.String()) + }) +} diff --git a/tests/integration/gpg_git_test.go b/tests/integration/gpg_git_test.go deleted file mode 100644 index 271c225a3f..0000000000 --- a/tests/integration/gpg_git_test.go +++ /dev/null @@ -1,304 +0,0 @@ -// Copyright 2019 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package integration - -import ( - "encoding/base64" - "fmt" - "net/url" - "os" - "testing" - - auth_model "forgejo.org/models/auth" - "forgejo.org/models/unittest" - user_model "forgejo.org/models/user" - "forgejo.org/modules/git" - "forgejo.org/modules/process" - "forgejo.org/modules/setting" - api "forgejo.org/modules/structs" - "forgejo.org/modules/test" - "forgejo.org/tests" - - "github.com/ProtonMail/go-crypto/openpgp" - "github.com/ProtonMail/go-crypto/openpgp/armor" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGPGGit(t *testing.T) { - tmpDir := t.TempDir() // use a temp dir to avoid messing with the user's GPG keyring - err := os.Chmod(tmpDir, 0o700) - require.NoError(t, err) - - t.Setenv("GNUPGHOME", tmpDir) - require.NoError(t, err) - - // Need to create a root key - rootKeyPair, err := importTestingKey() - require.NoError(t, err, "importTestingKey") - - defer test.MockVariableValue(&setting.Repository.Signing.SigningKey, rootKeyPair.PrimaryKey.KeyIdShortString())() - defer test.MockVariableValue(&setting.Repository.Signing.SigningName, "gitea")() - defer test.MockVariableValue(&setting.Repository.Signing.SigningEmail, "gitea@fake.local")() - defer test.MockVariableValue(&setting.Repository.Signing.InitialCommit, []string{"never"})() - defer test.MockVariableValue(&setting.Repository.Signing.CRUDActions, []string{"never"})() - - username := "user2" - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: username}) - baseAPITestContext := NewAPITestContext(t, username, "repo1") - - onGiteaRun(t, func(t *testing.T, u *url.URL) { - u.Path = baseAPITestContext.GitPath() - - t.Run("Unsigned-Initial", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) - t.Run("CreateRepository", doAPICreateRepository(testCtx, false, git.Sha1ObjectFormat)) // FIXME: use forEachObjectFormat - t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) { - assert.NotNil(t, branch.Commit) - assert.NotNil(t, branch.Commit.Verification) - assert.False(t, branch.Commit.Verification.Verified) - assert.Empty(t, branch.Commit.Verification.Signature) - })) - t.Run("CreateCRUDFile-Never", crudActionCreateFile( - t, testCtx, user, "master", "never", "unsigned-never.txt", func(t *testing.T, response api.FileResponse) { - assert.False(t, response.Verification.Verified) - })) - t.Run("CreateCRUDFile-Never", crudActionCreateFile( - t, testCtx, user, "never", "never2", "unsigned-never2.txt", func(t *testing.T, response api.FileResponse) { - assert.False(t, response.Verification.Verified) - })) - }) - - setting.Repository.Signing.CRUDActions = []string{"parentsigned"} - t.Run("Unsigned-Initial-CRUD-ParentSigned", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) - t.Run("CreateCRUDFile-ParentSigned", crudActionCreateFile( - t, testCtx, user, "master", "parentsigned", "signed-parent.txt", func(t *testing.T, response api.FileResponse) { - assert.False(t, response.Verification.Verified) - })) - t.Run("CreateCRUDFile-ParentSigned", crudActionCreateFile( - t, testCtx, user, "parentsigned", "parentsigned2", "signed-parent2.txt", func(t *testing.T, response api.FileResponse) { - assert.False(t, response.Verification.Verified) - })) - }) - - setting.Repository.Signing.CRUDActions = []string{"never"} - t.Run("Unsigned-Initial-CRUD-Never", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) - t.Run("CreateCRUDFile-Never", crudActionCreateFile( - t, testCtx, user, "parentsigned", "parentsigned-never", "unsigned-never2.txt", func(t *testing.T, response api.FileResponse) { - assert.False(t, response.Verification.Verified) - })) - }) - - setting.Repository.Signing.CRUDActions = []string{"always"} - t.Run("Unsigned-Initial-CRUD-Always", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) - t.Run("CreateCRUDFile-Always", crudActionCreateFile( - t, testCtx, user, "master", "always", "signed-always.txt", func(t *testing.T, response api.FileResponse) { - assert.NotNil(t, response.Verification) - if response.Verification == nil { - assert.FailNow(t, "no verification provided with response", "response: %v", response) - } - assert.True(t, response.Verification.Verified) - if !response.Verification.Verified { - t.FailNow() - } - assert.Equal(t, "gitea@fake.local", response.Verification.Signer.Email) - })) - t.Run("CreateCRUDFile-ParentSigned-always", crudActionCreateFile( - t, testCtx, user, "parentsigned", "parentsigned-always", "signed-parent2.txt", func(t *testing.T, response api.FileResponse) { - assert.NotNil(t, response.Verification) - if response.Verification == nil { - assert.FailNow(t, "no verification provided with response", "response: %v", response) - } - assert.True(t, response.Verification.Verified) - if !response.Verification.Verified { - t.FailNow() - } - assert.Equal(t, "gitea@fake.local", response.Verification.Signer.Email) - })) - }) - - setting.Repository.Signing.CRUDActions = []string{"parentsigned"} - t.Run("Unsigned-Initial-CRUD-ParentSigned", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) - t.Run("CreateCRUDFile-Always-ParentSigned", crudActionCreateFile( - t, testCtx, user, "always", "always-parentsigned", "signed-always-parentsigned.txt", func(t *testing.T, response api.FileResponse) { - assert.NotNil(t, response.Verification) - if response.Verification == nil { - assert.FailNow(t, "no verification provided with response", "response: %v", response) - } - assert.True(t, response.Verification.Verified) - if !response.Verification.Verified { - t.FailNow() - } - assert.Equal(t, "gitea@fake.local", response.Verification.Signer.Email) - })) - }) - - setting.Repository.Signing.InitialCommit = []string{"always"} - t.Run("AlwaysSign-Initial", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - testCtx := NewAPITestContext(t, username, "initial-always", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) - t.Run("CreateRepository", doAPICreateRepository(testCtx, false, git.Sha1ObjectFormat)) // FIXME: use forEachObjectFormat - t.Run("CheckMasterBranchSigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) { - assert.NotNil(t, branch.Commit) - if branch.Commit == nil { - assert.FailNow(t, "no commit provided with branch", "branch: %v", branch) - } - assert.NotNil(t, branch.Commit.Verification) - if branch.Commit.Verification == nil { - assert.FailNow(t, "no verification provided with branch commit", "commit: %v", branch.Commit) - } - assert.True(t, branch.Commit.Verification.Verified) - if !branch.Commit.Verification.Verified { - t.FailNow() - } - assert.Equal(t, "gitea@fake.local", branch.Commit.Verification.Signer.Email) - })) - }) - - setting.Repository.Signing.CRUDActions = []string{"never"} - t.Run("AlwaysSign-Initial-CRUD-Never", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - testCtx := NewAPITestContext(t, username, "initial-always-never", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) - t.Run("CreateRepository", doAPICreateRepository(testCtx, false, git.Sha1ObjectFormat)) // FIXME: use forEachObjectFormat - t.Run("CreateCRUDFile-Never", crudActionCreateFile( - t, testCtx, user, "master", "never", "unsigned-never.txt", func(t *testing.T, response api.FileResponse) { - assert.False(t, response.Verification.Verified) - })) - }) - - setting.Repository.Signing.CRUDActions = []string{"parentsigned"} - t.Run("AlwaysSign-Initial-CRUD-ParentSigned-On-Always", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - testCtx := NewAPITestContext(t, username, "initial-always-parent", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) - t.Run("CreateRepository", doAPICreateRepository(testCtx, false, git.Sha1ObjectFormat)) // FIXME: use forEachObjectFormat - t.Run("CreateCRUDFile-ParentSigned", crudActionCreateFile( - t, testCtx, user, "master", "parentsigned", "signed-parent.txt", func(t *testing.T, response api.FileResponse) { - assert.True(t, response.Verification.Verified) - if !response.Verification.Verified { - t.FailNow() - return - } - assert.Equal(t, "gitea@fake.local", response.Verification.Signer.Email) - })) - }) - - setting.Repository.Signing.CRUDActions = []string{"always"} - t.Run("AlwaysSign-Initial-CRUD-Always", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - testCtx := NewAPITestContext(t, username, "initial-always-always", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) - t.Run("CreateRepository", doAPICreateRepository(testCtx, false, git.Sha1ObjectFormat)) // FIXME: use forEachObjectFormat - t.Run("CreateCRUDFile-Always", crudActionCreateFile( - t, testCtx, user, "master", "always", "signed-always.txt", func(t *testing.T, response api.FileResponse) { - assert.True(t, response.Verification.Verified) - if !response.Verification.Verified { - t.FailNow() - return - } - assert.Equal(t, "gitea@fake.local", response.Verification.Signer.Email) - })) - }) - - setting.Repository.Signing.Merges = []string{"commitssigned"} - t.Run("UnsignedMerging", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) - t.Run("CreatePullRequest", func(t *testing.T) { - pr, err := doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "never2")(t) - require.NoError(t, err) - t.Run("MergePR", doAPIMergePullRequest(testCtx, testCtx.Username, testCtx.Reponame, pr.Index)) - }) - t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) { - assert.NotNil(t, branch.Commit) - assert.NotNil(t, branch.Commit.Verification) - assert.False(t, branch.Commit.Verification.Verified) - assert.Empty(t, branch.Commit.Verification.Signature) - })) - }) - - setting.Repository.Signing.Merges = []string{"basesigned"} - t.Run("BaseSignedMerging", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) - t.Run("CreatePullRequest", func(t *testing.T) { - pr, err := doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "parentsigned2")(t) - require.NoError(t, err) - t.Run("MergePR", doAPIMergePullRequest(testCtx, testCtx.Username, testCtx.Reponame, pr.Index)) - }) - t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) { - assert.NotNil(t, branch.Commit) - assert.NotNil(t, branch.Commit.Verification) - assert.False(t, branch.Commit.Verification.Verified) - assert.Empty(t, branch.Commit.Verification.Signature) - })) - }) - - setting.Repository.Signing.Merges = []string{"commitssigned"} - t.Run("CommitsSignedMerging", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - testCtx := NewAPITestContext(t, username, "initial-unsigned", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) - t.Run("CreatePullRequest", func(t *testing.T) { - pr, err := doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "always-parentsigned")(t) - require.NoError(t, err) - t.Run("MergePR", doAPIMergePullRequest(testCtx, testCtx.Username, testCtx.Reponame, pr.Index)) - }) - t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) { - assert.NotNil(t, branch.Commit) - assert.NotNil(t, branch.Commit.Verification) - assert.True(t, branch.Commit.Verification.Verified) - })) - }) - }) -} - -func crudActionCreateFile(_ *testing.T, ctx APITestContext, user *user_model.User, from, to, path string, callback ...func(*testing.T, api.FileResponse)) func(*testing.T) { - return doAPICreateFile(ctx, path, &api.CreateFileOptions{ - FileOptions: api.FileOptions{ - BranchName: from, - NewBranchName: to, - Message: fmt.Sprintf("from:%s to:%s path:%s", from, to, path), - Author: api.Identity{ - Name: user.FullName, - Email: user.Email, - }, - Committer: api.Identity{ - Name: user.FullName, - Email: user.Email, - }, - }, - ContentBase64: base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("This is new text for %s", path))), - }, callback...) -} - -func importTestingKey() (*openpgp.Entity, error) { - if _, _, err := process.GetManager().Exec("gpg --import tests/integration/private-testing.key", "gpg", "--import", "tests/integration/private-testing.key"); err != nil { - return nil, err - } - keyringFile, err := os.Open("tests/integration/private-testing.key") - if err != nil { - return nil, err - } - defer keyringFile.Close() - - block, err := armor.Decode(keyringFile) - if err != nil { - return nil, err - } - - keyring, err := openpgp.ReadKeyRing(block.Body) - if err != nil { - return nil, fmt.Errorf("Keyring access failed: '%w'", err) - } - - // There should only be one entity in this file. - return keyring[0], nil -} diff --git a/tests/integration/signing_git_test.go b/tests/integration/signing_git_test.go new file mode 100644 index 0000000000..bc662ee3bb --- /dev/null +++ b/tests/integration/signing_git_test.go @@ -0,0 +1,330 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "context" + "encoding/base64" + "fmt" + "net/url" + "os" + "path/filepath" + "testing" + + auth_model "forgejo.org/models/auth" + "forgejo.org/models/unittest" + user_model "forgejo.org/models/user" + "forgejo.org/modules/git" + "forgejo.org/modules/process" + "forgejo.org/modules/setting" + api "forgejo.org/modules/structs" + "forgejo.org/modules/test" + "forgejo.org/tests" + + "github.com/ProtonMail/go-crypto/openpgp" + "github.com/ProtonMail/go-crypto/openpgp/armor" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/crypto/ssh" +) + +func TestInstanceSigning(t *testing.T) { + t.Cleanup(func() { + // Cannot use t.Context(), it is in the done state. + require.NoError(t, git.InitFull(context.Background())) //nolint:usetesting + }) + + onGiteaRun(t, func(t *testing.T, u *url.URL) { + defer test.MockVariableValue(&setting.Repository.Signing.SigningName, "UwU")() + defer test.MockVariableValue(&setting.Repository.Signing.SigningEmail, "fox@example.com")() + defer test.MockProtect(&setting.Repository.Signing.InitialCommit)() + defer test.MockProtect(&setting.Repository.Signing.CRUDActions)() + + t.Run("SSH", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + pubKeyContent, err := os.ReadFile("tests/integration/ssh-signing-key.pub") + require.NoError(t, err) + + pubKey, _, _, _, err := ssh.ParseAuthorizedKey(pubKeyContent) + require.NoError(t, err) + signingKeyPath, err := filepath.Abs("tests/integration/ssh-signing-key") + require.NoError(t, err) + require.NoError(t, os.Chmod(signingKeyPath, 0o600)) + defer test.MockVariableValue(&setting.SSHInstanceKey, pubKey)() + defer test.MockVariableValue(&setting.Repository.Signing.Format, "ssh")() + defer test.MockVariableValue(&setting.Repository.Signing.SigningKey, signingKeyPath)() + + // Ensure the git config is updated with the new signing format. + require.NoError(t, git.InitFull(t.Context())) + + forEachObjectFormat(t, func(t *testing.T, objectFormat git.ObjectFormat) { + u2 := *u + testCRUD(t, &u2, "ssh", objectFormat) + }) + }) + + t.Run("PGP", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + // Use a new GNUPGPHOME to avoid messing with the existing GPG keyring. + tmpDir := t.TempDir() + require.NoError(t, os.Chmod(tmpDir, 0o700)) + t.Setenv("GNUPGHOME", tmpDir) + + rootKeyPair, err := importTestingKey() + require.NoError(t, err) + defer test.MockVariableValue(&setting.Repository.Signing.SigningKey, rootKeyPair.PrimaryKey.KeyIdShortString())() + defer test.MockVariableValue(&setting.Repository.Signing.Format, "openpgp")() + + // Ensure the git config is updated with the new signing format. + require.NoError(t, git.InitFull(t.Context())) + + forEachObjectFormat(t, func(t *testing.T, objectFormat git.ObjectFormat) { + u2 := *u + testCRUD(t, &u2, "pgp", objectFormat) + }) + }) + }) +} + +func testCRUD(t *testing.T, u *url.URL, signingFormat string, objectFormat git.ObjectFormat) { + t.Helper() + setting.Repository.Signing.CRUDActions = []string{"never"} + setting.Repository.Signing.InitialCommit = []string{"never"} + + username := "user2" + user := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: username}) + baseAPITestContext := NewAPITestContext(t, username, "repo1") + u.Path = baseAPITestContext.GitPath() + + suffix := "-" + signingFormat + "-" + objectFormat.Name() + + t.Run("Unsigned-Initial", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + + testCtx := NewAPITestContext(t, username, "initial-unsigned"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + t.Run("CreateRepository", doAPICreateRepository(testCtx, false, objectFormat)) + t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) { + assert.NotNil(t, branch.Commit) + assert.NotNil(t, branch.Commit.Verification) + assert.False(t, branch.Commit.Verification.Verified) + assert.Empty(t, branch.Commit.Verification.Signature) + })) + t.Run("CreateCRUDFile-Never", crudActionCreateFile( + t, testCtx, user, "master", "never", "unsigned-never.txt", func(t *testing.T, response api.FileResponse) { + assert.False(t, response.Verification.Verified) + })) + t.Run("CreateCRUDFile-Never", crudActionCreateFile( + t, testCtx, user, "never", "never2", "unsigned-never2.txt", func(t *testing.T, response api.FileResponse) { + assert.False(t, response.Verification.Verified) + })) + }) + + t.Run("Unsigned-Initial-CRUD-ParentSigned", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + setting.Repository.Signing.CRUDActions = []string{"parentsigned"} + + testCtx := NewAPITestContext(t, username, "initial-unsigned"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + t.Run("CreateCRUDFile-ParentSigned", crudActionCreateFile( + t, testCtx, user, "master", "parentsigned", "signed-parent.txt", func(t *testing.T, response api.FileResponse) { + assert.False(t, response.Verification.Verified) + })) + t.Run("CreateCRUDFile-ParentSigned", crudActionCreateFile( + t, testCtx, user, "parentsigned", "parentsigned2", "signed-parent2.txt", func(t *testing.T, response api.FileResponse) { + assert.False(t, response.Verification.Verified) + })) + }) + + t.Run("Unsigned-Initial-CRUD-Never", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + setting.Repository.Signing.InitialCommit = []string{"never"} + + testCtx := NewAPITestContext(t, username, "initial-unsigned"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + t.Run("CreateCRUDFile-Never", crudActionCreateFile( + t, testCtx, user, "parentsigned", "parentsigned-never", "unsigned-never2.txt", func(t *testing.T, response api.FileResponse) { + assert.False(t, response.Verification.Verified) + })) + }) + + t.Run("Unsigned-Initial-CRUD-Always", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + setting.Repository.Signing.CRUDActions = []string{"always"} + + testCtx := NewAPITestContext(t, username, "initial-unsigned"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + t.Run("CreateCRUDFile-Always", crudActionCreateFile( + t, testCtx, user, "master", "always", "signed-always.txt", func(t *testing.T, response api.FileResponse) { + require.NotNil(t, response.Verification) + assert.True(t, response.Verification.Verified) + assert.Equal(t, "fox@example.com", response.Verification.Signer.Email) + })) + t.Run("CreateCRUDFile-ParentSigned-always", crudActionCreateFile( + t, testCtx, user, "parentsigned", "parentsigned-always", "signed-parent2.txt", func(t *testing.T, response api.FileResponse) { + require.NotNil(t, response.Verification) + assert.True(t, response.Verification.Verified) + assert.Equal(t, "fox@example.com", response.Verification.Signer.Email) + })) + }) + + t.Run("Unsigned-Initial-CRUD-ParentSigned", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + setting.Repository.Signing.CRUDActions = []string{"parentsigned"} + + testCtx := NewAPITestContext(t, username, "initial-unsigned"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + t.Run("CreateCRUDFile-Always-ParentSigned", crudActionCreateFile( + t, testCtx, user, "always", "always-parentsigned", "signed-always-parentsigned.txt", func(t *testing.T, response api.FileResponse) { + require.NotNil(t, response.Verification) + assert.True(t, response.Verification.Verified) + assert.Equal(t, "fox@example.com", response.Verification.Signer.Email) + })) + }) + + t.Run("AlwaysSign-Initial", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + setting.Repository.Signing.InitialCommit = []string{"always"} + + testCtx := NewAPITestContext(t, username, "initial-always"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + t.Run("CreateRepository", doAPICreateRepository(testCtx, false, objectFormat)) + t.Run("CheckMasterBranchSigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) { + require.NotNil(t, branch.Commit) + require.NotNil(t, branch.Commit.Verification) + assert.True(t, branch.Commit.Verification.Verified) + assert.Equal(t, "fox@example.com", branch.Commit.Verification.Signer.Email) + })) + }) + + t.Run("AlwaysSign-Initial-CRUD-Never", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + setting.Repository.Signing.CRUDActions = []string{"never"} + + testCtx := NewAPITestContext(t, username, "initial-always-never"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + t.Run("CreateRepository", doAPICreateRepository(testCtx, false, objectFormat)) + t.Run("CreateCRUDFile-Never", crudActionCreateFile( + t, testCtx, user, "master", "never", "unsigned-never.txt", func(t *testing.T, response api.FileResponse) { + assert.False(t, response.Verification.Verified) + })) + }) + + t.Run("AlwaysSign-Initial-CRUD-ParentSigned-On-Always", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + setting.Repository.Signing.CRUDActions = []string{"parentsigned"} + + testCtx := NewAPITestContext(t, username, "initial-always-parent"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + t.Run("CreateRepository", doAPICreateRepository(testCtx, false, objectFormat)) + t.Run("CreateCRUDFile-ParentSigned", crudActionCreateFile( + t, testCtx, user, "master", "parentsigned", "signed-parent.txt", func(t *testing.T, response api.FileResponse) { + assert.True(t, response.Verification.Verified) + assert.Equal(t, "fox@example.com", response.Verification.Signer.Email) + })) + }) + + t.Run("AlwaysSign-Initial-CRUD-Always", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + setting.Repository.Signing.CRUDActions = []string{"always"} + + testCtx := NewAPITestContext(t, username, "initial-always-always"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + t.Run("CreateRepository", doAPICreateRepository(testCtx, false, objectFormat)) + t.Run("CreateCRUDFile-Always", crudActionCreateFile( + t, testCtx, user, "master", "always", "signed-always.txt", func(t *testing.T, response api.FileResponse) { + assert.True(t, response.Verification.Verified) + assert.Equal(t, "fox@example.com", response.Verification.Signer.Email) + })) + }) + + t.Run("UnsignedMerging", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + setting.Repository.Signing.Merges = []string{"commitssigned"} + + testCtx := NewAPITestContext(t, username, "initial-unsigned"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + t.Run("CreatePullRequest", func(t *testing.T) { + pr, err := doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "never2")(t) + require.NoError(t, err) + t.Run("MergePR", doAPIMergePullRequest(testCtx, testCtx.Username, testCtx.Reponame, pr.Index)) + }) + t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) { + require.NotNil(t, branch.Commit) + require.NotNil(t, branch.Commit.Verification) + assert.False(t, branch.Commit.Verification.Verified) + assert.Empty(t, branch.Commit.Verification.Signature) + })) + }) + + t.Run("BaseSignedMerging", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + setting.Repository.Signing.Merges = []string{"basesigned"} + + testCtx := NewAPITestContext(t, username, "initial-unsigned"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + t.Run("CreatePullRequest", func(t *testing.T) { + pr, err := doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "parentsigned2")(t) + require.NoError(t, err) + t.Run("MergePR", doAPIMergePullRequest(testCtx, testCtx.Username, testCtx.Reponame, pr.Index)) + }) + t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) { + require.NotNil(t, branch.Commit) + require.NotNil(t, branch.Commit.Verification) + assert.False(t, branch.Commit.Verification.Verified) + assert.Empty(t, branch.Commit.Verification.Signature) + })) + }) + + t.Run("CommitsSignedMerging", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() + setting.Repository.Signing.Merges = []string{"commitssigned"} + + testCtx := NewAPITestContext(t, username, "initial-unsigned"+suffix, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + t.Run("CreatePullRequest", func(t *testing.T) { + pr, err := doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "always-parentsigned")(t) + require.NoError(t, err) + t.Run("MergePR", doAPIMergePullRequest(testCtx, testCtx.Username, testCtx.Reponame, pr.Index)) + }) + t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) { + require.NotNil(t, branch.Commit) + require.NotNil(t, branch.Commit.Verification) + assert.True(t, branch.Commit.Verification.Verified) + })) + }) +} + +func crudActionCreateFile(_ *testing.T, ctx APITestContext, user *user_model.User, from, to, path string, callback ...func(*testing.T, api.FileResponse)) func(*testing.T) { + return doAPICreateFile(ctx, path, &api.CreateFileOptions{ + FileOptions: api.FileOptions{ + BranchName: from, + NewBranchName: to, + Message: fmt.Sprintf("from:%s to:%s path:%s", from, to, path), + Author: api.Identity{ + Name: user.FullName, + Email: user.Email, + }, + Committer: api.Identity{ + Name: user.FullName, + Email: user.Email, + }, + }, + ContentBase64: base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("This is new text for %s", path))), + }, callback...) +} + +func importTestingKey() (*openpgp.Entity, error) { + if _, _, err := process.GetManager().Exec("gpg --import tests/integration/private-testing.key", "gpg", "--import", "tests/integration/private-testing.key"); err != nil { + return nil, err + } + keyringFile, err := os.Open("tests/integration/private-testing.key") + if err != nil { + return nil, err + } + defer keyringFile.Close() + + block, err := armor.Decode(keyringFile) + if err != nil { + return nil, err + } + + keyring, err := openpgp.ReadKeyRing(block.Body) + if err != nil { + return nil, fmt.Errorf("Keyring access failed: '%w'", err) + } + + // There should only be one entity in this file. + return keyring[0], nil +} diff --git a/tests/integration/ssh-signing-key b/tests/integration/ssh-signing-key new file mode 100644 index 0000000000..e67df477c5 --- /dev/null +++ b/tests/integration/ssh-signing-key @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACBXkQvBnxcl7YstH9RO4S7++wV1u3uvLRbyHLPP9vWKhwAAAJhlmhmkZZoZ +pAAAAAtzc2gtZWQyNTUxOQAAACBXkQvBnxcl7YstH9RO4S7++wV1u3uvLRbyHLPP9vWKhw +AAAEDnOTuE2rDECN+2OsuUbQgGrMSY22tn+IF5JG5nuyJinVeRC8GfFyXtiy0f1E7hLv77 +BXW7e68tFvIcs8/29YqHAAAAE2d1c3RlZEBndXN0ZWQtYmVhc3QBAg== +-----END OPENSSH PRIVATE KEY----- diff --git a/tests/integration/ssh-signing-key.pub b/tests/integration/ssh-signing-key.pub new file mode 100644 index 0000000000..3d1ab60b47 --- /dev/null +++ b/tests/integration/ssh-signing-key.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFeRC8GfFyXtiy0f1E7hLv77BXW7e68tFvIcs8/29YqH