From fcb535c5c3b6b782d9242028fed4cd8c027c4e41 Mon Sep 17 00:00:00 2001
From: zeripath <art27@cantab.net>
Date: Wed, 16 Oct 2019 14:42:42 +0100
Subject: [PATCH] Sign merges, CRUD, Wiki and Repository initialisation with
 gpg key (#7631)

This PR fixes #7598 by providing a configurable way of signing commits across the Gitea instance. Per repository configurability and import/generation of trusted secure keys is not provided by this PR - from a security PoV that's probably impossible to do properly. Similarly web-signing, that is asking the user to sign something, is not implemented - this could be done at a later stage however.

## Features
- [x] If commit.gpgsign is set in .gitconfig sign commits and files created through repofiles. (merges should already have been signed.)
- [x] Verify commits signed with the default gpg as valid
- [x] Signer, Committer and Author can all be different
    - [x] Allow signer to be arbitrarily different - We still require the key to have an activated email on Gitea. A more complete implementation would be to use a keyserver and mark external-or-unactivated with an "unknown" trust level icon.
- [x] Add a signing-key.gpg endpoint to get the default gpg pub key if available
    - Rather than add a fake web-flow user I've added this as an endpoint on /api/v1/signing-key.gpg
    - [x] Try to match the default key with a user on gitea - this is done at verification time
- [x] Make things configurable?
    - app.ini configuration done
    - [x] when checking commits are signed need to check if they're actually verifiable too
- [x] Add documentation

I have decided that adjusting the docker to create a default gpg key is not the correct thing to do and therefore have not implemented this.
---
 Makefile                                      |   4 +
 custom/conf/app.ini.sample                    |  31 ++
 .../doc/advanced/config-cheat-sheet.en-us.md  |  19 +
 docs/content/doc/advanced/signing.en-us.md    | 162 ++++++++
 .../api_helper_for_declarative_test.go        |  35 ++
 integrations/api_repo_file_create_test.go     |   2 +-
 integrations/api_repo_file_update_test.go     |   2 +-
 integrations/gpg_git_test.go                  | 252 ++++++++++++
 integrations/mssql.ini.tmpl                   |   3 +
 integrations/mysql.ini.tmpl                   |   3 +
 integrations/mysql8.ini.tmpl                  |   3 +
 integrations/pgsql.ini.tmpl                   |   3 +
 integrations/repofiles_delete_test.go         |   2 +-
 integrations/repofiles_update_test.go         |   4 +-
 integrations/sqlite.ini                       |   3 +
 models/gpg_key.go                             | 365 ++++++++++++++----
 models/repo.go                                |  60 ++-
 models/repo_sign.go                           | 303 +++++++++++++++
 models/wiki.go                                |  21 +-
 modules/git/commit.go                         |   8 +
 modules/git/repo.go                           |  10 +
 modules/git/repo_gpg.go                       |  59 +++
 modules/git/repo_tree.go                      |  11 +-
 modules/git/utils.go                          |  28 ++
 modules/repofiles/file_test.go                |   2 +-
 modules/repofiles/temp_repo.go                |  13 +-
 modules/repofiles/verification.go             |  14 +-
 modules/setting/repository.go                 |  29 ++
 modules/structs/hook.go                       |   9 +-
 options/locale/locale_en-US.ini               |   3 +
 public/css/index.css                          |   6 +
 public/less/_base.less                        |  16 +
 public/less/_repository.less                  |   9 +
 routers/api/v1/api.go                         |   2 +
 routers/api/v1/convert/convert.go             |  21 +-
 routers/api/v1/misc/signing.go                |  62 +++
 services/pull/merge.go                        |  64 ++-
 templates/repo/commit_page.tmpl               |  39 +-
 templates/repo/commits_table.tmpl             |  13 +-
 templates/swagger/v1_json.tmpl                |  59 +++
 40 files changed, 1630 insertions(+), 124 deletions(-)
 create mode 100644 docs/content/doc/advanced/signing.en-us.md
 create mode 100644 integrations/gpg_git_test.go
 create mode 100644 models/repo_sign.go
 create mode 100644 modules/git/repo_gpg.go
 create mode 100644 routers/api/v1/misc/signing.go

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` &amp; `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",
-				}
+			commitVerification := hashAndVerifyWithSubKeys(sig, c.Signature.Payload, k, committer, committer, email)
+			if commitVerification != nil {
+				return commitVerification
 			}
-			//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,
-				}
-			}
-			//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,
-					}
-				}
-			}
-		}
-		return &CommitVerification{ //Default at this stage
-			Verified: false,
-			Reason:   "gpg.error.no_gpg_keys_found",
 		}
 	}
 
-	return &CommitVerification{
-		Verified: false,                         //Default value
-		Reason:   "gpg.error.not_signed_commit", //Default value
+	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
+			}
+		}
 	}
+
+	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{ //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"