summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile4
-rw-r--r--custom/conf/app.ini.sample31
-rw-r--r--docs/content/doc/advanced/config-cheat-sheet.en-us.md19
-rw-r--r--docs/content/doc/advanced/signing.en-us.md162
-rw-r--r--integrations/api_helper_for_declarative_test.go35
-rw-r--r--integrations/api_repo_file_create_test.go2
-rw-r--r--integrations/api_repo_file_update_test.go2
-rw-r--r--integrations/gpg_git_test.go252
-rw-r--r--integrations/mssql.ini.tmpl3
-rw-r--r--integrations/mysql.ini.tmpl3
-rw-r--r--integrations/mysql8.ini.tmpl3
-rw-r--r--integrations/pgsql.ini.tmpl3
-rw-r--r--integrations/repofiles_delete_test.go2
-rw-r--r--integrations/repofiles_update_test.go4
-rw-r--r--integrations/sqlite.ini3
-rw-r--r--models/gpg_key.go359
-rw-r--r--models/repo.go60
-rw-r--r--models/repo_sign.go303
-rw-r--r--models/wiki.go21
-rw-r--r--modules/git/commit.go8
-rw-r--r--modules/git/repo.go10
-rw-r--r--modules/git/repo_gpg.go59
-rw-r--r--modules/git/repo_tree.go11
-rw-r--r--modules/git/utils.go28
-rw-r--r--modules/repofiles/file_test.go2
-rw-r--r--modules/repofiles/temp_repo.go13
-rw-r--r--modules/repofiles/verification.go14
-rw-r--r--modules/setting/repository.go29
-rw-r--r--modules/structs/hook.go9
-rw-r--r--options/locale/locale_en-US.ini3
-rw-r--r--public/css/index.css6
-rw-r--r--public/less/_base.less16
-rw-r--r--public/less/_repository.less9
-rw-r--r--routers/api/v1/api.go2
-rw-r--r--routers/api/v1/convert/convert.go21
-rw-r--r--routers/api/v1/misc/signing.go62
-rw-r--r--services/pull/merge.go64
-rw-r--r--templates/repo/commit_page.tmpl39
-rw-r--r--templates/repo/commits_table.tmpl13
-rw-r--r--templates/swagger/v1_json.tmpl59
40 files changed, 1627 insertions, 121 deletions
diff --git a/Makefile b/Makefile
index 953abe83b0..ebcfadb21d 100644
--- a/Makefile
+++ b/Makefile
@@ -168,6 +168,10 @@ fmt-check:
test:
GO111MODULE=on $(GO) test -mod=vendor -tags='sqlite sqlite_unlock_notify' $(PACKAGES)
+.PHONY: test\#%
+test\#%:
+ GO111MODULE=on $(GO) test -mod=vendor -tags='sqlite sqlite_unlock_notify' -run $* $(PACKAGES)
+
.PHONY: coverage
coverage:
@hash gocovmerge > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
diff --git a/custom/conf/app.ini.sample b/custom/conf/app.ini.sample
index aa526804f2..e4e791d4a7 100644
--- a/custom/conf/app.ini.sample
+++ b/custom/conf/app.ini.sample
@@ -74,6 +74,37 @@ WORK_IN_PROGRESS_PREFIXES=WIP:,[WIP]
; List of reasons why a Pull Request or Issue can be locked
LOCK_REASONS=Too heated,Off-topic,Resolved,Spam
+[repository.signing]
+; 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
+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.
+; 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 overrided
+; by setting the SIGNING_KEY ID to the correct ID.)
+SIGNING_NAME =
+SIGNING_EMAIL =
+; Determines when gitea should sign the initial commit when creating a repository
+; Either:
+; - never
+; - pubkey: only sign if the user has a pubkey
+; - twofa: only sign if the user has logged in with twofa
+; - always
+; options other than none and always can be combined as comma separated list
+INITIAL_COMMIT = always
+; Determines when to sign for CRUD actions
+; - as above
+; - parentsigned: requires that the parent commit is signed.
+CRUD_ACTIONS = pubkey, twofa, parentsigned
+; Determines when to sign Wiki commits
+; - as above
+WIKI = never
+; Determines when to sign on merges
+; - basesigned: require that the parent of commit on the base repo is signed.
+; - commitssigned: require that all the commits in the head branch are signed.
+MERGES = pubkey, twofa, basesigned, commitssigned
+
[cors]
; More information about CORS can be found here: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#The_HTTP_response_headers
; enable cors headers (disabled by default)
diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
index 959607dd11..0df88c23e8 100644
--- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md
+++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
@@ -76,6 +76,25 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
- `LOCK_REASONS`: **Too heated,Off-topic,Resolved,Spam**: A list of reasons why a Pull Request or Issue can be locked
+### Repository - Signing (`repository.signing`)
+
+- `SIGNING_KEY`: **default**: \[none, KEYID, default \]: Key to sign with.
+- `SIGNING_NAME` & `SIGNING_EMAIL`: if a KEYID is provided as the `SIGNING_KEY`, use these as the Name and Email address of the signer. These should match publicized name and email address for the key.
+- `INITIAL_COMMIT`: **always**: \[never, pubkey, twofa, always\]: Sign initial commit.
+ - `never`: Never sign
+ - `pubkey`: Only sign if the user has a public key
+ - `twofa`: Only sign if the user is logged in with twofa
+ - `always`: Always sign
+ - Options other than `never` and `always` can be combined as a comma separated list.
+- `WIKI`: **never**: \[never, pubkey, twofa, always, parentsigned\]: Sign commits to wiki.
+- `CRUD_ACTIONS`: **pubkey, twofa, parentsigned**: \[never, pubkey, twofa, parentsigned, always\]: Sign CRUD actions.
+ - Options as above, with the addition of:
+ - `parentsigned`: Only sign if the parent commit is signed.
+- `MERGES`: **pubkey, twofa, basesigned, commitssigned**: \[never, pubkey, twofa, basesigned, commitssigned, always\]: Sign merges.
+ - `basesigned`: Only sign if the parent commit in the base repo is signed.
+ - `headsigned`: Only sign if the head commit in the head branch is signed.
+ - `commitssigned`: Only sign if all the commits in the head branch to the merge point are signed.
+
## CORS (`cors`)
- `ENABLED`: **false**: enable cors headers (disabled by default)
diff --git a/docs/content/doc/advanced/signing.en-us.md b/docs/content/doc/advanced/signing.en-us.md
new file mode 100644
index 0000000000..b6c99e269e
--- /dev/null
+++ b/docs/content/doc/advanced/signing.en-us.md
@@ -0,0 +1,162 @@
+---
+date: "2019-08-17T10:20:00+01:00"
+title: "GPG Commit Signatures"
+slug: "signing"
+weight: 20
+toc: false
+draft: false
+menu:
+ sidebar:
+ parent: "advanced"
+ name: "GPG Commit Signatures"
+ weight: 20
+ identifier: "signing"
+---
+
+# GPG Commit Signatures
+
+Gitea will verify GPG commit signatures in the provided tree by
+checking if the commits are signed by a key within the gitea database,
+or if the commit matches the default key for git.
+
+Keys are not checked to determine if they have expired or revoked.
+Keys are also not checked with keyservers.
+
+A commit will be marked with a grey unlocked icon if no key can be
+found to verify it. If a commit is marked with a red unlocked icon,
+it is reported to be signed with a key with an id.
+
+Please note: The signer of a commit does not have to be an author or
+committer of a commit.
+
+This functionality requires git >= 1.7.9 but for full functionality
+this requires git >= 2.0.0.
+
+## Automatic Signing
+
+There are a number of places where Gitea will generate commits itself:
+
+* Repository Initialisation
+* Wiki Changes
+* CRUD actions using the editor or the API
+* Merges from Pull Requests
+
+Depending on configuration and server trust you may want Gitea to
+sign these commits.
+
+## General Configuration
+
+Gitea's configuration for signing can be found with the
+`[repository.signing]` section of `app.ini`:
+
+```ini
+...
+[repository.signing]
+SIGNING_KEY = default
+SIGNING_NAME =
+SIGNING_EMAIL =
+INITIAL_COMMIT = always
+CRUD_ACTIONS = pubkey, twofa, parentsigned
+WIKI = never
+MERGES = pubkey, twofa, basesigned, commitssigned
+
+...
+```
+
+### `SIGNING_KEY`
+
+The first option to discuss is the `SIGNING_KEY`. There are three main
+options:
+
+* `none` - this prevents Gitea from signing any commits
+* `default` - Gitea will default to the key configured within
+`git config`
+* `KEYID` - Gitea will sign commits with the gpg key with the ID
+`KEYID`. In this case you should provide a `SIGNING_NAME` and
+`SIGNING_EMAIL` to be displayed for this key.
+
+The `default` option will interrogate `git config` for
+`commit.gpgsign` option - if this is set, then it will use the results
+of the `user.signingkey`, `user.name` and `user.email` as appropriate.
+
+Please note: by adjusting git's `config` file within Gitea's
+repositories, `SIGNING_KEY=default` could be used to provide different
+signing keys on a per-repository basis. However, this is cleary not an
+ideal UI and therefore subject to change.
+
+### `INITIAL_COMMIT`
+
+This option determines whether Gitea should sign the initial commit
+when creating a repository. The possible values are:
+
+* `never`: Never sign
+* `pubkey`: Only sign if the user has a public key
+* `twofa`: Only sign if the user logs in with two factor authentication
+* `always`: Always sign
+
+Options other than `never` and `always` can be combined as a comma
+separated list.
+
+### `WIKI`
+
+This options determines if Gitea should sign commits to the Wiki.
+The possible values are:
+
+* `never`: Never sign
+* `pubkey`: Only sign if the user has a public key
+* `twofa`: Only sign if the user logs in with two factor authentication
+* `parentsigned`: Only sign if the parent commit is signed.
+* `always`: Always sign
+
+Options other than `never` and `always` can be combined as a comma
+separated list.
+
+### `CRUD_ACTIONS`
+
+This option determines if Gitea should sign commits from the web
+editor or API CRUD actions. The possible values are:
+
+* `never`: Never sign
+* `pubkey`: Only sign if the user has a public key
+* `twofa`: Only sign if the user logs in with two factor authentication
+* `parentsigned`: Only sign if the parent commit is signed.
+* `always`: Always sign
+
+Options other than `never` and `always` can be combined as a comma
+separated list.
+
+### `MERGES`
+
+This option determines if Gitea should sign merge commits from PRs.
+The possible options are:
+
+* `never`: Never sign
+* `pubkey`: Only sign if the user has a public key
+* `twofa`: Only sign if the user logs in with two factor authentication
+* `basesigned`: Only sign if the parent commit in the base repo is signed.
+* `headsigned`: Only sign if the head commit in the head branch is signed.
+* `commitssigned`: Only sign if all the commits in the head branch to the merge point are signed.
+* `always`: Always sign
+
+Options other than `never` and `always` can be combined as a comma
+separated list.
+
+## Installing and generating a GPG key for Gitea
+
+It is up to a server administrator to determine how best to install
+a signing key. Gitea generates all its commits using the server `git`
+command at present - and therefore the server `gpg` will be used for
+signing (if configured.) Administrators should review best-practices
+for gpg - in particular it is probably advisable to only install a
+signing secret subkey without the master signing and certifying secret
+key.
+
+## Obtaining the Public Key of the Signing Key
+
+The public key used to sign Gitea's commits can be obtained from the API at:
+
+```/api/v1/signing-key.gpg```
+
+In cases where there is a repository specific key this can be obtained from:
+
+```/api/v1/repos/:username/:reponame/signing-key.gpg```
diff --git a/integrations/api_helper_for_declarative_test.go b/integrations/api_helper_for_declarative_test.go
index 805a986ae3..cae7691c4b 100644
--- a/integrations/api_helper_for_declarative_test.go
+++ b/integrations/api_helper_for_declarative_test.go
@@ -231,3 +231,38 @@ func doAPIMergePullRequest(ctx APITestContext, owner, repo string, index int64)
ctx.Session.MakeRequest(t, req, 200)
}
}
+
+func doAPIGetBranch(ctx APITestContext, branch string, callback ...func(*testing.T, api.Branch)) func(*testing.T) {
+ return func(t *testing.T) {
+ req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/branches/%s?token=%s", ctx.Username, ctx.Reponame, branch, ctx.Token)
+ if ctx.ExpectedCode != 0 {
+ ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
+ return
+ }
+ resp := ctx.Session.MakeRequest(t, req, http.StatusOK)
+
+ var branch api.Branch
+ DecodeJSON(t, resp, &branch)
+ if len(callback) > 0 {
+ callback[0](t, branch)
+ }
+ }
+}
+
+func doAPICreateFile(ctx APITestContext, treepath string, options *api.CreateFileOptions, callback ...func(*testing.T, api.FileResponse)) func(*testing.T) {
+ return func(t *testing.T) {
+ url := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", ctx.Username, ctx.Reponame, treepath, ctx.Token)
+ req := NewRequestWithJSON(t, "POST", url, &options)
+ if ctx.ExpectedCode != 0 {
+ ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
+ return
+ }
+ resp := ctx.Session.MakeRequest(t, req, http.StatusCreated)
+
+ var contents api.FileResponse
+ DecodeJSON(t, resp, &contents)
+ if len(callback) > 0 {
+ callback[0](t, contents)
+ }
+ }
+}
diff --git a/integrations/api_repo_file_create_test.go b/integrations/api_repo_file_create_test.go
index 42898bf259..4d76ff00ce 100644
--- a/integrations/api_repo_file_create_test.go
+++ b/integrations/api_repo_file_create_test.go
@@ -91,7 +91,7 @@ func getExpectedFileResponseForCreate(commitID, treePath string) *api.FileRespon
},
Verification: &api.PayloadCommitVerification{
Verified: false,
- Reason: "unsigned",
+ Reason: "gpg.error.not_signed_commit",
Signature: "",
Payload: "",
},
diff --git a/integrations/api_repo_file_update_test.go b/integrations/api_repo_file_update_test.go
index 366eb5e918..bf695d4344 100644
--- a/integrations/api_repo_file_update_test.go
+++ b/integrations/api_repo_file_update_test.go
@@ -94,7 +94,7 @@ func getExpectedFileResponseForUpdate(commitID, treePath string) *api.FileRespon
},
Verification: &api.PayloadCommitVerification{
Verified: false,
- Reason: "unsigned",
+ Reason: "gpg.error.not_signed_commit",
Signature: "",
Payload: "",
},
diff --git a/integrations/gpg_git_test.go b/integrations/gpg_git_test.go
new file mode 100644
index 0000000000..12f0a138c7
--- /dev/null
+++ b/integrations/gpg_git_test.go
@@ -0,0 +1,252 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integrations
+
+import (
+ "encoding/base64"
+ "fmt"
+ "io/ioutil"
+ "net/url"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/modules/process"
+ "code.gitea.io/gitea/modules/setting"
+ api "code.gitea.io/gitea/modules/structs"
+ "github.com/stretchr/testify/assert"
+ "golang.org/x/crypto/openpgp"
+ "golang.org/x/crypto/openpgp/armor"
+)
+
+func TestGPGGit(t *testing.T) {
+ onGiteaRun(t, testGPGGit)
+}
+
+func testGPGGit(t *testing.T, u *url.URL) {
+ username := "user2"
+ baseAPITestContext := NewAPITestContext(t, username, "repo1")
+
+ u.Path = baseAPITestContext.GitPath()
+
+ // OK Set a new GPG home
+ tmpDir, err := ioutil.TempDir("", "temp-gpg")
+ assert.NoError(t, err)
+ defer os.RemoveAll(tmpDir)
+
+ err = os.Chmod(tmpDir, 0700)
+ assert.NoError(t, err)
+
+ oldGNUPGHome := os.Getenv("GNUPGHOME")
+ err = os.Setenv("GNUPGHOME", tmpDir)
+ assert.NoError(t, err)
+ defer os.Setenv("GNUPGHOME", oldGNUPGHome)
+
+ // Need to create a root key
+ rootKeyPair, err := createGPGKey(tmpDir, "gitea", "gitea@fake.local")
+ assert.NoError(t, err)
+
+ rootKeyID := rootKeyPair.PrimaryKey.KeyIdShortString()
+
+ oldKeyID := setting.Repository.Signing.SigningKey
+ oldName := setting.Repository.Signing.SigningName
+ oldEmail := setting.Repository.Signing.SigningEmail
+ defer func() {
+ setting.Repository.Signing.SigningKey = oldKeyID
+ setting.Repository.Signing.SigningName = oldName
+ setting.Repository.Signing.SigningEmail = oldEmail
+ }()
+
+ setting.Repository.Signing.SigningKey = rootKeyID
+ setting.Repository.Signing.SigningName = "gitea"
+ setting.Repository.Signing.SigningEmail = "gitea@fake.local"
+ user := models.AssertExistsAndLoadBean(t, &models.User{Name: username}).(*models.User)
+
+ t.Run("Unsigned-Initial", func(t *testing.T) {
+ PrintCurrentTest(t)
+ setting.Repository.Signing.InitialCommit = []string{"never"}
+ testCtx := NewAPITestContext(t, username, "initial-unsigned")
+ t.Run("CreateRepository", doAPICreateRepository(testCtx, false))
+ 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.CRUDActions = []string{"never"}
+ 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("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("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("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, "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.True(t, response.Verification.Verified)
+ assert.Equal(t, "gitea@fake.local", response.Verification.Signer.Email)
+ }))
+ setting.Repository.Signing.CRUDActions = []string{"parentsigned"}
+ t.Run("CreateCRUDFile-Always-ParentSigned", crudActionCreateFile(
+ t, testCtx, user, "always", "always-parentsigned", "signed-always-parentsigned.txt", func(t *testing.T, response api.FileResponse) {
+ assert.True(t, response.Verification.Verified)
+ assert.Equal(t, "gitea@fake.local", response.Verification.Signer.Email)
+ }))
+ })
+ t.Run("AlwaysSign-Initial", func(t *testing.T) {
+ PrintCurrentTest(t)
+ setting.Repository.Signing.InitialCommit = []string{"always"}
+ testCtx := NewAPITestContext(t, username, "initial-always")
+ t.Run("CreateRepository", doAPICreateRepository(testCtx, false))
+ t.Run("CheckMasterBranchSigned", 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)
+ assert.Equal(t, "gitea@fake.local", branch.Commit.Verification.Signer.Email)
+ }))
+ setting.Repository.Signing.CRUDActions = []string{"never"}
+ 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("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, "gitea@fake.local", response.Verification.Signer.Email)
+ }))
+ setting.Repository.Signing.CRUDActions = []string{"always"}
+ 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, "gitea@fake.local", response.Verification.Signer.Email)
+ }))
+
+ })
+ t.Run("UnsignedMerging", func(t *testing.T) {
+ PrintCurrentTest(t)
+ testCtx := NewAPITestContext(t, username, "initial-unsigned")
+ var pr api.PullRequest
+ var err error
+ t.Run("CreatePullRequest", func(t *testing.T) {
+ pr, err = doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "never2")(t)
+ assert.NoError(t, err)
+ })
+ setting.Repository.Signing.Merges = []string{"commitssigned"}
+ 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("CreatePullRequest", func(t *testing.T) {
+ pr, err = doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "parentsigned2")(t)
+ assert.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("CreatePullRequest", func(t *testing.T) {
+ pr, err = doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "always-parentsigned")(t)
+ assert.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(t *testing.T, ctx APITestContext, user *models.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,
+ },
+ },
+ Content: base64.StdEncoding.EncodeToString([]byte("This is new text")),
+ }, callback...)
+}
+
+func createGPGKey(tmpDir, name, email string) (*openpgp.Entity, error) {
+ keyPair, err := openpgp.NewEntity(name, "test", email, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, id := range keyPair.Identities {
+ err := id.SelfSignature.SignUserId(id.UserId.Id, keyPair.PrimaryKey, keyPair.PrivateKey, nil)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ keyFile := filepath.Join(tmpDir, "temporary.key")
+ keyWriter, err := os.Create(keyFile)
+ if err != nil {
+ return nil, err
+ }
+ defer keyWriter.Close()
+ defer os.Remove(keyFile)
+
+ w, err := armor.Encode(keyWriter, openpgp.PrivateKeyType, nil)
+ if err != nil {
+ return nil, err
+ }
+ defer w.Close()
+
+ keyPair.SerializePrivate(w, nil)
+ if err := w.Close(); err != nil {
+ return nil, err
+ }
+ if err := keyWriter.Close(); err != nil {
+ return nil, err
+ }
+
+ if _, _, err := process.GetManager().Exec("gpg --import temporary.key", "gpg", "--import", keyFile); err != nil {
+ return nil, err
+ }
+ return keyPair, nil
+}
diff --git a/integrations/mssql.ini.tmpl b/integrations/mssql.ini.tmpl
index d38d038a4e..931e923cf4 100644
--- a/integrations/mssql.ini.tmpl
+++ b/integrations/mssql.ini.tmpl
@@ -21,6 +21,9 @@ ROOT = integrations/gitea-integration-mssql/gitea-repositories
LOCAL_COPY_PATH = tmp/local-repo-mssql
LOCAL_WIKI_PATH = tmp/local-wiki-mssql
+[repository.signing]
+SIGNING_KEY = none
+
[server]
SSH_DOMAIN = localhost
HTTP_PORT = 3003
diff --git a/integrations/mysql.ini.tmpl b/integrations/mysql.ini.tmpl
index 6eed7e1578..4dde212798 100644
--- a/integrations/mysql.ini.tmpl
+++ b/integrations/mysql.ini.tmpl
@@ -21,6 +21,9 @@ ROOT = integrations/gitea-integration-mysql/gitea-repositories
LOCAL_COPY_PATH = tmp/local-repo-mysql
LOCAL_WIKI_PATH = tmp/local-wiki-mysql
+[repository.signing]
+SIGNING_KEY = none
+
[server]
SSH_DOMAIN = localhost
HTTP_PORT = 3001
diff --git a/integrations/mysql8.ini.tmpl b/integrations/mysql8.ini.tmpl
index 1e14bc1356..1b1d3d2436 100644
--- a/integrations/mysql8.ini.tmpl
+++ b/integrations/mysql8.ini.tmpl
@@ -21,6 +21,9 @@ ROOT = integrations/gitea-integration-mysql8/gitea-repositories
LOCAL_COPY_PATH = tmp/local-repo-mysql8
LOCAL_WIKI_PATH = tmp/local-wiki-mysql8
+[repository.signing]
+SIGNING_KEY = none
+
[server]
SSH_DOMAIN = localhost
HTTP_PORT = 3004
diff --git a/integrations/pgsql.ini.tmpl b/integrations/pgsql.ini.tmpl
index cd5dc44ea8..6265e0d98e 100644
--- a/integrations/pgsql.ini.tmpl
+++ b/integrations/pgsql.ini.tmpl
@@ -21,6 +21,9 @@ ROOT = integrations/gitea-integration-pgsql/gitea-repositories
LOCAL_COPY_PATH = tmp/local-repo-pgsql
LOCAL_WIKI_PATH = tmp/local-wiki-pgsql
+[repository.signing]
+SIGNING_KEY = none
+
[server]
SSH_DOMAIN = localhost
HTTP_PORT = 3002
diff --git a/integrations/repofiles_delete_test.go b/integrations/repofiles_delete_test.go
index f4cb4510be..b4c535188b 100644
--- a/integrations/repofiles_delete_test.go
+++ b/integrations/repofiles_delete_test.go
@@ -53,7 +53,7 @@ func getExpectedDeleteFileResponse(u *url.URL) *api.FileResponse {
},
Verification: &api.PayloadCommitVerification{
Verified: false,
- Reason: "",
+ Reason: "gpg.error.not_signed_commit",
Signature: "",
Payload: "",
},
diff --git a/integrations/repofiles_update_test.go b/integrations/repofiles_update_test.go
index a4ce16d847..c475c70008 100644
--- a/integrations/repofiles_update_test.go
+++ b/integrations/repofiles_update_test.go
@@ -108,7 +108,7 @@ func getExpectedFileResponseForRepofilesCreate(commitID string) *api.FileRespons
},
Verification: &api.PayloadCommitVerification{
Verified: false,
- Reason: "unsigned",
+ Reason: "gpg.error.not_signed_commit",
Signature: "",
Payload: "",
},
@@ -175,7 +175,7 @@ func getExpectedFileResponseForRepofilesUpdate(commitID, filename string) *api.F
},
Verification: &api.PayloadCommitVerification{
Verified: false,
- Reason: "unsigned",
+ Reason: "gpg.error.not_signed_commit",
Signature: "",
Payload: "",
},
diff --git a/integrations/sqlite.ini b/integrations/sqlite.ini
index b188406ee9..de3355c166 100644
--- a/integrations/sqlite.ini
+++ b/integrations/sqlite.ini
@@ -17,6 +17,9 @@ ROOT = integrations/gitea-integration-sqlite/gitea-repositories
LOCAL_COPY_PATH = tmp/local-repo-sqlite
LOCAL_WIKI_PATH = tmp/local-wiki-sqlite
+[repository.signing]
+SIGNING_KEY = none
+
[server]
SSH_DOMAIN = localhost
HTTP_PORT = 3003
diff --git a/models/gpg_key.go b/models/gpg_key.go
index 72c6891d4d..5cfe67435e 100644
--- a/models/gpg_key.go
+++ b/models/gpg_key.go
@@ -17,6 +17,7 @@ import (
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/timeutil"
"github.com/go-xorm/xorm"
@@ -80,6 +81,12 @@ func GetGPGKeyByID(keyID int64) (*GPGKey, error) {
return key, nil
}
+// GetGPGKeysByKeyID returns public key by given ID.
+func GetGPGKeysByKeyID(keyID string) ([]*GPGKey, error) {
+ keys := make([]*GPGKey, 0, 1)
+ return keys, x.Where("key_id=?", keyID).Find(&keys)
+}
+
// GetGPGImportByKeyID returns the import public armored key by given KeyID.
func GetGPGImportByKeyID(keyID string) (*GPGKeyImport, error) {
key := new(GPGKeyImport)
@@ -355,10 +362,13 @@ func DeleteGPGKey(doer *User, id int64) (err error) {
// CommitVerification represents a commit validation of signature
type CommitVerification struct {
- Verified bool
- Reason string
- SigningUser *User
- SigningKey *GPGKey
+ Verified bool
+ Warning bool
+ Reason string
+ SigningUser *User
+ CommittingUser *User
+ SigningEmail string
+ SigningKey *GPGKey
}
// SignCommit represents a commit with validation of signature.
@@ -367,6 +377,17 @@ type SignCommit struct {
*UserCommit
}
+const (
+ // BadSignature is used as the reason when the signature has a KeyID that is in the db
+ // but no key that has that ID verifies the signature. This is a suspicious failure.
+ BadSignature = "gpg.error.probable_bad_signature"
+ // BadDefaultSignature is used as the reason when the signature has a KeyID that matches the
+ // default Key but is not verified by the default key. This is a suspicious failure.
+ BadDefaultSignature = "gpg.error.probable_bad_default_signature"
+ // NoKeyFound is used as the reason when no key can be found to verify the signature.
+ NoKeyFound = "gpg.error.no_gpg_keys_found"
+)
+
func readerFromBase64(s string) (io.Reader, error) {
bs, err := base64.StdEncoding.DecodeString(s)
if err != nil {
@@ -424,49 +445,207 @@ func verifySign(s *packet.Signature, h hash.Hash, k *GPGKey) error {
return pkey.VerifySignature(h, s)
}
-// ParseCommitWithSignature check if signature is good against keystore.
-func ParseCommitWithSignature(c *git.Commit) *CommitVerification {
- if c.Signature != nil && c.Committer != nil {
- //Parsing signature
- sig, err := extractSignature(c.Signature.Signature)
- if err != nil { //Skipping failed to extract sign
- log.Error("SignatureRead err: %v", err)
- return &CommitVerification{
- Verified: false,
- Reason: "gpg.error.extract_sign",
+func hashAndVerify(sig *packet.Signature, payload string, k *GPGKey, committer, signer *User, email string) *CommitVerification {
+ //Generating hash of commit
+ hash, err := populateHash(sig.Hash, []byte(payload))
+ if err != nil { //Skipping failed to generate hash
+ log.Error("PopulateHash: %v", err)
+ return &CommitVerification{
+ CommittingUser: committer,
+ Verified: false,
+ Reason: "gpg.error.generate_hash",
+ }
+ }
+
+ if err := verifySign(sig, hash, k); err == nil {
+ return &CommitVerification{ //Everything is ok
+ CommittingUser: committer,
+ Verified: true,
+ Reason: fmt.Sprintf("%s <%s> / %s", signer.Name, signer.Email, k.KeyID),
+ SigningUser: signer,
+ SigningKey: k,
+ SigningEmail: email,
+ }
+ }
+ return nil
+}
+
+func hashAndVerifyWithSubKeys(sig *packet.Signature, payload string, k *GPGKey, committer, signer *User, email string) *CommitVerification {
+ commitVerification := hashAndVerify(sig, payload, k, committer, signer, email)
+ if commitVerification != nil {
+ return commitVerification
+ }
+
+ //And test also SubsKey
+ for _, sk := range k.SubsKey {
+ commitVerification := hashAndVerify(sig, payload, sk, committer, signer, email)
+ if commitVerification != nil {
+ return commitVerification
+ }
+ }
+ return nil
+}
+
+func hashAndVerifyForKeyID(sig *packet.Signature, payload string, committer *User, keyID, name, email string) *CommitVerification {
+ if keyID == "" {
+ return nil
+ }
+ keys, err := GetGPGKeysByKeyID(keyID)
+ if err != nil {
+ log.Error("GetGPGKeysByKeyID: %v", err)
+ return &CommitVerification{
+ CommittingUser: committer,
+ Verified: false,
+ Reason: "gpg.error.failed_retrieval_gpg_keys",
+ }
+ }
+ if len(keys) == 0 {
+ return nil
+ }
+ for _, key := range keys {
+ activated := false
+ if len(email) != 0 {
+ for _, e := range key.Emails {
+ if e.IsActivated && strings.EqualFold(e.Email, email) {
+ activated = true
+ email = e.Email
+ break
+ }
+ }
+ } else {
+ for _, e := range key.Emails {
+ if e.IsActivated {
+ activated = true
+ email = e.Email
+ break
+ }
+ }
+ }
+ if !activated {
+ continue
+ }
+ signer := &User{
+ Name: name,
+ Email: email,
+ }
+ if key.OwnerID != 0 {
+ owner, err := GetUserByID(key.OwnerID)
+ if err == nil {
+ signer = owner
+ } else if !IsErrUserNotExist(err) {
+ log.Error("Failed to GetUserByID: %d for key ID: %d (%s) %v", key.OwnerID, key.ID, key.KeyID, err)
+ return &CommitVerification{
+ CommittingUser: committer,
+ Verified: false,
+ Reason: "gpg.error.no_committer_account",
+ }
}
}
+ commitVerification := hashAndVerifyWithSubKeys(sig, payload, key, committer, signer, email)
+ if commitVerification != nil {
+ return commitVerification
+ }
+ }
+ // This is a bad situation ... We have a key id that is in our database but the signature doesn't match.
+ return &CommitVerification{
+ CommittingUser: committer,
+ Verified: false,
+ Warning: true,
+ Reason: BadSignature,
+ }
+}
+// ParseCommitWithSignature check if signature is good against keystore.
+func ParseCommitWithSignature(c *git.Commit) *CommitVerification {
+ var committer *User
+ if c.Committer != nil {
+ var err error
//Find Committer account
- committer, err := GetUserByEmail(c.Committer.Email) //This find the user by primary email or activated email so commit will not be valid if email is not
- if err != nil { //Skipping not user for commiter
+ committer, err = GetUserByEmail(c.Committer.Email) //This finds the user by primary email or activated email so commit will not be valid if email is not
+ if err != nil { //Skipping not user for commiter
+ committer = &User{
+ Name: c.Committer.Name,
+ Email: c.Committer.Email,
+ }
// We can expect this to often be an ErrUserNotExist. in the case
// it is not, however, it is important to log it.
if !IsErrUserNotExist(err) {
log.Error("GetUserByEmail: %v", err)
+ return &CommitVerification{
+ CommittingUser: committer,
+ Verified: false,
+ Reason: "gpg.error.no_committer_account",
+ }
}
- return &CommitVerification{
- Verified: false,
- Reason: "gpg.error.no_committer_account",
- }
+
+ }
+ }
+
+ // If no signature just report the committer
+ if c.Signature == nil {
+ return &CommitVerification{
+ CommittingUser: committer,
+ Verified: false, //Default value
+ Reason: "gpg.error.not_signed_commit", //Default value
+ }
+ }
+
+ //Parsing signature
+ sig, err := extractSignature(c.Signature.Signature)
+ if err != nil { //Skipping failed to extract sign
+ log.Error("SignatureRead err: %v", err)
+ return &CommitVerification{
+ CommittingUser: committer,
+ Verified: false,
+ Reason: "gpg.error.extract_sign",
+ }
+ }
+
+ keyID := ""
+ if sig.IssuerKeyId != nil && (*sig.IssuerKeyId) != 0 {
+ keyID = fmt.Sprintf("%X", *sig.IssuerKeyId)
+ }
+ if keyID == "" && sig.IssuerFingerprint != nil && len(sig.IssuerFingerprint) > 0 {
+ keyID = fmt.Sprintf("%X", sig.IssuerFingerprint[12:20])
+ }
+
+ defaultReason := NoKeyFound
+
+ // First check if the sig has a keyID and if so just look at that
+ if commitVerification := hashAndVerifyForKeyID(
+ sig,
+ c.Signature.Payload,
+ committer,
+ keyID,
+ setting.AppName,
+ ""); commitVerification != nil {
+ if commitVerification.Reason == BadSignature {
+ defaultReason = BadSignature
+ } else {
+ return commitVerification
}
+ }
+ // Now try to associate the signature with the committer, if present
+ if committer.ID != 0 {
keys, err := ListGPGKeys(committer.ID)
if err != nil { //Skipping failed to get gpg keys of user
log.Error("ListGPGKeys: %v", err)
return &CommitVerification{
- Verified: false,
- Reason: "gpg.error.failed_retrieval_gpg_keys",
+ CommittingUser: committer,
+ Verified: false,
+ Reason: "gpg.error.failed_retrieval_gpg_keys",
}
}
for _, k := range keys {
//Pre-check (& optimization) that emails attached to key can be attached to the commiter email and can validate
canValidate := false
- lowerCommiterEmail := strings.ToLower(c.Committer.Email)
+ email := ""
for _, e := range k.Emails {
- if e.IsActivated && strings.ToLower(e.Email) == lowerCommiterEmail {
+ if e.IsActivated && strings.EqualFold(e.Email, c.Committer.Email) {
canValidate = true
+ email = e.Email
break
}
}
@@ -474,56 +653,102 @@ func ParseCommitWithSignature(c *git.Commit) *CommitVerification {
continue //Skip this key
}
- //Generating hash of commit
- hash, err := populateHash(sig.Hash, []byte(c.Signature.Payload))
- if err != nil { //Skipping ailed to generate hash
- log.Error("PopulateHash: %v", err)
- return &CommitVerification{
- Verified: false,
- Reason: "gpg.error.generate_hash",
- }
- }
- //We get PK
- if err := verifySign(sig, hash, k); err == nil {
- return &CommitVerification{ //Everything is ok
- Verified: true,
- Reason: fmt.Sprintf("%s <%s> / %s", c.Committer.Name, c.Committer.Email, k.KeyID),
- SigningUser: committer,
- SigningKey: k,
- }
+ commitVerification := hashAndVerifyWithSubKeys(sig, c.Signature.Payload, k, committer, committer, email)
+ if commitVerification != nil {
+ return commitVerification
}
- //And test also SubsKey
- for _, sk := range k.SubsKey {
-
- //Generating hash of commit
- hash, err := populateHash(sig.Hash, []byte(c.Signature.Payload))
- if err != nil { //Skipping ailed to generate hash
- log.Error("PopulateHash: %v", err)
- return &CommitVerification{
- Verified: false,
- Reason: "gpg.error.generate_hash",
- }
- }
- if err := verifySign(sig, hash, sk); err == nil {
- return &CommitVerification{ //Everything is ok
- Verified: true,
- Reason: fmt.Sprintf("%s <%s> / %s", c.Committer.Name, c.Committer.Email, sk.KeyID),
- SigningUser: committer,
- SigningKey: sk,
- }
- }
+ }
+ }
+
+ if setting.Repository.Signing.SigningKey != "" && setting.Repository.Signing.SigningKey != "default" && setting.Repository.Signing.SigningKey != "none" {
+ // OK we should try the default key
+ gpgSettings := git.GPGSettings{
+ Sign: true,
+ KeyID: setting.Repository.Signing.SigningKey,
+ Name: setting.Repository.Signing.SigningName,
+ Email: setting.Repository.Signing.SigningEmail,
+ }
+ if err := gpgSettings.LoadPublicKeyContent(); err != nil {
+ log.Error("Error getting default signing key: %s %v", gpgSettings.KeyID, err)
+ } else if commitVerification := verifyWithGPGSettings(&gpgSettings, sig, c.Signature.Payload, committer, keyID); commitVerification != nil {
+ if commitVerification.Reason == BadSignature {
+ defaultReason = BadSignature
+ } else {
+ return commitVerification
}
}
- return &CommitVerification{ //Default at this stage
- Verified: false,
- Reason: "gpg.error.no_gpg_keys_found",
+ }
+
+ defaultGPGSettings, err := c.GetRepositoryDefaultPublicGPGKey(false)
+ if err != nil {
+ log.Error("Error getting default public gpg key: %v", err)
+ } else if defaultGPGSettings.Sign {
+ if commitVerification := verifyWithGPGSettings(defaultGPGSettings, sig, c.Signature.Payload, committer, keyID); commitVerification != nil {
+ if commitVerification.Reason == BadSignature {
+ defaultReason = BadSignature
+ } else {
+ return commitVerification
+ }
}
}
- return &CommitVerification{
- Verified: false, //Default value
- Reason: "gpg.error.not_signed_commit", //Default value
+ return &CommitVerification{ //Default at this stage
+ CommittingUser: committer,
+ Verified: false,
+ Warning: defaultReason != NoKeyFound,
+ Reason: defaultReason,
+ SigningKey: &GPGKey{
+ KeyID: keyID,
+ },
+ }
+}
+
+func verifyWithGPGSettings(gpgSettings *git.GPGSettings, sig *packet.Signature, payload string, committer *User, keyID string) *CommitVerification {
+ // First try to find the key in the db
+ if commitVerification := hashAndVerifyForKeyID(sig, payload, committer, gpgSettings.KeyID, gpgSettings.Name, gpgSettings.Email); commitVerification != nil {
+ return commitVerification
}
+
+ // Otherwise we have to parse the key
+ ekey, err := checkArmoredGPGKeyString(gpgSettings.PublicKeyContent)
+ if err != nil {
+ log.Error("Unable to get default signing key: %v", err)
+ return &CommitVerification{
+ CommittingUser: committer,
+ Verified: false,
+ Reason: "gpg.error.generate_hash",
+ }
+ }
+ pubkey := ekey.PrimaryKey
+ content, err := base64EncPubKey(pubkey)
+ if err != nil {
+ return &CommitVerification{
+ CommittingUser: committer,
+ Verified: false,
+ Reason: "gpg.error.generate_hash",
+ }
+ }
+ k := &GPGKey{
+ Content: content,
+ CanSign: pubkey.CanSign(),
+ KeyID: pubkey.KeyIdString(),
+ }
+ if commitVerification := hashAndVerifyWithSubKeys(sig, payload, k, committer, &User{
+ Name: gpgSettings.Name,
+ Email: gpgSettings.Email,
+ }, gpgSettings.Email); commitVerification != nil {
+ return commitVerification
+ }
+ if keyID == k.KeyID {
+ // This is a bad situation ... We have a key id that matches our default key but the signature doesn't match.
+ return &CommitVerification{
+ CommittingUser: committer,
+ Verified: false,
+ Warning: true,
+ Reason: BadSignature,
+ }
+ }
+ return nil
}
// ParseCommitsWithSignature checks if signaute of commits are corresponding to users gpg keys.
diff --git a/models/repo.go b/models/repo.go
index d8a462c37b..06708d24ab 100644
--- a/models/repo.go
+++ b/models/repo.go
@@ -38,6 +38,7 @@ import (
"code.gitea.io/gitea/modules/timeutil"
"github.com/go-xorm/xorm"
+ "github.com/mcuadros/go-version"
"github.com/unknwon/com"
ini "gopkg.in/ini.v1"
"xorm.io/builder"
@@ -1126,7 +1127,20 @@ func CleanUpMigrateInfo(repo *Repository) (*Repository, error) {
}
// initRepoCommit temporarily changes with work directory.
-func initRepoCommit(tmpPath string, sig *git.Signature) (err error) {
+func initRepoCommit(tmpPath string, u *User) (err error) {
+ commitTimeStr := time.Now().Format(time.RFC3339)
+
+ sig := u.NewGitSig()
+ // Because this may call hooks we should pass in the environment
+ env := append(os.Environ(),
+ "GIT_AUTHOR_NAME="+sig.Name,
+ "GIT_AUTHOR_EMAIL="+sig.Email,
+ "GIT_AUTHOR_DATE="+commitTimeStr,
+ "GIT_COMMITTER_NAME="+sig.Name,
+ "GIT_COMMITTER_EMAIL="+sig.Email,
+ "GIT_COMMITTER_DATE="+commitTimeStr,
+ )
+
var stderr string
if _, stderr, err = process.GetManager().ExecDir(-1,
tmpPath, fmt.Sprintf("initRepoCommit (git add): %s", tmpPath),
@@ -1134,10 +1148,29 @@ func initRepoCommit(tmpPath string, sig *git.Signature) (err error) {
return fmt.Errorf("git add: %s", stderr)
}
- if _, stderr, err = process.GetManager().ExecDir(-1,
+ binVersion, err := git.BinVersion()
+ if err != nil {
+ return fmt.Errorf("Unable to get git version: %v", err)
+ }
+
+ args := []string{
+ "commit", fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email),
+ "-m", "Initial commit",
+ }
+
+ if version.Compare(binVersion, "1.7.9", ">=") {
+ sign, keyID := SignInitialCommit(tmpPath, u)
+ if sign {
+ args = append(args, "-S"+keyID)
+ } else if version.Compare(binVersion, "2.0.0", ">=") {
+ args = append(args, "--no-gpg-sign")
+ }
+ }
+
+ if _, stderr, err = process.GetManager().ExecDirEnv(-1,
tmpPath, fmt.Sprintf("initRepoCommit (git commit): %s", tmpPath),
- git.GitExecutable, "commit", fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email),
- "-m", "Initial commit"); err != nil {
+ env,
+ git.GitExecutable, args...); err != nil {
return fmt.Errorf("git commit: %s", stderr)
}
@@ -1189,9 +1222,24 @@ func getRepoInitFile(tp, name string) ([]byte, error) {
}
func prepareRepoCommit(e Engine, repo *Repository, tmpDir, repoPath string, opts CreateRepoOptions) error {
+ commitTimeStr := time.Now().Format(time.RFC3339)
+ authorSig := repo.Owner.NewGitSig()
+
+ // Because this may call hooks we should pass in the environment
+ env := append(os.Environ(),
+ "GIT_AUTHOR_NAME="+authorSig.Name,
+ "GIT_AUTHOR_EMAIL="+authorSig.Email,
+ "GIT_AUTHOR_DATE="+commitTimeStr,
+ "GIT_COMMITTER_NAME="+authorSig.Name,
+ "GIT_COMMITTER_EMAIL="+authorSig.Email,
+ "GIT_COMMITTER_DATE="+commitTimeStr,
+ )
+
// Clone to temporary path and do the init commit.
- _, stderr, err := process.GetManager().Exec(
+ _, stderr, err := process.GetManager().ExecDirEnv(
+ -1, "",
fmt.Sprintf("initRepository(git clone): %s", repoPath),
+ env,
git.GitExecutable, "clone", repoPath, tmpDir,
)
if err != nil {
@@ -1282,7 +1330,7 @@ func initRepository(e Engine, repoPath string, u *User, repo *Repository, opts C
}
// Apply changes and commit.
- if err = initRepoCommit(tmpDir, u.NewGitSig()); err != nil {
+ if err = initRepoCommit(tmpDir, u); err != nil {
return fmt.Errorf("initRepoCommit: %v", err)
}
}
diff --git a/models/repo_sign.go b/models/repo_sign.go
new file mode 100644
index 0000000000..bac69f76a8
--- /dev/null
+++ b/models/repo_sign.go
@@ -0,0 +1,303 @@
+// Copyright 2019 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package models
+
+import (
+ "strings"
+
+ "code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/process"
+ "code.gitea.io/gitea/modules/setting"
+)
+
+type signingMode string
+
+const (
+ never signingMode = "never"
+ always signingMode = "always"
+ pubkey signingMode = "pubkey"
+ twofa signingMode = "twofa"
+ parentSigned signingMode = "parentsigned"
+ baseSigned signingMode = "basesigned"
+ headSigned signingMode = "headsigned"
+ commitsSigned signingMode = "commitssigned"
+)
+
+func signingModeFromStrings(modeStrings []string) []signingMode {
+ returnable := make([]signingMode, 0, len(modeStrings))
+ for _, mode := range modeStrings {
+ signMode := signingMode(strings.ToLower(mode))
+ switch signMode {
+ case never:
+ return []signingMode{never}
+ case always:
+ return []signingMode{always}
+ case pubkey:
+ fallthrough
+ case twofa:
+ fallthrough
+ case parentSigned:
+ fallthrough
+ case baseSigned:
+ fallthrough
+ case headSigned:
+ fallthrough
+ case commitsSigned:
+ returnable = append(returnable, signMode)
+ }
+ }
+ if len(returnable) == 0 {
+ return []signingMode{never}
+ }
+ return returnable
+}
+
+func signingKey(repoPath string) string {
+ if setting.Repository.Signing.SigningKey == "none" {
+ return ""
+ }
+
+ 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("config", "--get", "commit.gpgsign").RunInDir(repoPath)
+ sign, valid := git.ParseBool(strings.TrimSpace(value))
+ if !sign || !valid {
+ return ""
+ }
+
+ signingKey, _ := git.NewCommand("config", "--get", "user.signingkey").RunInDir(repoPath)
+ return strings.TrimSpace(signingKey)
+ }
+
+ return setting.Repository.Signing.SigningKey
+}
+
+// PublicSigningKey gets the public signing key within a provided repository directory
+func PublicSigningKey(repoPath string) (string, error) {
+ signingKey := signingKey(repoPath)
+ if signingKey == "" {
+ return "", nil
+ }
+
+ content, stderr, err := process.GetManager().ExecDir(-1, repoPath,
+ "gpg --export -a", "gpg", "--export", "-a", signingKey)
+ if err != nil {
+ log.Error("Unable to get default signing key in %s: %s, %s, %v", repoPath, signingKey, stderr, err)
+ return "", err
+ }
+ return content, nil
+}
+
+// SignInitialCommit determines if we should sign the initial commit to this repository
+func SignInitialCommit(repoPath string, u *User) (bool, string) {
+ rules := signingModeFromStrings(setting.Repository.Signing.InitialCommit)
+ signingKey := signingKey(repoPath)
+ if signingKey == "" {
+ return false, ""
+ }
+
+ for _, rule := range rules {
+ switch rule {
+ case never:
+ return false, ""
+ case always:
+ break
+ case pubkey:
+ keys, err := ListGPGKeys(u.ID)
+ if err != nil || len(keys) == 0 {
+ return false, ""
+ }
+ case twofa:
+ twofa, err := GetTwoFactorByUID(u.ID)
+ if err != nil || twofa == nil {
+ return false, ""
+ }
+ }
+ }
+ return true, signingKey
+}
+
+// SignWikiCommit determines if we should sign the commits to this repository wiki
+func (repo *Repository) SignWikiCommit(u *User) (bool, string) {
+ rules := signingModeFromStrings(setting.Repository.Signing.Wiki)
+ signingKey := signingKey(repo.WikiPath())
+ if signingKey == "" {
+ return false, ""
+ }
+
+ for _, rule := range rules {
+ switch rule {
+ case never:
+ return false, ""
+ case always:
+ break
+ case pubkey:
+ keys, err := ListGPGKeys(u.ID)
+ if err != nil || len(keys) == 0 {
+ return false, ""
+ }
+ case twofa:
+ twofa, err := GetTwoFactorByUID(u.ID)
+ if err != nil || twofa == nil {
+ return false, ""
+ }
+ case parentSigned:
+ gitRepo, err := git.OpenRepository(repo.WikiPath())
+ if err != nil {
+ return false, ""
+ }
+ commit, err := gitRepo.GetCommit("HEAD")
+ if err != nil {
+ return false, ""
+ }
+ if commit.Signature == nil {
+ return false, ""
+ }
+ verification := ParseCommitWithSignature(commit)
+ if !verification.Verified {
+ return false, ""
+ }
+ }
+ }
+ return true, signingKey
+}
+
+// SignCRUDAction determines if we should sign a CRUD commit to this repository
+func (repo *Repository) SignCRUDAction(u *User, tmpBasePath, parentCommit string) (bool, string) {
+ rules := signingModeFromStrings(setting.Repository.Signing.CRUDActions)
+ signingKey := signingKey(repo.RepoPath())
+ if signingKey == "" {
+ return false, ""
+ }
+
+ for _, rule := range rules {
+ switch rule {
+ case never:
+ return false, ""
+ case always:
+ break
+ case pubkey:
+ keys, err := ListGPGKeys(u.ID)
+ if err != nil || len(keys) == 0 {
+ return false, ""
+ }
+ case twofa:
+ twofa, err := GetTwoFactorByUID(u.ID)
+ if err != nil || twofa == nil {
+ return false, ""
+ }
+ case parentSigned:
+ gitRepo, err := git.OpenRepository(tmpBasePath)
+ if err != nil {
+ return false, ""
+ }
+ commit, err := gitRepo.GetCommit(parentCommit)
+ if err != nil {
+ return false, ""
+ }
+ if commit.Signature == nil {
+ return false, ""
+ }
+ verification := ParseCommitWithSignature(commit)
+ if !verification.Verified {
+ return false, ""
+ }
+ }
+ }
+ return true, signingKey
+}
+
+// SignMerge determines if we should sign a merge commit to this repository
+func (repo *Repository) SignMerge(u *User, tmpBasePath, baseCommit, headCommit string) (bool, string) {
+ rules := signingModeFromStrings(setting.Repository.Signing.Merges)
+ signingKey := signingKey(repo.RepoPath())
+ if signingKey == "" {
+ return false, ""
+ }
+ var gitRepo *git.Repository
+ var err error
+
+ for _, rule := range rules {
+ switch rule {
+ case never:
+ return false, ""
+ case always:
+ break
+ case pubkey:
+ keys, err := ListGPGKeys(u.ID)
+ if err != nil || len(keys) == 0 {
+ return false, ""
+ }
+ case twofa:
+ twofa, err := GetTwoFactorByUID(u.ID)
+ if err != nil || twofa == nil {
+ return false, ""
+ }
+ case baseSigned:
+ if gitRepo == nil {
+ gitRepo, err = git.OpenRepository(tmpBasePath)
+ if err != nil {
+ return false, ""
+ }
+ }
+ commit, err := gitRepo.GetCommit(baseCommit)
+ if err != nil {
+ return false, ""
+ }
+ verification := ParseCommitWithSignature(commit)
+ if !verification.Verified {
+ return false, ""
+ }
+ case headSigned:
+ if gitRepo == nil {
+ gitRepo, err = git.OpenRepository(tmpBasePath)
+ if err != nil {
+ return false, ""
+ }
+ }
+ commit, err := gitRepo.GetCommit(headCommit)
+ if err != nil {
+ return false, ""
+ }
+ verification := ParseCommitWithSignature(commit)
+ if !verification.Verified {
+ return false, ""
+ }
+ case commitsSigned:
+ if gitRepo == nil {
+ gitRepo, err = git.OpenRepository(tmpBasePath)
+ if err != nil {
+ return false, ""
+ }
+ }
+ commit, err := gitRepo.GetCommit(headCommit)
+ if err != nil {
+ return false, ""
+ }
+ verification := ParseCommitWithSignature(commit)
+ if !verification.Verified {
+ return false, ""
+ }
+ // need to work out merge-base
+ mergeBaseCommit, _, err := gitRepo.GetMergeBase("", baseCommit, headCommit)
+ if err != nil {
+ return false, ""
+ }
+ commitList, err := commit.CommitsBeforeUntil(mergeBaseCommit)
+ if err != nil {
+ return false, ""
+ }
+ for e := commitList.Front(); e != nil; e = e.Next() {
+ commit = e.Value.(*git.Commit)
+ verification := ParseCommitWithSignature(commit)
+ if !verification.Verified {
+ return false, ""
+ }
+ }
+ }
+ }
+ return true, signingKey
+}
diff --git a/models/wiki.go b/models/wiki.go
index 0460e0f079..858fe1d6d0 100644
--- a/models/wiki.go
+++ b/models/wiki.go
@@ -205,6 +205,13 @@ func (repo *Repository) updateWikiPage(doer *User, oldWikiName, newWikiName, con
commitTreeOpts := git.CommitTreeOpts{
Message: message,
}
+
+ sign, signingKey := repo.SignWikiCommit(doer)
+ if sign {
+ commitTreeOpts.KeyID = signingKey
+ } else {
+ commitTreeOpts.NoGPGSign = true
+ }
if hasMasterBranch {
commitTreeOpts.Parents = []string{"HEAD"}
}
@@ -307,11 +314,19 @@ func (repo *Repository) DeleteWikiPage(doer *User, wikiName string) (err error)
return err
}
message := "Delete page '" + wikiName + "'"
-
- commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), tree, git.CommitTreeOpts{
+ commitTreeOpts := git.CommitTreeOpts{
Message: message,
Parents: []string{"HEAD"},
- })
+ }
+
+ sign, signingKey := repo.SignWikiCommit(doer)
+ if sign {
+ commitTreeOpts.KeyID = signingKey
+ } else {
+ commitTreeOpts.NoGPGSign = true
+ }
+
+ commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), tree, commitTreeOpts)
if err != nil {
return err
}
diff --git a/modules/git/commit.go b/modules/git/commit.go
index eb442f988d..45b943e79e 100644
--- a/modules/git/commit.go
+++ b/modules/git/commit.go
@@ -498,3 +498,11 @@ func GetFullCommitID(repoPath, shortID string) (string, error) {
}
return strings.TrimSpace(commitID), nil
}
+
+// GetRepositoryDefaultPublicGPGKey returns the default public key for this commit
+func (c *Commit) GetRepositoryDefaultPublicGPGKey(forceUpdate bool) (*GPGSettings, error) {
+ if c.repo == nil {
+ return nil, nil
+ }
+ return c.repo.GetDefaultPublicGPGKey(forceUpdate)
+}
diff --git a/modules/git/repo.go b/modules/git/repo.go
index 1a9112132f..dd886f3a2e 100644
--- a/modules/git/repo.go
+++ b/modules/git/repo.go
@@ -32,6 +32,16 @@ type Repository struct {
gogitRepo *gogit.Repository
gogitStorage *filesystem.Storage
+ gpgSettings *GPGSettings
+}
+
+// GPGSettings represents the default GPG settings for this repository
+type GPGSettings struct {
+ Sign bool
+ KeyID string
+ Email string
+ Name string
+ PublicKeyContent string
}
const prettyLogFormat = `--pretty=format:%H`
diff --git a/modules/git/repo_gpg.go b/modules/git/repo_gpg.go
new file mode 100644
index 0000000000..b4c3f3b431
--- /dev/null
+++ b/modules/git/repo_gpg.go
@@ -0,0 +1,59 @@
+// Copyright 2015 The Gogs Authors. All rights reserved.
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package git
+
+import (
+ "fmt"
+ "strings"
+
+ "code.gitea.io/gitea/modules/process"
+)
+
+// LoadPublicKeyContent will load the key from gpg
+func (gpgSettings *GPGSettings) LoadPublicKeyContent() error {
+ content, stderr, err := process.GetManager().Exec(
+ "gpg -a --export",
+ "gpg", "-a", "--export", gpgSettings.KeyID)
+ if err != nil {
+ return fmt.Errorf("Unable to get default signing key: %s, %s, %v", gpgSettings.KeyID, stderr, err)
+ }
+ gpgSettings.PublicKeyContent = content
+ return nil
+}
+
+// GetDefaultPublicGPGKey will return and cache the default public GPG settings for this repository
+func (repo *Repository) GetDefaultPublicGPGKey(forceUpdate bool) (*GPGSettings, error) {
+ if repo.gpgSettings != nil && !forceUpdate {
+ return repo.gpgSettings, nil
+ }
+
+ gpgSettings := &GPGSettings{
+ Sign: true,
+ }
+
+ value, _ := NewCommand("config", "--get", "commit.gpgsign").RunInDir(repo.Path)
+ sign, valid := ParseBool(strings.TrimSpace(value))
+ if !sign || !valid {
+ gpgSettings.Sign = false
+ repo.gpgSettings = gpgSettings
+ return gpgSettings, nil
+ }
+
+ signingKey, _ := NewCommand("config", "--get", "user.signingkey").RunInDir(repo.Path)
+ gpgSettings.KeyID = strings.TrimSpace(signingKey)
+
+ defaultEmail, _ := NewCommand("config", "--get", "user.email").RunInDir(repo.Path)
+ gpgSettings.Email = strings.TrimSpace(defaultEmail)
+
+ defaultName, _ := NewCommand("config", "--get", "user.name").RunInDir(repo.Path)
+ gpgSettings.Name = strings.TrimSpace(defaultName)
+
+ if err := gpgSettings.LoadPublicKeyContent(); err != nil {
+ return nil, err
+ }
+ repo.gpgSettings = gpgSettings
+ return repo.gpgSettings, nil
+}
diff --git a/modules/git/repo_tree.go b/modules/git/repo_tree.go
index f5262ba81c..8f91f4efac 100644
--- a/modules/git/repo_tree.go
+++ b/modules/git/repo_tree.go
@@ -56,10 +56,11 @@ func (repo *Repository) GetTree(idStr string) (*Tree, error) {
// CommitTreeOpts represents the possible options to CommitTree
type CommitTreeOpts struct {
- Parents []string
- Message string
- KeyID string
- NoGPGSign bool
+ Parents []string
+ Message string
+ KeyID string
+ NoGPGSign bool
+ AlwaysSign bool
}
// CommitTree creates a commit from a given tree id for the user with provided message
@@ -90,7 +91,7 @@ func (repo *Repository) CommitTree(sig *Signature, tree *Tree, opts CommitTreeOp
_, _ = messageBytes.WriteString(opts.Message)
_, _ = messageBytes.WriteString("\n")
- if opts.KeyID != "" {
+ if version.Compare(binVersion, "1.7.9", ">=") && (opts.KeyID != "" || opts.AlwaysSign) {
cmd.AddArguments(fmt.Sprintf("-S%s", opts.KeyID))
}
diff --git a/modules/git/utils.go b/modules/git/utils.go
index 83cd21f34e..e791f16041 100644
--- a/modules/git/utils.go
+++ b/modules/git/utils.go
@@ -7,6 +7,7 @@ package git
import (
"fmt"
"os"
+ "strconv"
"strings"
"sync"
)
@@ -86,3 +87,30 @@ func RefEndName(refStr string) string {
return refStr
}
+
+// ParseBool returns the boolean value represented by the string as per git's git_config_bool
+// true will be returned for the result if the string is empty, but valid will be false.
+// "true", "yes", "on" are all true, true
+// "false", "no", "off" are all false, true
+// 0 is false, true
+// Any other integer is true, true
+// Anything else will return false, false
+func ParseBool(value string) (result bool, valid bool) {
+ // Empty strings are true but invalid
+ if len(value) == 0 {
+ return true, false
+ }
+ // These are the git expected true and false values
+ if strings.EqualFold(value, "true") || strings.EqualFold(value, "yes") || strings.EqualFold(value, "on") {
+ return true, true
+ }
+ if strings.EqualFold(value, "false") || strings.EqualFold(value, "no") || strings.EqualFold(value, "off") {
+ return false, true
+ }
+ // Try a number
+ intValue, err := strconv.ParseInt(value, 10, 32)
+ if err != nil {
+ return false, false
+ }
+ return intValue != 0, true
+}
diff --git a/modules/repofiles/file_test.go b/modules/repofiles/file_test.go
index 7c45139dd9..95ec175ed4 100644
--- a/modules/repofiles/file_test.go
+++ b/modules/repofiles/file_test.go
@@ -73,7 +73,7 @@ func getExpectedFileResponse() *api.FileResponse {
},
Verification: &api.PayloadCommitVerification{
Verified: false,
- Reason: "",
+ Reason: "gpg.error.not_signed_commit",
Signature: "",
Payload: "",
},
diff --git a/modules/repofiles/temp_repo.go b/modules/repofiles/temp_repo.go
index 4a50e64192..b07d2a8973 100644
--- a/modules/repofiles/temp_repo.go
+++ b/modules/repofiles/temp_repo.go
@@ -261,7 +261,6 @@ func (t *TemporaryUploadRepository) CommitTree(author, committer *models.User, t
return "", fmt.Errorf("Unable to get git version: %v", err)
}
- // FIXME: Should we add SSH_ORIGINAL_COMMAND to this
// Because this may call hooks we should pass in the environment
env := append(os.Environ(),
"GIT_AUTHOR_NAME="+authorSig.Name,
@@ -271,13 +270,21 @@ func (t *TemporaryUploadRepository) CommitTree(author, committer *models.User, t
"GIT_COMMITTER_EMAIL="+committerSig.Email,
"GIT_COMMITTER_DATE="+commitTimeStr,
)
+
messageBytes := new(bytes.Buffer)
_, _ = messageBytes.WriteString(message)
_, _ = messageBytes.WriteString("\n")
args := []string{"commit-tree", treeHash, "-p", "HEAD"}
- if version.Compare(binVersion, "2.0.0", ">=") {
- args = append(args, "--no-gpg-sign")
+
+ // Determine if we should sign
+ if version.Compare(binVersion, "1.7.9", ">=") {
+ sign, keyID := t.repo.SignCRUDAction(author, t.basePath, "HEAD")
+ if sign {
+ args = append(args, "-S"+keyID)
+ } else if version.Compare(binVersion, "2.0.0", ">=") {
+ args = append(args, "--no-gpg-sign")
+ }
}
commitHash, stderr, err := process.GetManager().ExecDirEnvStdIn(5*time.Minute,
diff --git a/modules/repofiles/verification.go b/modules/repofiles/verification.go
index 9fc084daaf..3889b7993c 100644
--- a/modules/repofiles/verification.go
+++ b/modules/repofiles/verification.go
@@ -18,10 +18,16 @@ func GetPayloadCommitVerification(commit *git.Commit) *structs.PayloadCommitVeri
verification.Signature = commit.Signature.Signature
verification.Payload = commit.Signature.Payload
}
- if verification.Reason != "" {
- verification.Reason = commitVerification.Reason
- } else if verification.Verified {
- verification.Reason = "unsigned"
+ if commitVerification.SigningUser != nil {
+ verification.Signer = &structs.PayloadUser{
+ Name: commitVerification.SigningUser.Name,
+ Email: commitVerification.SigningUser.Email,
+ }
+ }
+ verification.Verified = commitVerification.Verified
+ verification.Reason = commitVerification.Reason
+ if verification.Reason == "" && !verification.Verified {
+ verification.Reason = "gpg.error.not_signed_commit"
}
return verification
}
diff --git a/modules/setting/repository.go b/modules/setting/repository.go
index 728741576d..19c68d003f 100644
--- a/modules/setting/repository.go
+++ b/modules/setting/repository.go
@@ -65,6 +65,16 @@ var (
Issue struct {
LockReasons []string
} `ini:"repository.issue"`
+
+ Signing struct {
+ SigningKey string
+ SigningName string
+ SigningEmail string
+ InitialCommit []string
+ CRUDActions []string `ini:"CRUD_ACTIONS"`
+ Merges []string
+ Wiki []string
+ } `ini:"repository.signing"`
}{
AnsiCharset: "",
ForcePrivate: false,
@@ -122,6 +132,25 @@ var (
}{
LockReasons: strings.Split("Too heated,Off-topic,Spam,Resolved", ","),
},
+
+ // Signing settings
+ Signing: struct {
+ SigningKey string
+ SigningName string
+ SigningEmail string
+ InitialCommit []string
+ CRUDActions []string `ini:"CRUD_ACTIONS"`
+ Merges []string
+ Wiki []string
+ }{
+ SigningKey: "default",
+ SigningName: "",
+ SigningEmail: "",
+ InitialCommit: []string{"always"},
+ CRUDActions: []string{"pubkey", "twofa", "parentsigned"},
+ Merges: []string{"pubkey", "twofa", "basesigned", "commitssigned"},
+ Wiki: []string{"never"},
+ },
}
RepoRootPath string
ScriptType = "bash"
diff --git a/modules/structs/hook.go b/modules/structs/hook.go
index 9a25219e36..2c923d36c5 100644
--- a/modules/structs/hook.go
+++ b/modules/structs/hook.go
@@ -91,10 +91,11 @@ type PayloadCommit struct {
// PayloadCommitVerification represents the GPG verification of a commit
type PayloadCommitVerification struct {
- Verified bool `json:"verified"`
- Reason string `json:"reason"`
- Signature string `json:"signature"`
- Payload string `json:"payload"`
+ Verified bool `json:"verified"`
+ Reason string `json:"reason"`
+ Signature string `json:"signature"`
+ Signer *PayloadUser `json:"signer"`
+ Payload string `json:"payload"`
}
var (
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 76a1daa451..4d73d91aa2 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1974,12 +1974,15 @@ mark_as_unread = Mark as unread
mark_all_as_read = Mark all as read
[gpg]
+default_key=Signed with default key
error.extract_sign = Failed to extract signature
error.generate_hash = Failed to generate hash of commit
error.no_committer_account = No account linked to committer's email address
error.no_gpg_keys_found = "No known key found for this signature in database"
error.not_signed_commit = "Not a signed commit"
error.failed_retrieval_gpg_keys = "Failed to retrieve any key attached to the committer's account"
+error.probable_bad_signature = "WARNING! Although there is a key with this ID in the database it does not verify this commit! This commit is SUSPICIOUS."
+error.probable_bad_default_signature = "WARNING! Although the default key has this ID it does not verify this commit! This commit is SUSPICIOUS."
[units]
error.no_unit_allowed_repo = You are not allowed to access any section of this repository.
diff --git a/public/css/index.css b/public/css/index.css
index fda26f4e08..6fdcb5c225 100644
--- a/public/css/index.css
+++ b/public/css/index.css
@@ -225,6 +225,10 @@ footer .ui.left,footer .ui.right{line-height:40px}
.inline-grouped-list{display:inline-block;vertical-align:top}
.inline-grouped-list>.ui{display:block;margin-top:5px;margin-bottom:10px}
.inline-grouped-list>.ui:first-child{margin-top:1px}
+i.icons .icon:first-child{margin-right:0}
+i.icon.centerlock{top:1.5em}
+.ui.label>.detail .icons{margin-right:.25em}
+.ui.label>.detail .icons .icon{margin-right:0}
.lines-num{vertical-align:top;text-align:right!important;color:#999;background:#f5f5f5;width:1%;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
.lines-num span:before{content:attr(data-line-number);line-height:20px!important;padding:0 10px;cursor:pointer;display:block}
.lines-code,.lines-num{padding:0!important}
@@ -654,6 +658,8 @@ footer .ui.left,footer .ui.right{line-height:40px}
.repository #commits-table.ui.basic.striped.table tbody tr:nth-child(2n){background-color:rgba(0,0,0,.02)!important}
.repository #commits-table td.sha .sha.label,.repository #repo-files-table .sha.label{border:1px solid #bbb}
.repository #commits-table td.sha .sha.label .detail.icon,.repository #repo-files-table .sha.label .detail.icon{background:#fafafa;margin:-6px -10px -4px 0;padding:5px 3px 5px 6px;border-left:1px solid #bbb;border-top-left-radius:0;border-bottom-left-radius:0}
+.repository #commits-table td.sha .sha.label.isSigned.isWarning,.repository #repo-files-table .sha.label.isSigned.isWarning{border:1px solid #db2828;background:rgba(219,40,40,.1)}
+.repository #commits-table td.sha .sha.label.isSigned.isWarning .detail.icon,.repository #repo-files-table .sha.label.isSigned.isWarning .detail.icon{border-left:1px solid rgba(219,40,40,.5)}
.repository #commits-table td.sha .sha.label.isSigned.isVerified,.repository #repo-files-table .sha.label.isSigned.isVerified{border:1px solid #21ba45;background:rgba(33,186,69,.1)}
.repository #commits-table td.sha .sha.label.isSigned.isVerified .detail.icon,.repository #repo-files-table .sha.label.isSigned.isVerified .detail.icon{border-left:1px solid #21ba45}
.repository #commits-table td.sha .sha.label.isSigned.isVerified:hover,.repository #repo-files-table .sha.label.isSigned.isVerified:hover{background:rgba(33,186,69,.3)!important}
diff --git a/public/less/_base.less b/public/less/_base.less
index e295be368d..62b2084a3b 100644
--- a/public/less/_base.less
+++ b/public/less/_base.less
@@ -950,6 +950,22 @@ footer {
}
}
+i.icons .icon:first-child {
+ margin-right: 0;
+}
+
+i.icon.centerlock {
+ top: 1.5em;
+}
+
+.ui.label > .detail .icons {
+ margin-right: 0.25em;
+}
+
+.ui.label > .detail .icons .icon {
+ margin-right: 0;
+}
+
.lines-num {
vertical-align: top;
text-align: right !important;
diff --git a/public/less/_repository.less b/public/less/_repository.less
index 5f6a7fbd97..3586eeccf0 100644
--- a/public/less/_repository.less
+++ b/public/less/_repository.less
@@ -1212,6 +1212,15 @@
border-bottom-left-radius: 0;
}
+ &.isSigned.isWarning {
+ border: 1px solid #db2828;
+ background: fade(#db2828, 10%);
+
+ .detail.icon {
+ border-left: 1px solid fade(#db2828, 50%);
+ }
+ }
+
&.isSigned.isVerified {
border: 1px solid #21ba45;
background: fade(#21ba45, 10%);
diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go
index 04ff91fbbf..f8ab9025b7 100644
--- a/routers/api/v1/api.go
+++ b/routers/api/v1/api.go
@@ -507,6 +507,7 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Get("/swagger", misc.Swagger)
}
m.Get("/version", misc.Version)
+ m.Get("/signing-key.gpg", misc.SigningKey)
m.Post("/markdown", bind(api.MarkdownOption{}), misc.Markdown)
m.Post("/markdown/raw", misc.MarkdownRaw)
@@ -771,6 +772,7 @@ func RegisterRoutes(m *macaron.Macaron) {
m.Delete("", bind(api.DeleteFileOptions{}), repo.DeleteFile)
}, reqRepoWriter(models.UnitTypeCode), reqToken())
}, reqRepoReader(models.UnitTypeCode))
+ m.Get("/signing-key.gpg", misc.SigningKey)
m.Group("/topics", func() {
m.Combo("").Get(repo.ListTopics).
Put(reqToken(), reqAdmin(), bind(api.RepoTopicOptions{}), repo.UpdateTopics)
diff --git a/routers/api/v1/convert/convert.go b/routers/api/v1/convert/convert.go
index e0e7f609c7..0262051390 100644
--- a/routers/api/v1/convert/convert.go
+++ b/routers/api/v1/convert/convert.go
@@ -12,6 +12,7 @@ import (
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup"
+ "code.gitea.io/gitea/modules/structs"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
@@ -84,17 +85,21 @@ func ToCommit(repo *models.Repository, c *git.Commit) *api.PayloadCommit {
// ToVerification convert a git.Commit.Signature to an api.PayloadCommitVerification
func ToVerification(c *git.Commit) *api.PayloadCommitVerification {
verif := models.ParseCommitWithSignature(c)
- var signature, payload string
+ commitVerification := &api.PayloadCommitVerification{
+ Verified: verif.Verified,
+ Reason: verif.Reason,
+ }
if c.Signature != nil {
- signature = c.Signature.Signature
- payload = c.Signature.Payload
+ commitVerification.Signature = c.Signature.Signature
+ commitVerification.Payload = c.Signature.Payload
}
- return &api.PayloadCommitVerification{
- Verified: verif.Verified,
- Reason: verif.Reason,
- Signature: signature,
- Payload: payload,
+ if verif.SigningUser != nil {
+ commitVerification.Signer = &structs.PayloadUser{
+ Name: verif.SigningUser.Name,
+ Email: verif.SigningUser.Email,
+ }
}
+ return commitVerification
}
// ToPublicKey convert models.PublicKey to api.PublicKey
diff --git a/routers/api/v1/misc/signing.go b/routers/api/v1/misc/signing.go
new file mode 100644
index 0000000000..f5428670af
--- /dev/null
+++ b/routers/api/v1/misc/signing.go
@@ -0,0 +1,62 @@
+package misc
+
+import (
+ "fmt"
+ "net/http"
+
+ "code.gitea.io/gitea/models"
+ "code.gitea.io/gitea/modules/context"
+ "code.gitea.io/gitea/modules/log"
+)
+
+// SigningKey returns the public key of the default signing key if it exists
+func SigningKey(ctx *context.Context) {
+ // swagger:operation GET /signing-key.gpg miscellaneous getSigningKey
+ // ---
+ // summary: Get default signing-key.gpg
+ // produces:
+ // - text/plain
+ // responses:
+ // "200":
+ // description: "GPG armored public key"
+ // schema:
+ // type: string
+
+ // swagger:operation GET /repos/{owner}/{repo}/signing-key.gpg repository repoSigningKey
+ // ---
+ // summary: Get signing-key.gpg 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: "GPG armored public key"
+ // schema:
+ // type: string
+
+ path := ""
+ if ctx.Repo != nil && ctx.Repo.Repository != nil {
+ path = ctx.Repo.Repository.RepoPath()
+ }
+
+ content, err := models.PublicSigningKey(path)
+ if err != nil {
+ ctx.ServerError("gpg export", err)
+ return
+ }
+ _, err = ctx.Write([]byte(content))
+ if err != nil {
+ log.Error("Error writing key content %v", err)
+ ctx.Error(http.StatusInternalServerError, fmt.Sprintf("%v", err))
+ }
+}
diff --git a/services/pull/merge.go b/services/pull/merge.go
index 355d6dd911..0d762dbc30 100644
--- a/services/pull/merge.go
+++ b/services/pull/merge.go
@@ -13,6 +13,7 @@ import (
"os"
"path/filepath"
"strings"
+ "time"
"code.gitea.io/gitea/models"
"code.gitea.io/gitea/modules/cache"
@@ -28,6 +29,11 @@ import (
// Merge merges pull request to base repository.
// FIXME: add repoWorkingPull make sure two merges does not happen at same time.
func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repository, mergeStyle models.MergeStyle, message string) (err error) {
+ binVersion, err := git.BinVersion()
+ if err != nil {
+ return fmt.Errorf("Unable to get git version: %v", err)
+ }
+
if err = pr.GetHeadRepo(); err != nil {
return fmt.Errorf("GetHeadRepo: %v", err)
} else if err = pr.GetBaseRepo(); err != nil {
@@ -176,6 +182,30 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor
return fmt.Errorf("git read-tree HEAD: %s", errbuf.String())
}
+ // Determine if we should sign
+ signArg := ""
+ if version.Compare(binVersion, "1.7.9", ">=") {
+ sign, keyID := pr.BaseRepo.SignMerge(doer, tmpBasePath, "HEAD", trackingBranch)
+ if sign {
+ signArg = "-S" + keyID
+ } else if version.Compare(binVersion, "2.0.0", ">=") {
+ signArg = "--no-gpg-sign"
+ }
+ }
+
+ sig := doer.NewGitSig()
+ commitTimeStr := time.Now().Format(time.RFC3339)
+
+ // Because this may call hooks we should pass in the environment
+ env := append(os.Environ(),
+ "GIT_AUTHOR_NAME="+sig.Name,
+ "GIT_AUTHOR_EMAIL="+sig.Email,
+ "GIT_AUTHOR_DATE="+commitTimeStr,
+ "GIT_COMMITTER_NAME="+sig.Name,
+ "GIT_COMMITTER_EMAIL="+sig.Email,
+ "GIT_COMMITTER_DATE="+commitTimeStr,
+ )
+
// Merge commits.
switch mergeStyle {
case models.MergeStyleMerge:
@@ -183,9 +213,14 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor
return fmt.Errorf("git merge --no-ff --no-commit [%s]: %v - %s", tmpBasePath, err, errbuf.String())
}
- sig := doer.NewGitSig()
- if err := git.NewCommand("commit", fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email), "-m", message).RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil {
- return fmt.Errorf("git commit [%s]: %v - %s", tmpBasePath, err, errbuf.String())
+ if signArg == "" {
+ if err := git.NewCommand("commit", "-m", message).RunInDirTimeoutEnvPipeline(env, -1, tmpBasePath, nil, &errbuf); err != nil {
+ return fmt.Errorf("git commit [%s]: %v - %s", tmpBasePath, err, errbuf.String())
+ }
+ } else {
+ if err := git.NewCommand("commit", signArg, "-m", message).RunInDirTimeoutEnvPipeline(env, -1, tmpBasePath, nil, &errbuf); err != nil {
+ return fmt.Errorf("git commit [%s]: %v - %s", tmpBasePath, err, errbuf.String())
+ }
}
case models.MergeStyleRebase:
// Checkout head branch
@@ -223,9 +258,14 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor
}
// Set custom message and author and create merge commit
- sig := doer.NewGitSig()
- if err := git.NewCommand("commit", fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email), "-m", message).RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil {
- return fmt.Errorf("git commit [%s]: %v - %s", tmpBasePath, err, errbuf.String())
+ if signArg == "" {
+ if err := git.NewCommand("commit", "-m", message).RunInDirTimeoutEnvPipeline(env, -1, tmpBasePath, nil, &errbuf); err != nil {
+ return fmt.Errorf("git commit [%s]: %v - %s", tmpBasePath, err, errbuf.String())
+ }
+ } else {
+ if err := git.NewCommand("commit", signArg, "-m", message).RunInDirTimeoutEnvPipeline(env, -1, tmpBasePath, nil, &errbuf); err != nil {
+ return fmt.Errorf("git commit [%s]: %v - %s", tmpBasePath, err, errbuf.String())
+ }
}
case models.MergeStyleSquash:
@@ -234,8 +274,14 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor
return fmt.Errorf("git merge --squash [%s -> %s]: %s", headRepoPath, tmpBasePath, errbuf.String())
}
sig := pr.Issue.Poster.NewGitSig()
- if err := git.NewCommand("commit", fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email), "-m", message).RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil {
- return fmt.Errorf("git commit [%s]: %v - %s", tmpBasePath, err, errbuf.String())
+ if signArg == "" {
+ if err := git.NewCommand("commit", fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email), "-m", message).RunInDirTimeoutEnvPipeline(env, -1, tmpBasePath, nil, &errbuf); err != nil {
+ return fmt.Errorf("git commit [%s]: %v - %s", tmpBasePath, err, errbuf.String())
+ }
+ } else {
+ if err := git.NewCommand("commit", signArg, fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email), "-m", message).RunInDirTimeoutEnvPipeline(env, -1, tmpBasePath, nil, &errbuf); err != nil {
+ return fmt.Errorf("git commit [%s]: %v - %s", tmpBasePath, err, errbuf.String())
+ }
}
default:
return models.ErrInvalidMergeStyle{ID: pr.BaseRepo.ID, Style: mergeStyle}
@@ -270,7 +316,7 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor
headUser = doer
}
- env := models.FullPushingEnvironment(
+ env = models.FullPushingEnvironment(
headUser,
doer,
pr.BaseRepo,
diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl
index 0cfdf5156d..5b19523cf2 100644
--- a/templates/repo/commit_page.tmpl
+++ b/templates/repo/commit_page.tmpl
@@ -26,6 +26,16 @@
<img class="ui avatar image" src="{{AvatarLink .Commit.Author.Email}}" />
<strong>{{.Commit.Author.Name}}</strong>
{{end}}
+ {{if or (ne .Commit.Committer.Name .Commit.Author.Name) (ne .Commit.Committer.Email .Commit.Author.Email)}}
+ <span> </span>
+ {{if ne .Verification.CommittingUser.ID 0}}
+ <img class="ui avatar image" src="{{.Verification.CommittingUser.RelAvatarLink}}" />
+ <a href="{{.Verification.CommittingUser.HomeLink}}"><strong>{{.Commit.Committer.Name}}</strong></a> <{{.Commit.Committer.Email}}>
+ {{else}}
+ <img class="ui avatar image" src="{{AvatarLink .Commit.Committer.Email}}" />
+ <strong>{{.Commit.Committer.Name}}</strong>
+ {{end}}
+ {{end}}
<span class="text grey" id="authored-time">{{TimeSince .Commit.Author.When $.Lang}}</span>
</div>
<div class="seven wide right aligned column">
@@ -50,15 +60,36 @@
{{if .Commit.Signature}}
{{if .Verification.Verified }}
<div class="ui bottom attached positive message">
- <i class="green lock icon"></i>
- <span>{{.i18n.Tr "repo.commits.signed_by"}}:</span>
- <a href="{{.Verification.SigningUser.HomeLink}}"><strong>{{.Commit.Committer.Name}}</strong></a> <{{.Commit.Committer.Email}}>
- <span class="pull-right"><span>{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span> {{.Verification.SigningKey.KeyID}}</span>
+ {{if ne .Verification.SigningUser.ID 0}}
+ <i class="green lock icon"></i>
+ <span>{{.i18n.Tr "repo.commits.signed_by"}}:</span>
+ <img class="ui avatar image" src="{{.Verification.SigningUser.RelAvatarLink}}" />
+ <a href="{{.Verification.SigningUser.HomeLink}}"><strong>{{.Verification.SigningUser.Name}}</strong></a> <{{.Verification.SigningEmail}}>
+ <span class="pull-right"><span>{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span> {{.Verification.SigningKey.KeyID}}</span>
+ {{else}}
+ <i class="icons" title="{{.i18n.Tr "gpg.default_key"}}">
+ <i class="green lock icon"></i>
+ <i class="tiny inverted cog icon centerlock"></i>
+ </i>
+ <span>{{.i18n.Tr "repo.commits.signed_by"}}:</span>
+ <img class="ui avatar image" src="{{AvatarLink .Verification.SigningEmail}}" />
+ <strong>{{.Verification.SigningUser.Name}}</strong> <{{.Verification.SigningEmail}}>
+ <span class="pull-right"><span>{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span> <i class="cogs icon" title="{{.i18n.Tr "gpg.default_key"}}"></i>{{.Verification.SigningKey.KeyID}}</span>
+ {{end}}
+ </div>
+ {{else if .Verification.Warning}}
+ <div class="ui bottom attached message">
+ <i class="red unlock icon"></i>
+ <span class="red text">{{.i18n.Tr .Verification.Reason}}</span>
+ <span class="pull-right"><span class="red text">{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span> <i class="red warning icon"></i>{{.Verification.SigningKey.KeyID}}</span>
</div>
{{else}}
<div class="ui bottom attached message">
<i class="grey unlock icon"></i>
{{.i18n.Tr .Verification.Reason}}
+ {{if and .Verification.SigningKey (ne .Verification.SigningKey.KeyID "")}}
+ <span class="pull-right"><span class="red text">{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span> <i class="red warning icon"></i>{{.Verification.SigningKey.KeyID}}</span>
+ {{end}}
</div>
{{end}}
{{end}}
diff --git a/templates/repo/commits_table.tmpl b/templates/repo/commits_table.tmpl
index e11bbee0e8..09a2c072b1 100644
--- a/templates/repo/commits_table.tmpl
+++ b/templates/repo/commits_table.tmpl
@@ -56,12 +56,21 @@
{{end}}
</td>
<td class="sha">
- <a rel="nofollow" class="ui sha label {{if .Signature}} isSigned {{if .Verification.Verified }} isVerified {{end}}{{end}}" href="{{AppSubUrl}}/{{$.Username}}/{{$.Reponame}}/commit/{{.ID}}">
+ <a rel="nofollow" class="ui sha label {{if .Signature}} isSigned {{if .Verification.Verified }} isVerified {{else if .Verification.Warning}} isWarning {{end}}{{end}}" href="{{AppSubUrl}}/{{$.Username}}/{{$.Reponame}}/commit/{{.ID}}">
{{ShortSha .ID.String}}
{{if .Signature}}
<div class="ui detail icon button">
{{if .Verification.Verified}}
- <i title="{{.Verification.Reason}}" class="lock green icon"></i>
+ {{if ne .Verification.SigningUser.ID 0}}
+ <i title="{{.Verification.Reason}}" class="lock green icon"></i>
+ {{else}}
+ <i title="{{.Verification.Reason}}" class="icons">
+ <i class="green lock icon"></i>
+ <i class="tiny inverted cog icon centerlock"></i>
+ </i>
+ {{end}}
+ {{else if .Verification.Warning}}
+ <i title="{{$.i18n.Tr .Verification.Reason}}" class="red unlock icon"></i>
{{else}}
<i title="{{$.i18n.Tr .Verification.Reason}}" class="unlock icon"></i>
{{end}}
diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl
index d8750d8bcc..5be36d23be 100644
--- a/templates/swagger/v1_json.tmpl
+++ b/templates/swagger/v1_json.tmpl
@@ -5140,6 +5140,42 @@
}
}
},
+ "/repos/{owner}/{repo}/signing-key.gpg": {
+ "get": {
+ "produces": [
+ "text/plain"
+ ],
+ "tags": [
+ "repository"
+ ],
+ "summary": "Get signing-key.gpg for given repository",
+ "operationId": "repoSigningKey",
+ "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": "GPG armored public key",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
"/repos/{owner}/{repo}/stargazers": {
"get": {
"produces": [
@@ -5691,6 +5727,26 @@
}
}
},
+ "/signing-key.gpg": {
+ "get": {
+ "produces": [
+ "text/plain"
+ ],
+ "tags": [
+ "miscellaneous"
+ ],
+ "summary": "Get default signing-key.gpg",
+ "operationId": "getSigningKey",
+ "responses": {
+ "200": {
+ "description": "GPG armored public key",
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
"/teams/{id}": {
"get": {
"produces": [
@@ -9525,6 +9581,9 @@
"type": "string",
"x-go-name": "Signature"
},
+ "signer": {
+ "$ref": "#/definitions/PayloadUser"
+ },
"verified": {
"type": "boolean",
"x-go-name": "Verified"