From 3ca5dc7e32b372d14ff80d96f14b8f6a805862f1 Mon Sep 17 00:00:00 2001
From: Lunny Xiao <xiaolunwen@gmail.com>
Date: Fri, 10 Dec 2021 16:14:24 +0800
Subject: [PATCH] Move keys to models/asymkey (#17917)

* Move keys to models/keys

* Rename models/keys -> models/asymkey

* change the missed package name

* Fix package alias

* Fix test

* Fix docs

* Fix test

* Fix test

* merge
---
 cmd/admin.go                                  |   3 +-
 cmd/serv.go                                   |   5 +-
 .../doc/advanced/config-cheat-sheet.en-us.md  |   2 +-
 integrations/api_admin_test.go                |   6 +-
 integrations/api_keys_test.go                 |   8 +-
 integrations/api_private_serv_test.go         |   8 +-
 models/asymkey/error.go                       | 248 ++++++++++++++++
 models/{ => asymkey}/gpg_key.go               |  11 +-
 models/{ => asymkey}/gpg_key_add.go           |   2 +-
 .../gpg_key_commit_verification.go            |  24 +-
 models/{ => asymkey}/gpg_key_common.go        |   2 +-
 models/{ => asymkey}/gpg_key_import.go        |   2 +-
 models/{ => asymkey}/gpg_key_test.go          |   2 +-
 models/{ => asymkey}/gpg_key_verify.go        |   2 +-
 models/asymkey/main_test.go                   |  29 ++
 models/{ => asymkey}/ssh_key.go               |  45 +--
 .../{ => asymkey}/ssh_key_authorized_keys.go  |   8 +-
 .../ssh_key_authorized_principals.go          |   2 +-
 models/{ => asymkey}/ssh_key_deploy.go        |  82 +-----
 models/{ => asymkey}/ssh_key_fingerprint.go   |   2 +-
 models/{ => asymkey}/ssh_key_parse.go         |   5 +-
 models/{ => asymkey}/ssh_key_principals.go    |   4 +-
 models/{ => asymkey}/ssh_key_test.go          |   7 +-
 models/commit.go                              |   8 +-
 models/commit_status.go                       |   5 +-
 models/db/error.go                            |  13 +
 models/error.go                               | 277 ------------------
 models/main_test.go                           |   6 +
 models/pull_sign.go                           | 133 ---------
 models/repo.go                                |  61 +++-
 models/statistic.go                           |   3 +-
 models/user.go                                |   9 +-
 models/user_test.go                           |  69 -----
 modules/context/repo.go                       |   7 +-
 modules/convert/convert.go                    |  13 +-
 modules/doctor/authorizedkeys.go              |   8 +-
 modules/gitgraph/graph_models.go              |   9 +-
 modules/private/serv.go                       |   8 +-
 modules/repository/init.go                    |   3 +-
 modules/ssh/ssh.go                            |  10 +-
 routers/api/v1/admin/user.go                  |   8 +-
 routers/api/v1/misc/signing.go                |   4 +-
 routers/api/v1/repo/key.go                    |  36 +--
 routers/api/v1/repo/pull.go                   |   3 +-
 routers/api/v1/repo/repo.go                   |   2 +-
 routers/api/v1/user/gpg_key.go                |  52 ++--
 routers/api/v1/user/key.go                    |  35 +--
 routers/init.go                               |   3 +-
 routers/private/hook_verification.go          |   4 +-
 routers/private/key.go                        |  12 +-
 routers/private/serv.go                       |  23 +-
 routers/web/admin/repos.go                    |   2 +-
 routers/web/repo/commit.go                    |   7 +-
 routers/web/repo/issue.go                     |   7 +-
 routers/web/repo/setting.go                   |  29 +-
 routers/web/repo/settings_test.go             |   5 +-
 routers/web/repo/view.go                      |   7 +-
 routers/web/user/home.go                      |   9 +-
 routers/web/user/setting/keys.go              |  79 ++---
 services/asymkey/deploy_key.go                |  30 ++
 services/asymkey/main_test.go                 |  16 +
 .../repo_sign.go => services/asymkey/sign.go  | 163 ++++++++++-
 services/asymkey/ssh_key.go                   |  49 ++++
 services/asymkey/ssh_key_test.go              |  83 ++++++
 .../auth/source/ldap/source_authenticate.go   |  10 +-
 services/auth/source/ldap/source_sync.go      |  10 +-
 services/cron/tasks_extended.go               |   5 +-
 services/pull/merge.go                        |   5 +-
 services/repository/archiver/archiver.go      |   3 +-
 services/repository/files/commit.go           |   3 +-
 services/repository/files/temp_repo.go        |   3 +-
 services/repository/files/update.go           |   5 +-
 services/repository/repository.go             |   8 +-
 services/user/user.go                         |  12 +-
 services/wiki/wiki.go                         |   5 +-
 75 files changed, 1001 insertions(+), 887 deletions(-)
 create mode 100644 models/asymkey/error.go
 rename models/{ => asymkey}/gpg_key.go (95%)
 rename models/{ => asymkey}/gpg_key_add.go (99%)
 rename models/{ => asymkey}/gpg_key_commit_verification.go (94%)
 rename models/{ => asymkey}/gpg_key_common.go (99%)
 rename models/{ => asymkey}/gpg_key_import.go (98%)
 rename models/{ => asymkey}/gpg_key_test.go (99%)
 rename models/{ => asymkey}/gpg_key_verify.go (99%)
 create mode 100644 models/asymkey/main_test.go
 rename models/{ => asymkey}/ssh_key.go (93%)
 rename models/{ => asymkey}/ssh_key_authorized_keys.go (97%)
 rename models/{ => asymkey}/ssh_key_authorized_principals.go (99%)
 rename models/{ => asymkey}/ssh_key_deploy.go (75%)
 rename models/{ => asymkey}/ssh_key_fingerprint.go (99%)
 rename models/{ => asymkey}/ssh_key_parse.go (99%)
 rename models/{ => asymkey}/ssh_key_principals.go (98%)
 rename models/{ => asymkey}/ssh_key_test.go (99%)
 delete mode 100644 models/pull_sign.go
 create mode 100644 services/asymkey/deploy_key.go
 create mode 100644 services/asymkey/main_test.go
 rename models/repo_sign.go => services/asymkey/sign.go (57%)
 create mode 100644 services/asymkey/ssh_key.go
 create mode 100644 services/asymkey/ssh_key_test.go

diff --git a/cmd/admin.go b/cmd/admin.go
index d11b824fa1..f36e9f5de7 100644
--- a/cmd/admin.go
+++ b/cmd/admin.go
@@ -14,6 +14,7 @@ import (
 	"text/tabwriter"
 
 	"code.gitea.io/gitea/models"
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/login"
 	user_model "code.gitea.io/gitea/models/user"
@@ -625,7 +626,7 @@ func runRegenerateKeys(_ *cli.Context) error {
 	if err := initDB(ctx); err != nil {
 		return err
 	}
-	return models.RewriteAllPublicKeys()
+	return asymkey_model.RewriteAllPublicKeys()
 }
 
 func parseOAuth2Config(c *cli.Context) *oauth2.Source {
diff --git a/cmd/serv.go b/cmd/serv.go
index e34e300cbe..c3c9eff195 100644
--- a/cmd/serv.go
+++ b/cmd/serv.go
@@ -17,6 +17,7 @@ import (
 	"time"
 
 	"code.gitea.io/gitea/models"
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/perm"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/json"
@@ -129,9 +130,9 @@ func runServ(c *cli.Context) error {
 			return fail("Internal error", "Failed to check provided key: %v", err)
 		}
 		switch key.Type {
-		case models.KeyTypeDeploy:
+		case asymkey_model.KeyTypeDeploy:
 			println("Hi there! You've successfully authenticated with the deploy key named " + key.Name + ", but Gitea does not provide shell access.")
-		case models.KeyTypePrincipal:
+		case asymkey_model.KeyTypePrincipal:
 			println("Hi there! You've successfully authenticated with the principal " + key.Content + ", but Gitea does not provide shell access.")
 		default:
 			println("Hi there, " + user.Name + "! You've successfully authenticated with the key named " + key.Name + ", but Gitea does not provide shell access.")
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 bb2eb1c8ca..e2a2687fa9 100644
--- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md
+++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
@@ -277,7 +277,7 @@ The following configuration set `Content-Type: application/vnd.android.package-a
 - `SSH_AUTHORIZED_PRINCIPALS_ALLOW`: **off** or **username, email**: \[off, username, email, anything\]: Specify the principals values that users are allowed to use as principal. When set to `anything` no checks are done on the principal string. When set to `off` authorized principal are not allowed to be set.
 - `SSH_CREATE_AUTHORIZED_PRINCIPALS_FILE`: **false/true**: Gitea will create a authorized_principals file by default when it is not using the internal ssh server and `SSH_AUTHORIZED_PRINCIPALS_ALLOW` is not `off`.
 - `SSH_AUTHORIZED_PRINCIPALS_BACKUP`: **false/true**: Enable SSH Authorized Principals Backup when rewriting all keys, default is true if `SSH_AUTHORIZED_PRINCIPALS_ALLOW` is not `off`.
-- `SSH_AUTHORIZED_KEYS_COMMAND_TEMPLATE`: **{{.AppPath}} --config={{.CustomConf}} serv key-{{.Key.ID}}**: Set the template for the command to passed on authorized keys. Possible keys are: AppPath, AppWorkPath, CustomConf, CustomPath, Key - where Key is a `models.PublicKey` and the others are strings which are shellquoted.
+- `SSH_AUTHORIZED_KEYS_COMMAND_TEMPLATE`: **{{.AppPath}} --config={{.CustomConf}} serv key-{{.Key.ID}}**: Set the template for the command to passed on authorized keys. Possible keys are: AppPath, AppWorkPath, CustomConf, CustomPath, Key - where Key is a `models/asymkey.PublicKey` and the others are strings which are shellquoted.
 - `SSH_SERVER_CIPHERS`: **aes128-ctr, aes192-ctr, aes256-ctr, aes128-gcm@openssh.com, arcfour256, arcfour128**: For the built-in SSH server, choose the ciphers to support for SSH connections, for system SSH this setting has no effect.
 - `SSH_SERVER_KEY_EXCHANGES`: **diffie-hellman-group1-sha1, diffie-hellman-group14-sha1, ecdh-sha2-nistp256, ecdh-sha2-nistp384, ecdh-sha2-nistp521, curve25519-sha256@libssh.org**: For the built-in SSH server, choose the key exchange algorithms to support for SSH connections, for system SSH this setting has no effect.
 - `SSH_SERVER_MACS`: **hmac-sha2-256-etm@openssh.com, hmac-sha2-256, hmac-sha1, hmac-sha1-96**: For the built-in SSH server, choose the MACs to support for SSH connections, for system SSH this setting has no effect
diff --git a/integrations/api_admin_test.go b/integrations/api_admin_test.go
index 98270ed587..b935d3eac5 100644
--- a/integrations/api_admin_test.go
+++ b/integrations/api_admin_test.go
@@ -9,7 +9,7 @@ import (
 	"net/http"
 	"testing"
 
-	"code.gitea.io/gitea/models"
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/json"
@@ -34,7 +34,7 @@ func TestAPIAdminCreateAndDeleteSSHKey(t *testing.T) {
 
 	var newPublicKey api.PublicKey
 	DecodeJSON(t, resp, &newPublicKey)
-	unittest.AssertExistsAndLoadBean(t, &models.PublicKey{
+	unittest.AssertExistsAndLoadBean(t, &asymkey_model.PublicKey{
 		ID:          newPublicKey.ID,
 		Name:        newPublicKey.Title,
 		Content:     newPublicKey.Key,
@@ -45,7 +45,7 @@ func TestAPIAdminCreateAndDeleteSSHKey(t *testing.T) {
 	req = NewRequestf(t, "DELETE", "/api/v1/admin/users/%s/keys/%d?token=%s",
 		keyOwner.Name, newPublicKey.ID, token)
 	session.MakeRequest(t, req, http.StatusNoContent)
-	unittest.AssertNotExistsBean(t, &models.PublicKey{ID: newPublicKey.ID})
+	unittest.AssertNotExistsBean(t, &asymkey_model.PublicKey{ID: newPublicKey.ID})
 }
 
 func TestAPIAdminDeleteMissingSSHKey(t *testing.T) {
diff --git a/integrations/api_keys_test.go b/integrations/api_keys_test.go
index 5fbd51318f..b1f455d932 100644
--- a/integrations/api_keys_test.go
+++ b/integrations/api_keys_test.go
@@ -10,7 +10,7 @@ import (
 	"net/url"
 	"testing"
 
-	"code.gitea.io/gitea/models"
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/perm"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
@@ -65,7 +65,7 @@ func TestCreateReadOnlyDeployKey(t *testing.T) {
 
 	var newDeployKey api.DeployKey
 	DecodeJSON(t, resp, &newDeployKey)
-	unittest.AssertExistsAndLoadBean(t, &models.DeployKey{
+	unittest.AssertExistsAndLoadBean(t, &asymkey_model.DeployKey{
 		ID:      newDeployKey.ID,
 		Name:    rawKeyBody.Title,
 		Content: rawKeyBody.Key,
@@ -90,7 +90,7 @@ func TestCreateReadWriteDeployKey(t *testing.T) {
 
 	var newDeployKey api.DeployKey
 	DecodeJSON(t, resp, &newDeployKey)
-	unittest.AssertExistsAndLoadBean(t, &models.DeployKey{
+	unittest.AssertExistsAndLoadBean(t, &asymkey_model.DeployKey{
 		ID:      newDeployKey.ID,
 		Name:    rawKeyBody.Title,
 		Content: rawKeyBody.Key,
@@ -116,7 +116,7 @@ func TestCreateUserKey(t *testing.T) {
 
 	var newPublicKey api.PublicKey
 	DecodeJSON(t, resp, &newPublicKey)
-	unittest.AssertExistsAndLoadBean(t, &models.PublicKey{
+	unittest.AssertExistsAndLoadBean(t, &asymkey_model.PublicKey{
 		ID:      newPublicKey.ID,
 		OwnerID: user.ID,
 		Name:    rawKeyBody.Title,
diff --git a/integrations/api_private_serv_test.go b/integrations/api_private_serv_test.go
index 388e5da00d..68308bafc5 100644
--- a/integrations/api_private_serv_test.go
+++ b/integrations/api_private_serv_test.go
@@ -9,7 +9,7 @@ import (
 	"net/url"
 	"testing"
 
-	"code.gitea.io/gitea/models"
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/perm"
 	"code.gitea.io/gitea/modules/private"
 
@@ -27,7 +27,7 @@ func TestAPIPrivateNoServ(t *testing.T) {
 		assert.Equal(t, int64(1), key.ID)
 		assert.Equal(t, "user2@localhost", key.Name)
 
-		deployKey, err := models.AddDeployKey(1, "test-deploy", "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBGXEEzWmm1dxb+57RoK5KVCL0w2eNv9cqJX2AGGVlkFsVDhOXHzsadS3LTK4VlEbbrDMJdoti9yM8vclA8IeRacAAAAEc3NoOg== nocomment", false)
+		deployKey, err := asymkey_model.AddDeployKey(1, "test-deploy", "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBGXEEzWmm1dxb+57RoK5KVCL0w2eNv9cqJX2AGGVlkFsVDhOXHzsadS3LTK4VlEbbrDMJdoti9yM8vclA8IeRacAAAAEc3NoOg== nocomment", false)
 		assert.NoError(t, err)
 
 		key, user, err = private.ServNoCommand(ctx, deployKey.KeyID)
@@ -85,7 +85,7 @@ func TestAPIPrivateServ(t *testing.T) {
 		assert.Empty(t, results)
 
 		// Add reading deploy key
-		deployKey, err := models.AddDeployKey(19, "test-deploy", "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBGXEEzWmm1dxb+57RoK5KVCL0w2eNv9cqJX2AGGVlkFsVDhOXHzsadS3LTK4VlEbbrDMJdoti9yM8vclA8IeRacAAAAEc3NoOg== nocomment", true)
+		deployKey, err := asymkey_model.AddDeployKey(19, "test-deploy", "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBGXEEzWmm1dxb+57RoK5KVCL0w2eNv9cqJX2AGGVlkFsVDhOXHzsadS3LTK4VlEbbrDMJdoti9yM8vclA8IeRacAAAAEc3NoOg== nocomment", true)
 		assert.NoError(t, err)
 
 		// Can pull from repo we're a deploy key for
@@ -117,7 +117,7 @@ func TestAPIPrivateServ(t *testing.T) {
 		assert.Empty(t, results)
 
 		// Add writing deploy key
-		deployKey, err = models.AddDeployKey(20, "test-deploy", "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBGXEEzWmm1dxb+57RoK5KVCL0w2eNv9cqJX2AGGVlkFsVDhOXHzsadS3LTK4VlEbbrDMJdoti9yM8vclA8IeRacAAAAEc3NoOg== nocomment", false)
+		deployKey, err = asymkey_model.AddDeployKey(20, "test-deploy", "sk-ecdsa-sha2-nistp256@openssh.com AAAAInNrLWVjZHNhLXNoYTItbmlzdHAyNTZAb3BlbnNzaC5jb20AAAAIbmlzdHAyNTYAAABBBGXEEzWmm1dxb+57RoK5KVCL0w2eNv9cqJX2AGGVlkFsVDhOXHzsadS3LTK4VlEbbrDMJdoti9yM8vclA8IeRacAAAAEc3NoOg== nocomment", false)
 		assert.NoError(t, err)
 
 		// Cannot push to a private repo with reading key
diff --git a/models/asymkey/error.go b/models/asymkey/error.go
new file mode 100644
index 0000000000..7add553bd6
--- /dev/null
+++ b/models/asymkey/error.go
@@ -0,0 +1,248 @@
+// Copyright 2021 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 asymkey
+
+import "fmt"
+
+// ErrKeyUnableVerify represents a "KeyUnableVerify" kind of error.
+type ErrKeyUnableVerify struct {
+	Result string
+}
+
+// IsErrKeyUnableVerify checks if an error is a ErrKeyUnableVerify.
+func IsErrKeyUnableVerify(err error) bool {
+	_, ok := err.(ErrKeyUnableVerify)
+	return ok
+}
+
+func (err ErrKeyUnableVerify) Error() string {
+	return fmt.Sprintf("Unable to verify key content [result: %s]", err.Result)
+}
+
+// ErrKeyNotExist represents a "KeyNotExist" kind of error.
+type ErrKeyNotExist struct {
+	ID int64
+}
+
+// IsErrKeyNotExist checks if an error is a ErrKeyNotExist.
+func IsErrKeyNotExist(err error) bool {
+	_, ok := err.(ErrKeyNotExist)
+	return ok
+}
+
+func (err ErrKeyNotExist) Error() string {
+	return fmt.Sprintf("public key does not exist [id: %d]", err.ID)
+}
+
+// ErrKeyAlreadyExist represents a "KeyAlreadyExist" kind of error.
+type ErrKeyAlreadyExist struct {
+	OwnerID     int64
+	Fingerprint string
+	Content     string
+}
+
+// IsErrKeyAlreadyExist checks if an error is a ErrKeyAlreadyExist.
+func IsErrKeyAlreadyExist(err error) bool {
+	_, ok := err.(ErrKeyAlreadyExist)
+	return ok
+}
+
+func (err ErrKeyAlreadyExist) Error() string {
+	return fmt.Sprintf("public key already exists [owner_id: %d, finger_print: %s, content: %s]",
+		err.OwnerID, err.Fingerprint, err.Content)
+}
+
+// ErrKeyNameAlreadyUsed represents a "KeyNameAlreadyUsed" kind of error.
+type ErrKeyNameAlreadyUsed struct {
+	OwnerID int64
+	Name    string
+}
+
+// IsErrKeyNameAlreadyUsed checks if an error is a ErrKeyNameAlreadyUsed.
+func IsErrKeyNameAlreadyUsed(err error) bool {
+	_, ok := err.(ErrKeyNameAlreadyUsed)
+	return ok
+}
+
+func (err ErrKeyNameAlreadyUsed) Error() string {
+	return fmt.Sprintf("public key already exists [owner_id: %d, name: %s]", err.OwnerID, err.Name)
+}
+
+// ErrGPGNoEmailFound represents a "ErrGPGNoEmailFound" kind of error.
+type ErrGPGNoEmailFound struct {
+	FailedEmails []string
+	ID           string
+}
+
+// IsErrGPGNoEmailFound checks if an error is a ErrGPGNoEmailFound.
+func IsErrGPGNoEmailFound(err error) bool {
+	_, ok := err.(ErrGPGNoEmailFound)
+	return ok
+}
+
+func (err ErrGPGNoEmailFound) Error() string {
+	return fmt.Sprintf("none of the emails attached to the GPG key could be found: %v", err.FailedEmails)
+}
+
+// ErrGPGInvalidTokenSignature represents a "ErrGPGInvalidTokenSignature" kind of error.
+type ErrGPGInvalidTokenSignature struct {
+	Wrapped error
+	ID      string
+}
+
+// IsErrGPGInvalidTokenSignature checks if an error is a ErrGPGInvalidTokenSignature.
+func IsErrGPGInvalidTokenSignature(err error) bool {
+	_, ok := err.(ErrGPGInvalidTokenSignature)
+	return ok
+}
+
+func (err ErrGPGInvalidTokenSignature) Error() string {
+	return "the provided signature does not sign the token with the provided key"
+}
+
+// ErrGPGKeyParsing represents a "ErrGPGKeyParsing" kind of error.
+type ErrGPGKeyParsing struct {
+	ParseError error
+}
+
+// IsErrGPGKeyParsing checks if an error is a ErrGPGKeyParsing.
+func IsErrGPGKeyParsing(err error) bool {
+	_, ok := err.(ErrGPGKeyParsing)
+	return ok
+}
+
+func (err ErrGPGKeyParsing) Error() string {
+	return fmt.Sprintf("failed to parse gpg key %s", err.ParseError.Error())
+}
+
+// ErrGPGKeyNotExist represents a "GPGKeyNotExist" kind of error.
+type ErrGPGKeyNotExist struct {
+	ID int64
+}
+
+// IsErrGPGKeyNotExist checks if an error is a ErrGPGKeyNotExist.
+func IsErrGPGKeyNotExist(err error) bool {
+	_, ok := err.(ErrGPGKeyNotExist)
+	return ok
+}
+
+func (err ErrGPGKeyNotExist) Error() string {
+	return fmt.Sprintf("public gpg key does not exist [id: %d]", err.ID)
+}
+
+// ErrGPGKeyImportNotExist represents a "GPGKeyImportNotExist" kind of error.
+type ErrGPGKeyImportNotExist struct {
+	ID string
+}
+
+// IsErrGPGKeyImportNotExist checks if an error is a ErrGPGKeyImportNotExist.
+func IsErrGPGKeyImportNotExist(err error) bool {
+	_, ok := err.(ErrGPGKeyImportNotExist)
+	return ok
+}
+
+func (err ErrGPGKeyImportNotExist) Error() string {
+	return fmt.Sprintf("public gpg key import does not exist [id: %s]", err.ID)
+}
+
+// ErrGPGKeyIDAlreadyUsed represents a "GPGKeyIDAlreadyUsed" kind of error.
+type ErrGPGKeyIDAlreadyUsed struct {
+	KeyID string
+}
+
+// IsErrGPGKeyIDAlreadyUsed checks if an error is a ErrKeyNameAlreadyUsed.
+func IsErrGPGKeyIDAlreadyUsed(err error) bool {
+	_, ok := err.(ErrGPGKeyIDAlreadyUsed)
+	return ok
+}
+
+func (err ErrGPGKeyIDAlreadyUsed) Error() string {
+	return fmt.Sprintf("public key already exists [key_id: %s]", err.KeyID)
+}
+
+// ErrGPGKeyAccessDenied represents a "GPGKeyAccessDenied" kind of Error.
+type ErrGPGKeyAccessDenied struct {
+	UserID int64
+	KeyID  int64
+}
+
+// IsErrGPGKeyAccessDenied checks if an error is a ErrGPGKeyAccessDenied.
+func IsErrGPGKeyAccessDenied(err error) bool {
+	_, ok := err.(ErrGPGKeyAccessDenied)
+	return ok
+}
+
+// Error pretty-prints an error of type ErrGPGKeyAccessDenied.
+func (err ErrGPGKeyAccessDenied) Error() string {
+	return fmt.Sprintf("user does not have access to the key [user_id: %d, key_id: %d]",
+		err.UserID, err.KeyID)
+}
+
+// ErrKeyAccessDenied represents a "KeyAccessDenied" kind of error.
+type ErrKeyAccessDenied struct {
+	UserID int64
+	KeyID  int64
+	Note   string
+}
+
+// IsErrKeyAccessDenied checks if an error is a ErrKeyAccessDenied.
+func IsErrKeyAccessDenied(err error) bool {
+	_, ok := err.(ErrKeyAccessDenied)
+	return ok
+}
+
+func (err ErrKeyAccessDenied) Error() string {
+	return fmt.Sprintf("user does not have access to the key [user_id: %d, key_id: %d, note: %s]",
+		err.UserID, err.KeyID, err.Note)
+}
+
+// ErrDeployKeyNotExist represents a "DeployKeyNotExist" kind of error.
+type ErrDeployKeyNotExist struct {
+	ID     int64
+	KeyID  int64
+	RepoID int64
+}
+
+// IsErrDeployKeyNotExist checks if an error is a ErrDeployKeyNotExist.
+func IsErrDeployKeyNotExist(err error) bool {
+	_, ok := err.(ErrDeployKeyNotExist)
+	return ok
+}
+
+func (err ErrDeployKeyNotExist) Error() string {
+	return fmt.Sprintf("Deploy key does not exist [id: %d, key_id: %d, repo_id: %d]", err.ID, err.KeyID, err.RepoID)
+}
+
+// ErrDeployKeyAlreadyExist represents a "DeployKeyAlreadyExist" kind of error.
+type ErrDeployKeyAlreadyExist struct {
+	KeyID  int64
+	RepoID int64
+}
+
+// IsErrDeployKeyAlreadyExist checks if an error is a ErrDeployKeyAlreadyExist.
+func IsErrDeployKeyAlreadyExist(err error) bool {
+	_, ok := err.(ErrDeployKeyAlreadyExist)
+	return ok
+}
+
+func (err ErrDeployKeyAlreadyExist) Error() string {
+	return fmt.Sprintf("public key already exists [key_id: %d, repo_id: %d]", err.KeyID, err.RepoID)
+}
+
+// ErrDeployKeyNameAlreadyUsed represents a "DeployKeyNameAlreadyUsed" kind of error.
+type ErrDeployKeyNameAlreadyUsed struct {
+	RepoID int64
+	Name   string
+}
+
+// IsErrDeployKeyNameAlreadyUsed checks if an error is a ErrDeployKeyNameAlreadyUsed.
+func IsErrDeployKeyNameAlreadyUsed(err error) bool {
+	_, ok := err.(ErrDeployKeyNameAlreadyUsed)
+	return ok
+}
+
+func (err ErrDeployKeyNameAlreadyUsed) Error() string {
+	return fmt.Sprintf("public key with name already exists [repo_id: %d, name: %s]", err.RepoID, err.Name)
+}
diff --git a/models/gpg_key.go b/models/asymkey/gpg_key.go
similarity index 95%
rename from models/gpg_key.go
rename to models/asymkey/gpg_key.go
index ce27a9237e..ced6ca37a3 100644
--- a/models/gpg_key.go
+++ b/models/asymkey/gpg_key.go
@@ -2,9 +2,10 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package models
+package asymkey
 
 import (
+	"context"
 	"fmt"
 	"strings"
 	"time"
@@ -63,12 +64,8 @@ func (key *GPGKey) AfterLoad(session *xorm.Session) {
 }
 
 // ListGPGKeys returns a list of public keys belongs to given user.
-func ListGPGKeys(uid int64, listOptions db.ListOptions) ([]*GPGKey, error) {
-	return listGPGKeys(db.GetEngine(db.DefaultContext), uid, listOptions)
-}
-
-func listGPGKeys(e db.Engine, uid int64, listOptions db.ListOptions) ([]*GPGKey, error) {
-	sess := e.Table(&GPGKey{}).Where("owner_id=? AND primary_key_id=''", uid)
+func ListGPGKeys(ctx context.Context, uid int64, listOptions db.ListOptions) ([]*GPGKey, error) {
+	sess := db.GetEngine(ctx).Table(&GPGKey{}).Where("owner_id=? AND primary_key_id=''", uid)
 	if listOptions.Page != 0 {
 		sess = db.SetSessionPagination(sess, &listOptions)
 	}
diff --git a/models/gpg_key_add.go b/models/asymkey/gpg_key_add.go
similarity index 99%
rename from models/gpg_key_add.go
rename to models/asymkey/gpg_key_add.go
index 711fc86dee..8f84bba1df 100644
--- a/models/gpg_key_add.go
+++ b/models/asymkey/gpg_key_add.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package models
+package asymkey
 
 import (
 	"strings"
diff --git a/models/gpg_key_commit_verification.go b/models/asymkey/gpg_key_commit_verification.go
similarity index 94%
rename from models/gpg_key_commit_verification.go
rename to models/asymkey/gpg_key_commit_verification.go
index 48f58c07b6..5ec4e335d5 100644
--- a/models/gpg_key_commit_verification.go
+++ b/models/asymkey/gpg_key_commit_verification.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package models
+package asymkey
 
 import (
 	"fmt"
@@ -70,7 +70,7 @@ const (
 )
 
 // ParseCommitsWithSignature checks if signaute of commits are corresponding to users gpg keys.
-func ParseCommitsWithSignature(oldCommits []*user_model.UserCommit, repository *repo_model.Repository) []*SignCommit {
+func ParseCommitsWithSignature(oldCommits []*user_model.UserCommit, repoTrustModel repo_model.TrustModelType, isCodeReader func(*user_model.User) (bool, error)) []*SignCommit {
 	newCommits := make([]*SignCommit, 0, len(oldCommits))
 	keyMap := map[string]bool{}
 
@@ -80,7 +80,7 @@ func ParseCommitsWithSignature(oldCommits []*user_model.UserCommit, repository *
 			Verification: ParseCommitWithSignature(c.Commit),
 		}
 
-		_ = CalculateTrustStatus(signCommit.Verification, repository, &keyMap)
+		_ = CalculateTrustStatus(signCommit.Verification, repoTrustModel, isCodeReader, &keyMap)
 
 		newCommits = append(newCommits, signCommit)
 	}
@@ -159,7 +159,7 @@ func ParseCommitWithSignature(c *git.Commit) *CommitVerification {
 
 	// Now try to associate the signature with the committer, if present
 	if committer.ID != 0 {
-		keys, err := ListGPGKeys(committer.ID, db.ListOptions{})
+		keys, err := ListGPGKeys(db.DefaultContext, committer.ID, db.ListOptions{})
 		if err != nil { // Skipping failed to get gpg keys of user
 			log.Error("ListGPGKeys: %v", err)
 			return &CommitVerification{
@@ -448,18 +448,16 @@ func hashAndVerifyForKeyID(sig *packet.Signature, payload string, committer *use
 }
 
 // CalculateTrustStatus will calculate the TrustStatus for a commit verification within a repository
-func CalculateTrustStatus(verification *CommitVerification, repository *repo_model.Repository, keyMap *map[string]bool) (err error) {
+// There are several trust models in Gitea
+func CalculateTrustStatus(verification *CommitVerification, repoTrustModel repo_model.TrustModelType, isCodeReader func(*user_model.User) (bool, error), keyMap *map[string]bool) (err error) {
 	if !verification.Verified {
 		return
 	}
 
-	// There are several trust models in Gitea
-	trustModel := repository.GetTrustModel()
-
 	// In the Committer trust model a signature is trusted if it matches the committer
 	// - it doesn't matter if they're a collaborator, the owner, Gitea or Github
 	// NB: This model is commit verification only
-	if trustModel == repo_model.CommitterTrustModel {
+	if repoTrustModel == repo_model.CommitterTrustModel {
 		// default to "unmatched"
 		verification.TrustStatus = "unmatched"
 
@@ -482,7 +480,7 @@ func CalculateTrustStatus(verification *CommitVerification, repository *repo_mod
 
 		// However in the repo_model.CollaboratorCommitterTrustModel we cannot mark this as trusted
 		// unless the default key matches the email of a non-user.
-		if trustModel == repo_model.CollaboratorCommitterTrustModel && (verification.CommittingUser.ID != 0 ||
+		if repoTrustModel == repo_model.CollaboratorCommitterTrustModel && (verification.CommittingUser.ID != 0 ||
 			verification.SigningUser.Email != verification.CommittingUser.Email) {
 			verification.TrustStatus = "untrusted"
 		}
@@ -494,11 +492,11 @@ func CalculateTrustStatus(verification *CommitVerification, repository *repo_mod
 		var has bool
 		isMember, has = (*keyMap)[verification.SigningKey.KeyID]
 		if !has {
-			isMember, err = IsOwnerMemberCollaborator(repository, verification.SigningUser.ID)
+			isMember, err = isCodeReader(verification.SigningUser)
 			(*keyMap)[verification.SigningKey.KeyID] = isMember
 		}
 	} else {
-		isMember, err = IsOwnerMemberCollaborator(repository, verification.SigningUser.ID)
+		isMember, err = isCodeReader(verification.SigningUser)
 	}
 
 	if !isMember {
@@ -508,7 +506,7 @@ func CalculateTrustStatus(verification *CommitVerification, repository *repo_mod
 			// This should be marked as questionable unless the signing user is a collaborator/team member etc.
 			verification.TrustStatus = "unmatched"
 		}
-	} else if trustModel == repo_model.CollaboratorCommitterTrustModel && verification.CommittingUser.ID != verification.SigningUser.ID {
+	} else if repoTrustModel == repo_model.CollaboratorCommitterTrustModel && verification.CommittingUser.ID != verification.SigningUser.ID {
 		// The committing user and the signing user are not the same and our trustmodel states that they must match
 		verification.TrustStatus = "unmatched"
 	}
diff --git a/models/gpg_key_common.go b/models/asymkey/gpg_key_common.go
similarity index 99%
rename from models/gpg_key_common.go
rename to models/asymkey/gpg_key_common.go
index 72803625ee..1ea510c45f 100644
--- a/models/gpg_key_common.go
+++ b/models/asymkey/gpg_key_common.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package models
+package asymkey
 
 import (
 	"bytes"
diff --git a/models/gpg_key_import.go b/models/asymkey/gpg_key_import.go
similarity index 98%
rename from models/gpg_key_import.go
rename to models/asymkey/gpg_key_import.go
index 1eed929627..210c4b835b 100644
--- a/models/gpg_key_import.go
+++ b/models/asymkey/gpg_key_import.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package models
+package asymkey
 
 import "code.gitea.io/gitea/models/db"
 
diff --git a/models/gpg_key_test.go b/models/asymkey/gpg_key_test.go
similarity index 99%
rename from models/gpg_key_test.go
rename to models/asymkey/gpg_key_test.go
index 8f51c146aa..07bb77bdf4 100644
--- a/models/gpg_key_test.go
+++ b/models/asymkey/gpg_key_test.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package models
+package asymkey
 
 import (
 	"testing"
diff --git a/models/gpg_key_verify.go b/models/asymkey/gpg_key_verify.go
similarity index 99%
rename from models/gpg_key_verify.go
rename to models/asymkey/gpg_key_verify.go
index 1824086021..152765cc3a 100644
--- a/models/gpg_key_verify.go
+++ b/models/asymkey/gpg_key_verify.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package models
+package asymkey
 
 import (
 	"strconv"
diff --git a/models/asymkey/main_test.go b/models/asymkey/main_test.go
new file mode 100644
index 0000000000..1c4f7752e2
--- /dev/null
+++ b/models/asymkey/main_test.go
@@ -0,0 +1,29 @@
+// Copyright 2021 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 asymkey
+
+import (
+	"path/filepath"
+	"testing"
+
+	"code.gitea.io/gitea/models/unittest"
+	"code.gitea.io/gitea/modules/setting"
+)
+
+func init() {
+	setting.SetCustomPathAndConf("", "", "")
+	setting.LoadForTest()
+}
+
+func TestMain(m *testing.M) {
+	unittest.MainTest(m, filepath.Join("..", ".."),
+		"gpg_key.yml",
+		"public_key.yml",
+		"deploy_key.yml",
+		"gpg_key_import.yml",
+		"user.yml",
+		"email_address.yml",
+	)
+}
diff --git a/models/ssh_key.go b/models/asymkey/ssh_key.go
similarity index 93%
rename from models/ssh_key.go
rename to models/asymkey/ssh_key.go
index 0d97096149..cc63663221 100644
--- a/models/ssh_key.go
+++ b/models/asymkey/ssh_key.go
@@ -3,9 +3,10 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package models
+package asymkey
 
 import (
+	"context"
 	"fmt"
 	"strings"
 	"time"
@@ -247,13 +248,13 @@ func UpdatePublicKeyUpdated(id int64) error {
 	return nil
 }
 
-// deletePublicKeys does the actual key deletion but does not update authorized_keys file.
-func deletePublicKeys(e db.Engine, keyIDs ...int64) error {
+// DeletePublicKeys does the actual key deletion but does not update authorized_keys file.
+func DeletePublicKeys(ctx context.Context, keyIDs ...int64) error {
 	if len(keyIDs) == 0 {
 		return nil
 	}
 
-	_, err := e.In("id", keyIDs).Delete(new(PublicKey))
+	_, err := db.GetEngine(ctx).In("id", keyIDs).Delete(new(PublicKey))
 	return err
 }
 
@@ -325,40 +326,6 @@ func PublicKeyIsExternallyManaged(id int64) (bool, error) {
 	return false, nil
 }
 
-// DeletePublicKey deletes SSH key information both in database and authorized_keys file.
-func DeletePublicKey(doer *user_model.User, id int64) (err error) {
-	key, err := GetPublicKeyByID(id)
-	if err != nil {
-		return err
-	}
-
-	// Check if user has access to delete this key.
-	if !doer.IsAdmin && doer.ID != key.OwnerID {
-		return ErrKeyAccessDenied{doer.ID, key.ID, "public"}
-	}
-
-	ctx, committer, err := db.TxContext()
-	if err != nil {
-		return err
-	}
-	defer committer.Close()
-
-	if err = deletePublicKeys(db.GetEngine(ctx), id); err != nil {
-		return err
-	}
-
-	if err = committer.Commit(); err != nil {
-		return err
-	}
-	committer.Close()
-
-	if key.Type == KeyTypePrincipal {
-		return RewriteAllPrincipalKeys()
-	}
-
-	return RewriteAllPublicKeys()
-}
-
 // deleteKeysMarkedForDeletion returns true if ssh keys needs update
 func deleteKeysMarkedForDeletion(keys []string) (bool, error) {
 	// Start session
@@ -377,7 +344,7 @@ func deleteKeysMarkedForDeletion(keys []string) (bool, error) {
 			log.Error("SearchPublicKeyByContent: %v", err)
 			continue
 		}
-		if err = deletePublicKeys(sess, key.ID); err != nil {
+		if err = DeletePublicKeys(ctx, key.ID); err != nil {
 			log.Error("deletePublicKeys: %v", err)
 			continue
 		}
diff --git a/models/ssh_key_authorized_keys.go b/models/asymkey/ssh_key_authorized_keys.go
similarity index 97%
rename from models/ssh_key_authorized_keys.go
rename to models/asymkey/ssh_key_authorized_keys.go
index 7843390ffc..dd058f5d11 100644
--- a/models/ssh_key_authorized_keys.go
+++ b/models/asymkey/ssh_key_authorized_keys.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package models
+package asymkey
 
 import (
 	"bufio"
@@ -118,10 +118,6 @@ func appendAuthorizedKeysToFile(keys ...*PublicKey) error {
 // Note: db.GetEngine(db.DefaultContext).Iterate does not get latest data after insert/delete, so we have to call this function
 // outside any session scope independently.
 func RewriteAllPublicKeys() error {
-	return rewriteAllPublicKeys(db.GetEngine(db.DefaultContext))
-}
-
-func rewriteAllPublicKeys(e db.Engine) error {
 	// Don't rewrite key if internal server
 	if setting.SSH.StartBuiltinServer || !setting.SSH.CreateAuthorizedKeysFile {
 		return nil
@@ -169,7 +165,7 @@ func rewriteAllPublicKeys(e db.Engine) error {
 		}
 	}
 
-	if err := regeneratePublicKeys(e, t); err != nil {
+	if err := RegeneratePublicKeys(t); err != nil {
 		return err
 	}
 
diff --git a/models/ssh_key_authorized_principals.go b/models/asymkey/ssh_key_authorized_principals.go
similarity index 99%
rename from models/ssh_key_authorized_principals.go
rename to models/asymkey/ssh_key_authorized_principals.go
index c053b4b6d5..a8c48c50aa 100644
--- a/models/ssh_key_authorized_principals.go
+++ b/models/asymkey/ssh_key_authorized_principals.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package models
+package asymkey
 
 import (
 	"bufio"
diff --git a/models/ssh_key_deploy.go b/models/asymkey/ssh_key_deploy.go
similarity index 75%
rename from models/ssh_key_deploy.go
rename to models/asymkey/ssh_key_deploy.go
index 672974afb3..fc6324792a 100644
--- a/models/ssh_key_deploy.go
+++ b/models/asymkey/ssh_key_deploy.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package models
+package asymkey
 
 import (
 	"context"
@@ -11,8 +11,6 @@ import (
 
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/perm"
-	repo_model "code.gitea.io/gitea/models/repo"
-	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/timeutil"
 
 	"xorm.io/builder"
@@ -169,13 +167,9 @@ func AddDeployKey(repoID int64, name, content string, readOnly bool) (*DeployKey
 }
 
 // GetDeployKeyByID returns deploy key by given ID.
-func GetDeployKeyByID(id int64) (*DeployKey, error) {
-	return getDeployKeyByID(db.GetEngine(db.DefaultContext), id)
-}
-
-func getDeployKeyByID(e db.Engine, id int64) (*DeployKey, error) {
+func GetDeployKeyByID(ctx context.Context, id int64) (*DeployKey, error) {
 	key := new(DeployKey)
-	has, err := e.ID(id).Get(key)
+	has, err := db.GetEngine(ctx).ID(id).Get(key)
 	if err != nil {
 		return nil, err
 	} else if !has {
@@ -215,68 +209,6 @@ func UpdateDeployKey(key *DeployKey) error {
 	return err
 }
 
-// DeleteDeployKey deletes deploy key from its repository authorized_keys file if needed.
-func DeleteDeployKey(doer *user_model.User, id int64) error {
-	ctx, committer, err := db.TxContext()
-	if err != nil {
-		return err
-	}
-	defer committer.Close()
-
-	if err := deleteDeployKey(ctx, doer, id); err != nil {
-		return err
-	}
-	return committer.Commit()
-}
-
-func deleteDeployKey(ctx context.Context, doer *user_model.User, id int64) error {
-	sess := db.GetEngine(ctx)
-	key, err := getDeployKeyByID(sess, id)
-	if err != nil {
-		if IsErrDeployKeyNotExist(err) {
-			return nil
-		}
-		return fmt.Errorf("GetDeployKeyByID: %v", err)
-	}
-
-	// Check if user has access to delete this key.
-	if !doer.IsAdmin {
-		repo, err := repo_model.GetRepositoryByIDCtx(ctx, key.RepoID)
-		if err != nil {
-			return fmt.Errorf("repo_model.GetRepositoryByID: %v", err)
-		}
-		has, err := isUserRepoAdmin(sess, repo, doer)
-		if err != nil {
-			return fmt.Errorf("GetUserRepoPermission: %v", err)
-		} else if !has {
-			return ErrKeyAccessDenied{doer.ID, key.ID, "deploy"}
-		}
-	}
-
-	if _, err = sess.ID(key.ID).Delete(new(DeployKey)); err != nil {
-		return fmt.Errorf("delete deploy key [%d]: %v", key.ID, err)
-	}
-
-	// Check if this is the last reference to same key content.
-	has, err := sess.
-		Where("key_id = ?", key.KeyID).
-		Get(new(DeployKey))
-	if err != nil {
-		return err
-	} else if !has {
-		if err = deletePublicKeys(sess, key.KeyID); err != nil {
-			return err
-		}
-
-		// after deleted the public keys, should rewrite the public keys file
-		if err = rewriteAllPublicKeys(sess); err != nil {
-			return err
-		}
-	}
-
-	return nil
-}
-
 // ListDeployKeysOptions are options for ListDeployKeys
 type ListDeployKeysOptions struct {
 	db.ListOptions
@@ -300,12 +232,8 @@ func (opt ListDeployKeysOptions) toCond() builder.Cond {
 }
 
 // ListDeployKeys returns a list of deploy keys matching the provided arguments.
-func ListDeployKeys(opts *ListDeployKeysOptions) ([]*DeployKey, error) {
-	return listDeployKeys(db.GetEngine(db.DefaultContext), opts)
-}
-
-func listDeployKeys(e db.Engine, opts *ListDeployKeysOptions) ([]*DeployKey, error) {
-	sess := e.Where(opts.toCond())
+func ListDeployKeys(ctx context.Context, opts *ListDeployKeysOptions) ([]*DeployKey, error) {
+	sess := db.GetEngine(ctx).Where(opts.toCond())
 
 	if opts.Page != 0 {
 		sess = db.SetSessionPagination(sess, opts)
diff --git a/models/ssh_key_fingerprint.go b/models/asymkey/ssh_key_fingerprint.go
similarity index 99%
rename from models/ssh_key_fingerprint.go
rename to models/asymkey/ssh_key_fingerprint.go
index 85296c961c..437f283bfa 100644
--- a/models/ssh_key_fingerprint.go
+++ b/models/asymkey/ssh_key_fingerprint.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package models
+package asymkey
 
 import (
 	"errors"
diff --git a/models/ssh_key_parse.go b/models/asymkey/ssh_key_parse.go
similarity index 99%
rename from models/ssh_key_parse.go
rename to models/asymkey/ssh_key_parse.go
index 748c66da7d..734bd4ccab 100644
--- a/models/ssh_key_parse.go
+++ b/models/asymkey/ssh_key_parse.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package models
+package asymkey
 
 import (
 	"crypto/rsa"
@@ -18,6 +18,7 @@ import (
 	"strconv"
 	"strings"
 
+	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/process"
 	"code.gitea.io/gitea/modules/setting"
@@ -157,7 +158,7 @@ func parseKeyString(content string) (string, error) {
 // It returns the actual public key line on success.
 func CheckPublicKeyString(content string) (_ string, err error) {
 	if setting.SSH.Disabled {
-		return "", ErrSSHDisabled{}
+		return "", db.ErrSSHDisabled{}
 	}
 
 	content, err = parseKeyString(content)
diff --git a/models/ssh_key_principals.go b/models/asymkey/ssh_key_principals.go
similarity index 98%
rename from models/ssh_key_principals.go
rename to models/asymkey/ssh_key_principals.go
index 9a17a56f1a..19fc6644cb 100644
--- a/models/ssh_key_principals.go
+++ b/models/asymkey/ssh_key_principals.go
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package models
+package asymkey
 
 import (
 	"errors"
@@ -76,7 +76,7 @@ func addPrincipalKey(e db.Engine, key *PublicKey) (err error) {
 // CheckPrincipalKeyString strips spaces and returns an error if the given principal contains newlines
 func CheckPrincipalKeyString(user *user_model.User, content string) (_ string, err error) {
 	if setting.SSH.Disabled {
-		return "", ErrSSHDisabled{}
+		return "", db.ErrSSHDisabled{}
 	}
 
 	content = strings.TrimSpace(content)
diff --git a/models/ssh_key_test.go b/models/asymkey/ssh_key_test.go
similarity index 99%
rename from models/ssh_key_test.go
rename to models/asymkey/ssh_key_test.go
index b52a36bdbd..62c07c6035 100644
--- a/models/ssh_key_test.go
+++ b/models/asymkey/ssh_key_test.go
@@ -3,7 +3,7 @@
 // Use of this source code is governed by a MIT-style
 // license that can be found in the LICENSE file.
 
-package models
+package asymkey
 
 import (
 	"strings"
@@ -14,11 +14,6 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-func init() {
-	setting.SetCustomPathAndConf("", "", "")
-	setting.LoadForTest()
-}
-
 func Test_SSHParsePublicKey(t *testing.T) {
 	testCases := []struct {
 		name          string
diff --git a/models/commit.go b/models/commit.go
index 8de71da1b3..5df6964a1d 100644
--- a/models/commit.go
+++ b/models/commit.go
@@ -5,6 +5,7 @@
 package models
 
 import (
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/git"
@@ -13,9 +14,12 @@ import (
 // ConvertFromGitCommit converts git commits into SignCommitWithStatuses
 func ConvertFromGitCommit(commits []*git.Commit, repo *repo_model.Repository) []*SignCommitWithStatuses {
 	return ParseCommitsWithStatus(
-		ParseCommitsWithSignature(
+		asymkey_model.ParseCommitsWithSignature(
 			user_model.ValidateCommitsWithEmails(commits),
-			repo,
+			repo.GetTrustModel(),
+			func(user *user_model.User) (bool, error) {
+				return IsUserRepoAdmin(repo, user)
+			},
 		),
 		repo,
 	)
diff --git a/models/commit_status.go b/models/commit_status.go
index e0942d2028..93b6f93f96 100644
--- a/models/commit_status.go
+++ b/models/commit_status.go
@@ -12,6 +12,7 @@ import (
 	"strings"
 	"time"
 
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
@@ -328,11 +329,11 @@ func NewCommitStatus(opts NewCommitStatusOptions) error {
 type SignCommitWithStatuses struct {
 	Status   *CommitStatus
 	Statuses []*CommitStatus
-	*SignCommit
+	*asymkey_model.SignCommit
 }
 
 // ParseCommitsWithStatus checks commits latest statuses and calculates its worst status state
-func ParseCommitsWithStatus(oldCommits []*SignCommit, repo *repo_model.Repository) []*SignCommitWithStatuses {
+func ParseCommitsWithStatus(oldCommits []*asymkey_model.SignCommit, repo *repo_model.Repository) []*SignCommitWithStatuses {
 	newCommits := make([]*SignCommitWithStatuses, 0, len(oldCommits))
 
 	for _, c := range oldCommits {
diff --git a/models/db/error.go b/models/db/error.go
index adaeedcc09..f20cc9b4cb 100644
--- a/models/db/error.go
+++ b/models/db/error.go
@@ -29,3 +29,16 @@ func ErrCancelledf(format string, args ...interface{}) error {
 		fmt.Sprintf(format, args...),
 	}
 }
+
+// ErrSSHDisabled represents an "SSH disabled" error.
+type ErrSSHDisabled struct{}
+
+// IsErrSSHDisabled checks if an error is a ErrSSHDisabled.
+func IsErrSSHDisabled(err error) bool {
+	_, ok := err.(ErrSSHDisabled)
+	return ok
+}
+
+func (err ErrSSHDisabled) Error() string {
+	return "SSH is disabled"
+}
diff --git a/models/error.go b/models/error.go
index 20ed7f90e1..54556fd787 100644
--- a/models/error.go
+++ b/models/error.go
@@ -28,19 +28,6 @@ func (err ErrNotExist) Error() string {
 	return fmt.Sprintf("record does not exist [id: %d]", err.ID)
 }
 
-// ErrSSHDisabled represents an "SSH disabled" error.
-type ErrSSHDisabled struct{}
-
-// IsErrSSHDisabled checks if an error is a ErrSSHDisabled.
-func IsErrSSHDisabled(err error) bool {
-	_, ok := err.(ErrSSHDisabled)
-	return ok
-}
-
-func (err ErrSSHDisabled) Error() string {
-	return "SSH is disabled"
-}
-
 // ErrUserOwnRepos represents a "UserOwnRepos" kind of error.
 type ErrUserOwnRepos struct {
 	UID int64
@@ -151,254 +138,6 @@ func (err ErrWikiInvalidFileName) Error() string {
 	return fmt.Sprintf("Invalid wiki filename: %s", err.FileName)
 }
 
-// __________     ___.   .__  .__          ____  __.
-// \______   \__ _\_ |__ |  | |__| ____   |    |/ _|____ ___.__.
-//  |     ___/  |  \ __ \|  | |  |/ ___\  |      <_/ __ <   |  |
-//  |    |   |  |  / \_\ \  |_|  \  \___  |    |  \  ___/\___  |
-//  |____|   |____/|___  /____/__|\___  > |____|__ \___  > ____|
-//                     \/             \/          \/   \/\/
-
-// ErrKeyUnableVerify represents a "KeyUnableVerify" kind of error.
-type ErrKeyUnableVerify struct {
-	Result string
-}
-
-// IsErrKeyUnableVerify checks if an error is a ErrKeyUnableVerify.
-func IsErrKeyUnableVerify(err error) bool {
-	_, ok := err.(ErrKeyUnableVerify)
-	return ok
-}
-
-func (err ErrKeyUnableVerify) Error() string {
-	return fmt.Sprintf("Unable to verify key content [result: %s]", err.Result)
-}
-
-// ErrKeyNotExist represents a "KeyNotExist" kind of error.
-type ErrKeyNotExist struct {
-	ID int64
-}
-
-// IsErrKeyNotExist checks if an error is a ErrKeyNotExist.
-func IsErrKeyNotExist(err error) bool {
-	_, ok := err.(ErrKeyNotExist)
-	return ok
-}
-
-func (err ErrKeyNotExist) Error() string {
-	return fmt.Sprintf("public key does not exist [id: %d]", err.ID)
-}
-
-// ErrKeyAlreadyExist represents a "KeyAlreadyExist" kind of error.
-type ErrKeyAlreadyExist struct {
-	OwnerID     int64
-	Fingerprint string
-	Content     string
-}
-
-// IsErrKeyAlreadyExist checks if an error is a ErrKeyAlreadyExist.
-func IsErrKeyAlreadyExist(err error) bool {
-	_, ok := err.(ErrKeyAlreadyExist)
-	return ok
-}
-
-func (err ErrKeyAlreadyExist) Error() string {
-	return fmt.Sprintf("public key already exists [owner_id: %d, finger_print: %s, content: %s]",
-		err.OwnerID, err.Fingerprint, err.Content)
-}
-
-// ErrKeyNameAlreadyUsed represents a "KeyNameAlreadyUsed" kind of error.
-type ErrKeyNameAlreadyUsed struct {
-	OwnerID int64
-	Name    string
-}
-
-// IsErrKeyNameAlreadyUsed checks if an error is a ErrKeyNameAlreadyUsed.
-func IsErrKeyNameAlreadyUsed(err error) bool {
-	_, ok := err.(ErrKeyNameAlreadyUsed)
-	return ok
-}
-
-func (err ErrKeyNameAlreadyUsed) Error() string {
-	return fmt.Sprintf("public key already exists [owner_id: %d, name: %s]", err.OwnerID, err.Name)
-}
-
-// ErrGPGNoEmailFound represents a "ErrGPGNoEmailFound" kind of error.
-type ErrGPGNoEmailFound struct {
-	FailedEmails []string
-	ID           string
-}
-
-// IsErrGPGNoEmailFound checks if an error is a ErrGPGNoEmailFound.
-func IsErrGPGNoEmailFound(err error) bool {
-	_, ok := err.(ErrGPGNoEmailFound)
-	return ok
-}
-
-func (err ErrGPGNoEmailFound) Error() string {
-	return fmt.Sprintf("none of the emails attached to the GPG key could be found: %v", err.FailedEmails)
-}
-
-// ErrGPGInvalidTokenSignature represents a "ErrGPGInvalidTokenSignature" kind of error.
-type ErrGPGInvalidTokenSignature struct {
-	Wrapped error
-	ID      string
-}
-
-// IsErrGPGInvalidTokenSignature checks if an error is a ErrGPGInvalidTokenSignature.
-func IsErrGPGInvalidTokenSignature(err error) bool {
-	_, ok := err.(ErrGPGInvalidTokenSignature)
-	return ok
-}
-
-func (err ErrGPGInvalidTokenSignature) Error() string {
-	return "the provided signature does not sign the token with the provided key"
-}
-
-// ErrGPGKeyParsing represents a "ErrGPGKeyParsing" kind of error.
-type ErrGPGKeyParsing struct {
-	ParseError error
-}
-
-// IsErrGPGKeyParsing checks if an error is a ErrGPGKeyParsing.
-func IsErrGPGKeyParsing(err error) bool {
-	_, ok := err.(ErrGPGKeyParsing)
-	return ok
-}
-
-func (err ErrGPGKeyParsing) Error() string {
-	return fmt.Sprintf("failed to parse gpg key %s", err.ParseError.Error())
-}
-
-// ErrGPGKeyNotExist represents a "GPGKeyNotExist" kind of error.
-type ErrGPGKeyNotExist struct {
-	ID int64
-}
-
-// IsErrGPGKeyNotExist checks if an error is a ErrGPGKeyNotExist.
-func IsErrGPGKeyNotExist(err error) bool {
-	_, ok := err.(ErrGPGKeyNotExist)
-	return ok
-}
-
-func (err ErrGPGKeyNotExist) Error() string {
-	return fmt.Sprintf("public gpg key does not exist [id: %d]", err.ID)
-}
-
-// ErrGPGKeyImportNotExist represents a "GPGKeyImportNotExist" kind of error.
-type ErrGPGKeyImportNotExist struct {
-	ID string
-}
-
-// IsErrGPGKeyImportNotExist checks if an error is a ErrGPGKeyImportNotExist.
-func IsErrGPGKeyImportNotExist(err error) bool {
-	_, ok := err.(ErrGPGKeyImportNotExist)
-	return ok
-}
-
-func (err ErrGPGKeyImportNotExist) Error() string {
-	return fmt.Sprintf("public gpg key import does not exist [id: %s]", err.ID)
-}
-
-// ErrGPGKeyIDAlreadyUsed represents a "GPGKeyIDAlreadyUsed" kind of error.
-type ErrGPGKeyIDAlreadyUsed struct {
-	KeyID string
-}
-
-// IsErrGPGKeyIDAlreadyUsed checks if an error is a ErrKeyNameAlreadyUsed.
-func IsErrGPGKeyIDAlreadyUsed(err error) bool {
-	_, ok := err.(ErrGPGKeyIDAlreadyUsed)
-	return ok
-}
-
-func (err ErrGPGKeyIDAlreadyUsed) Error() string {
-	return fmt.Sprintf("public key already exists [key_id: %s]", err.KeyID)
-}
-
-// ErrGPGKeyAccessDenied represents a "GPGKeyAccessDenied" kind of Error.
-type ErrGPGKeyAccessDenied struct {
-	UserID int64
-	KeyID  int64
-}
-
-// IsErrGPGKeyAccessDenied checks if an error is a ErrGPGKeyAccessDenied.
-func IsErrGPGKeyAccessDenied(err error) bool {
-	_, ok := err.(ErrGPGKeyAccessDenied)
-	return ok
-}
-
-// Error pretty-prints an error of type ErrGPGKeyAccessDenied.
-func (err ErrGPGKeyAccessDenied) Error() string {
-	return fmt.Sprintf("user does not have access to the key [user_id: %d, key_id: %d]",
-		err.UserID, err.KeyID)
-}
-
-// ErrKeyAccessDenied represents a "KeyAccessDenied" kind of error.
-type ErrKeyAccessDenied struct {
-	UserID int64
-	KeyID  int64
-	Note   string
-}
-
-// IsErrKeyAccessDenied checks if an error is a ErrKeyAccessDenied.
-func IsErrKeyAccessDenied(err error) bool {
-	_, ok := err.(ErrKeyAccessDenied)
-	return ok
-}
-
-func (err ErrKeyAccessDenied) Error() string {
-	return fmt.Sprintf("user does not have access to the key [user_id: %d, key_id: %d, note: %s]",
-		err.UserID, err.KeyID, err.Note)
-}
-
-// ErrDeployKeyNotExist represents a "DeployKeyNotExist" kind of error.
-type ErrDeployKeyNotExist struct {
-	ID     int64
-	KeyID  int64
-	RepoID int64
-}
-
-// IsErrDeployKeyNotExist checks if an error is a ErrDeployKeyNotExist.
-func IsErrDeployKeyNotExist(err error) bool {
-	_, ok := err.(ErrDeployKeyNotExist)
-	return ok
-}
-
-func (err ErrDeployKeyNotExist) Error() string {
-	return fmt.Sprintf("Deploy key does not exist [id: %d, key_id: %d, repo_id: %d]", err.ID, err.KeyID, err.RepoID)
-}
-
-// ErrDeployKeyAlreadyExist represents a "DeployKeyAlreadyExist" kind of error.
-type ErrDeployKeyAlreadyExist struct {
-	KeyID  int64
-	RepoID int64
-}
-
-// IsErrDeployKeyAlreadyExist checks if an error is a ErrDeployKeyAlreadyExist.
-func IsErrDeployKeyAlreadyExist(err error) bool {
-	_, ok := err.(ErrDeployKeyAlreadyExist)
-	return ok
-}
-
-func (err ErrDeployKeyAlreadyExist) Error() string {
-	return fmt.Sprintf("public key already exists [key_id: %d, repo_id: %d]", err.KeyID, err.RepoID)
-}
-
-// ErrDeployKeyNameAlreadyUsed represents a "DeployKeyNameAlreadyUsed" kind of error.
-type ErrDeployKeyNameAlreadyUsed struct {
-	RepoID int64
-	Name   string
-}
-
-// IsErrDeployKeyNameAlreadyUsed checks if an error is a ErrDeployKeyNameAlreadyUsed.
-func IsErrDeployKeyNameAlreadyUsed(err error) bool {
-	_, ok := err.(ErrDeployKeyNameAlreadyUsed)
-	return ok
-}
-
-func (err ErrDeployKeyNameAlreadyUsed) Error() string {
-	return fmt.Sprintf("public key with name already exists [repo_id: %d, name: %s]", err.RepoID, err.Name)
-}
-
 //    _____                                   ___________     __
 //   /  _  \   ____  ____  ____   ______ _____\__    ___/___ |  | __ ____   ____
 //  /  /_\  \_/ ___\/ ___\/ __ \ /  ___//  ___/ |    | /  _ \|  |/ // __ \ /    \
@@ -878,22 +617,6 @@ func (err ErrUserDoesNotHaveAccessToRepo) Error() string {
 	return fmt.Sprintf("user doesn't have access to repo [user_id: %d, repo_name: %s]", err.UserID, err.RepoName)
 }
 
-// ErrWontSign explains the first reason why a commit would not be signed
-// There may be other reasons - this is just the first reason found
-type ErrWontSign struct {
-	Reason signingMode
-}
-
-func (e *ErrWontSign) Error() string {
-	return fmt.Sprintf("wont sign: %s", e.Reason)
-}
-
-// IsErrWontSign checks if an error is a ErrWontSign
-func IsErrWontSign(err error) bool {
-	_, ok := err.(*ErrWontSign)
-	return ok
-}
-
 // __________                             .__
 // \______   \____________    ____   ____ |  |__
 //  |    |  _/\_  __ \__  \  /    \_/ ___\|  |  \
diff --git a/models/main_test.go b/models/main_test.go
index 20107eab1e..8d5291a8aa 100644
--- a/models/main_test.go
+++ b/models/main_test.go
@@ -10,10 +10,16 @@ import (
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
+	"code.gitea.io/gitea/modules/setting"
 
 	"github.com/stretchr/testify/assert"
 )
 
+func init() {
+	setting.SetCustomPathAndConf("", "", "")
+	setting.LoadForTest()
+}
+
 // TestFixturesAreConsistent assert that test fixtures are consistent
 func TestFixturesAreConsistent(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
diff --git a/models/pull_sign.go b/models/pull_sign.go
deleted file mode 100644
index 269e8e06d2..0000000000
--- a/models/pull_sign.go
+++ /dev/null
@@ -1,133 +0,0 @@
-// 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 (
-	"code.gitea.io/gitea/models/db"
-	"code.gitea.io/gitea/models/login"
-	user_model "code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/git"
-	"code.gitea.io/gitea/modules/log"
-	"code.gitea.io/gitea/modules/setting"
-)
-
-// SignMerge determines if we should sign a PR merge commit to the base repository
-func (pr *PullRequest) SignMerge(u *user_model.User, tmpBasePath, baseCommit, headCommit string) (bool, string, *git.Signature, error) {
-	if err := pr.LoadBaseRepo(); err != nil {
-		log.Error("Unable to get Base Repo for pull request")
-		return false, "", nil, err
-	}
-	repo := pr.BaseRepo
-
-	signingKey, signer := SigningKey(repo.RepoPath())
-	if signingKey == "" {
-		return false, "", nil, &ErrWontSign{noKey}
-	}
-	rules := signingModeFromStrings(setting.Repository.Signing.Merges)
-
-	var gitRepo *git.Repository
-	var err error
-
-Loop:
-	for _, rule := range rules {
-		switch rule {
-		case never:
-			return false, "", nil, &ErrWontSign{never}
-		case always:
-			break Loop
-		case pubkey:
-			keys, err := ListGPGKeys(u.ID, db.ListOptions{})
-			if err != nil {
-				return false, "", nil, err
-			}
-			if len(keys) == 0 {
-				return false, "", nil, &ErrWontSign{pubkey}
-			}
-		case twofa:
-			twofaModel, err := login.GetTwoFactorByUID(u.ID)
-			if err != nil && !login.IsErrTwoFactorNotEnrolled(err) {
-				return false, "", nil, err
-			}
-			if twofaModel == nil {
-				return false, "", nil, &ErrWontSign{twofa}
-			}
-		case approved:
-			protectedBranch, err := GetProtectedBranchBy(repo.ID, pr.BaseBranch)
-			if err != nil {
-				return false, "", nil, err
-			}
-			if protectedBranch == nil {
-				return false, "", nil, &ErrWontSign{approved}
-			}
-			if protectedBranch.GetGrantedApprovalsCount(pr) < 1 {
-				return false, "", nil, &ErrWontSign{approved}
-			}
-		case baseSigned:
-			if gitRepo == nil {
-				gitRepo, err = git.OpenRepository(tmpBasePath)
-				if err != nil {
-					return false, "", nil, err
-				}
-				defer gitRepo.Close()
-			}
-			commit, err := gitRepo.GetCommit(baseCommit)
-			if err != nil {
-				return false, "", nil, err
-			}
-			verification := ParseCommitWithSignature(commit)
-			if !verification.Verified {
-				return false, "", nil, &ErrWontSign{baseSigned}
-			}
-		case headSigned:
-			if gitRepo == nil {
-				gitRepo, err = git.OpenRepository(tmpBasePath)
-				if err != nil {
-					return false, "", nil, err
-				}
-				defer gitRepo.Close()
-			}
-			commit, err := gitRepo.GetCommit(headCommit)
-			if err != nil {
-				return false, "", nil, err
-			}
-			verification := ParseCommitWithSignature(commit)
-			if !verification.Verified {
-				return false, "", nil, &ErrWontSign{headSigned}
-			}
-		case commitsSigned:
-			if gitRepo == nil {
-				gitRepo, err = git.OpenRepository(tmpBasePath)
-				if err != nil {
-					return false, "", nil, err
-				}
-				defer gitRepo.Close()
-			}
-			commit, err := gitRepo.GetCommit(headCommit)
-			if err != nil {
-				return false, "", nil, err
-			}
-			verification := ParseCommitWithSignature(commit)
-			if !verification.Verified {
-				return false, "", nil, &ErrWontSign{commitsSigned}
-			}
-			// need to work out merge-base
-			mergeBaseCommit, _, err := gitRepo.GetMergeBase("", baseCommit, headCommit)
-			if err != nil {
-				return false, "", nil, err
-			}
-			commitList, err := commit.CommitsBeforeUntil(mergeBaseCommit)
-			if err != nil {
-				return false, "", nil, err
-			}
-			for _, commit := range commitList {
-				verification := ParseCommitWithSignature(commit)
-				if !verification.Verified {
-					return false, "", nil, &ErrWontSign{commitsSigned}
-				}
-			}
-		}
-	}
-	return true, signingKey, signer, nil
-}
diff --git a/models/repo.go b/models/repo.go
index 6bdc4c20d2..adc62c9528 100644
--- a/models/repo.go
+++ b/models/repo.go
@@ -20,6 +20,7 @@ import (
 	_ "image/jpeg" // Needed for jpeg support
 
 	admin_model "code.gitea.io/gitea/models/admin"
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/perm"
 	repo_model "code.gitea.io/gitea/models/repo"
@@ -856,12 +857,13 @@ func DeleteRepository(doer *user_model.User, uid, repoID int64) error {
 	}
 
 	// Delete Deploy Keys
-	deployKeys, err := listDeployKeys(sess, &ListDeployKeysOptions{RepoID: repoID})
+	deployKeys, err := asymkey_model.ListDeployKeys(ctx, &asymkey_model.ListDeployKeysOptions{RepoID: repoID})
 	if err != nil {
 		return fmt.Errorf("listDeployKeys: %v", err)
 	}
+	var needRewriteKeysFile = len(deployKeys) > 0
 	for _, dKey := range deployKeys {
-		if err := deleteDeployKey(ctx, doer, dKey.ID); err != nil {
+		if err := DeleteDeployKey(ctx, doer, dKey.ID); err != nil {
 			return fmt.Errorf("deleteDeployKeys: %v", err)
 		}
 	}
@@ -1049,6 +1051,12 @@ func DeleteRepository(doer *user_model.User, uid, repoID int64) error {
 
 	committer.Close()
 
+	if needRewriteKeysFile {
+		if err := asymkey_model.RewriteAllPublicKeys(); err != nil {
+			log.Error("RewriteAllPublicKeys failed: %v", err)
+		}
+	}
+
 	// We should always delete the files after the database transaction succeed. If
 	// we delete the file but the database rollback, the repository will be broken.
 
@@ -1407,3 +1415,52 @@ func LinkedRepository(a *repo_model.Attachment) (*repo_model.Repository, unit.Ty
 	}
 	return nil, -1, nil
 }
+
+// DeleteDeployKey delete deploy keys
+func DeleteDeployKey(ctx context.Context, doer *user_model.User, id int64) error {
+	key, err := asymkey_model.GetDeployKeyByID(ctx, id)
+	if err != nil {
+		if asymkey_model.IsErrDeployKeyNotExist(err) {
+			return nil
+		}
+		return fmt.Errorf("GetDeployKeyByID: %v", err)
+	}
+
+	sess := db.GetEngine(ctx)
+
+	// Check if user has access to delete this key.
+	if !doer.IsAdmin {
+		repo, err := repo_model.GetRepositoryByIDCtx(ctx, key.RepoID)
+		if err != nil {
+			return fmt.Errorf("GetRepositoryByID: %v", err)
+		}
+		has, err := isUserRepoAdmin(sess, repo, doer)
+		if err != nil {
+			return fmt.Errorf("GetUserRepoPermission: %v", err)
+		} else if !has {
+			return asymkey_model.ErrKeyAccessDenied{
+				UserID: doer.ID,
+				KeyID:  key.ID,
+				Note:   "deploy",
+			}
+		}
+	}
+
+	if _, err = sess.ID(key.ID).Delete(new(asymkey_model.DeployKey)); err != nil {
+		return fmt.Errorf("delete deploy key [%d]: %v", key.ID, err)
+	}
+
+	// Check if this is the last reference to same key content.
+	has, err := sess.
+		Where("key_id = ?", key.KeyID).
+		Get(new(asymkey_model.DeployKey))
+	if err != nil {
+		return err
+	} else if !has {
+		if err = asymkey_model.DeletePublicKeys(ctx, key.KeyID); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
diff --git a/models/statistic.go b/models/statistic.go
index 055f312c11..175815081f 100644
--- a/models/statistic.go
+++ b/models/statistic.go
@@ -5,6 +5,7 @@
 package models
 
 import (
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/login"
 	repo_model "code.gitea.io/gitea/models/repo"
@@ -47,7 +48,7 @@ func GetStatistic() (stats Statistic) {
 	e := db.GetEngine(db.DefaultContext)
 	stats.Counter.User = user_model.CountUsers()
 	stats.Counter.Org = CountOrganizations()
-	stats.Counter.PublicKey, _ = e.Count(new(PublicKey))
+	stats.Counter.PublicKey, _ = e.Count(new(asymkey_model.PublicKey))
 	stats.Counter.Repo = repo_model.CountRepositories(true)
 	stats.Counter.Watch, _ = e.Count(new(Watch))
 	stats.Counter.Star, _ = e.Count(new(Star))
diff --git a/models/user.go b/models/user.go
index 1427833e21..2a727dd124 100644
--- a/models/user.go
+++ b/models/user.go
@@ -12,6 +12,7 @@ import (
 
 	_ "image/jpeg" // Needed for jpeg support
 
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
@@ -234,23 +235,23 @@ func DeleteUser(ctx context.Context, u *user_model.User) (err error) {
 	}
 
 	// ***** START: PublicKey *****
-	if _, err = e.Delete(&PublicKey{OwnerID: u.ID}); err != nil {
+	if _, err = e.Delete(&asymkey_model.PublicKey{OwnerID: u.ID}); err != nil {
 		return fmt.Errorf("deletePublicKeys: %v", err)
 	}
 	// ***** END: PublicKey *****
 
 	// ***** START: GPGPublicKey *****
-	keys, err := listGPGKeys(e, u.ID, db.ListOptions{})
+	keys, err := asymkey_model.ListGPGKeys(ctx, u.ID, db.ListOptions{})
 	if err != nil {
 		return fmt.Errorf("ListGPGKeys: %v", err)
 	}
 	// Delete GPGKeyImport(s).
 	for _, key := range keys {
-		if _, err = e.Delete(&GPGKeyImport{KeyID: key.KeyID}); err != nil {
+		if _, err = e.Delete(&asymkey_model.GPGKeyImport{KeyID: key.KeyID}); err != nil {
 			return fmt.Errorf("deleteGPGKeyImports: %v", err)
 		}
 	}
-	if _, err = e.Delete(&GPGKey{OwnerID: u.ID}); err != nil {
+	if _, err = e.Delete(&asymkey_model.GPGKey{OwnerID: u.ID}); err != nil {
 		return fmt.Errorf("deleteGPGKeys: %v", err)
 	}
 	// ***** END: GPGPublicKey *****
diff --git a/models/user_test.go b/models/user_test.go
index d3c7718d69..4749a3af73 100644
--- a/models/user_test.go
+++ b/models/user_test.go
@@ -8,7 +8,6 @@ import (
 	"fmt"
 	"testing"
 
-	"code.gitea.io/gitea/models/login"
 	"code.gitea.io/gitea/models/unittest"
 	user_model "code.gitea.io/gitea/models/user"
 
@@ -121,71 +120,3 @@ func TestGetOrgRepositoryIDs(t *testing.T) {
 	// User 5's team has no access to any repo
 	assert.Len(t, accessibleRepos, 0)
 }
-
-func TestAddLdapSSHPublicKeys(t *testing.T) {
-	assert.NoError(t, unittest.PrepareTestDatabase())
-
-	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
-	s := &login.Source{ID: 1}
-
-	testCases := []struct {
-		keyString   string
-		number      int
-		keyContents []string
-	}{
-		{
-			keyString: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM= nocomment\n",
-			number:    1,
-			keyContents: []string{
-				"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM=",
-			},
-		},
-		{
-			keyString: `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM= nocomment
-ssh-dss AAAAB3NzaC1kc3MAAACBAOChCC7lf6Uo9n7BmZ6M8St19PZf4Tn59NriyboW2x/DZuYAz3ibZ2OkQ3S0SqDIa0HXSEJ1zaExQdmbO+Ux/wsytWZmCczWOVsaszBZSl90q8UnWlSH6P+/YA+RWJm5SFtuV9PtGIhyZgoNuz5kBQ7K139wuQsecdKktISwTakzAAAAFQCzKsO2JhNKlL+wwwLGOcLffoAmkwAAAIBpK7/3xvduajLBD/9vASqBQIHrgK2J+wiQnIb/Wzy0UsVmvfn8A+udRbBo+csM8xrSnlnlJnjkJS3qiM5g+eTwsLIV1IdKPEwmwB+VcP53Cw6lSyWyJcvhFb0N6s08NZysLzvj0N+ZC/FnhKTLzIyMtkHf/IrPCwlM+pV/M/96YgAAAIEAqQcGn9CKgzgPaguIZooTAOQdvBLMI5y0bQjOW6734XOpqQGf/Kra90wpoasLKZjSYKNPjE+FRUOrStLrxcNs4BeVKhy2PYTRnybfYVk1/dmKgH6P1YSRONsGKvTsH6c5IyCRG0ncCgYeF8tXppyd642982daopE7zQ/NPAnJfag= nocomment`,
-			number: 2,
-			keyContents: []string{
-				"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM=",
-				"ssh-dss AAAAB3NzaC1kc3MAAACBAOChCC7lf6Uo9n7BmZ6M8St19PZf4Tn59NriyboW2x/DZuYAz3ibZ2OkQ3S0SqDIa0HXSEJ1zaExQdmbO+Ux/wsytWZmCczWOVsaszBZSl90q8UnWlSH6P+/YA+RWJm5SFtuV9PtGIhyZgoNuz5kBQ7K139wuQsecdKktISwTakzAAAAFQCzKsO2JhNKlL+wwwLGOcLffoAmkwAAAIBpK7/3xvduajLBD/9vASqBQIHrgK2J+wiQnIb/Wzy0UsVmvfn8A+udRbBo+csM8xrSnlnlJnjkJS3qiM5g+eTwsLIV1IdKPEwmwB+VcP53Cw6lSyWyJcvhFb0N6s08NZysLzvj0N+ZC/FnhKTLzIyMtkHf/IrPCwlM+pV/M/96YgAAAIEAqQcGn9CKgzgPaguIZooTAOQdvBLMI5y0bQjOW6734XOpqQGf/Kra90wpoasLKZjSYKNPjE+FRUOrStLrxcNs4BeVKhy2PYTRnybfYVk1/dmKgH6P1YSRONsGKvTsH6c5IyCRG0ncCgYeF8tXppyd642982daopE7zQ/NPAnJfag=",
-			},
-		},
-		{
-			keyString: `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM= nocomment
-# comment asmdna,ndp
-ssh-dss AAAAB3NzaC1kc3MAAACBAOChCC7lf6Uo9n7BmZ6M8St19PZf4Tn59NriyboW2x/DZuYAz3ibZ2OkQ3S0SqDIa0HXSEJ1zaExQdmbO+Ux/wsytWZmCczWOVsaszBZSl90q8UnWlSH6P+/YA+RWJm5SFtuV9PtGIhyZgoNuz5kBQ7K139wuQsecdKktISwTakzAAAAFQCzKsO2JhNKlL+wwwLGOcLffoAmkwAAAIBpK7/3xvduajLBD/9vASqBQIHrgK2J+wiQnIb/Wzy0UsVmvfn8A+udRbBo+csM8xrSnlnlJnjkJS3qiM5g+eTwsLIV1IdKPEwmwB+VcP53Cw6lSyWyJcvhFb0N6s08NZysLzvj0N+ZC/FnhKTLzIyMtkHf/IrPCwlM+pV/M/96YgAAAIEAqQcGn9CKgzgPaguIZooTAOQdvBLMI5y0bQjOW6734XOpqQGf/Kra90wpoasLKZjSYKNPjE+FRUOrStLrxcNs4BeVKhy2PYTRnybfYVk1/dmKgH6P1YSRONsGKvTsH6c5IyCRG0ncCgYeF8tXppyd642982daopE7zQ/NPAnJfag= nocomment`,
-			number: 2,
-			keyContents: []string{
-				"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM=",
-				"ssh-dss AAAAB3NzaC1kc3MAAACBAOChCC7lf6Uo9n7BmZ6M8St19PZf4Tn59NriyboW2x/DZuYAz3ibZ2OkQ3S0SqDIa0HXSEJ1zaExQdmbO+Ux/wsytWZmCczWOVsaszBZSl90q8UnWlSH6P+/YA+RWJm5SFtuV9PtGIhyZgoNuz5kBQ7K139wuQsecdKktISwTakzAAAAFQCzKsO2JhNKlL+wwwLGOcLffoAmkwAAAIBpK7/3xvduajLBD/9vASqBQIHrgK2J+wiQnIb/Wzy0UsVmvfn8A+udRbBo+csM8xrSnlnlJnjkJS3qiM5g+eTwsLIV1IdKPEwmwB+VcP53Cw6lSyWyJcvhFb0N6s08NZysLzvj0N+ZC/FnhKTLzIyMtkHf/IrPCwlM+pV/M/96YgAAAIEAqQcGn9CKgzgPaguIZooTAOQdvBLMI5y0bQjOW6734XOpqQGf/Kra90wpoasLKZjSYKNPjE+FRUOrStLrxcNs4BeVKhy2PYTRnybfYVk1/dmKgH6P1YSRONsGKvTsH6c5IyCRG0ncCgYeF8tXppyd642982daopE7zQ/NPAnJfag=",
-			},
-		},
-		{
-			keyString: `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM= nocomment
-382488320jasdj1lasmva/vasodifipi4193-fksma.cm
-ssh-dss AAAAB3NzaC1kc3MAAACBAOChCC7lf6Uo9n7BmZ6M8St19PZf4Tn59NriyboW2x/DZuYAz3ibZ2OkQ3S0SqDIa0HXSEJ1zaExQdmbO+Ux/wsytWZmCczWOVsaszBZSl90q8UnWlSH6P+/YA+RWJm5SFtuV9PtGIhyZgoNuz5kBQ7K139wuQsecdKktISwTakzAAAAFQCzKsO2JhNKlL+wwwLGOcLffoAmkwAAAIBpK7/3xvduajLBD/9vASqBQIHrgK2J+wiQnIb/Wzy0UsVmvfn8A+udRbBo+csM8xrSnlnlJnjkJS3qiM5g+eTwsLIV1IdKPEwmwB+VcP53Cw6lSyWyJcvhFb0N6s08NZysLzvj0N+ZC/FnhKTLzIyMtkHf/IrPCwlM+pV/M/96YgAAAIEAqQcGn9CKgzgPaguIZooTAOQdvBLMI5y0bQjOW6734XOpqQGf/Kra90wpoasLKZjSYKNPjE+FRUOrStLrxcNs4BeVKhy2PYTRnybfYVk1/dmKgH6P1YSRONsGKvTsH6c5IyCRG0ncCgYeF8tXppyd642982daopE7zQ/NPAnJfag= nocomment`,
-			number: 2,
-			keyContents: []string{
-				"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM=",
-				"ssh-dss AAAAB3NzaC1kc3MAAACBAOChCC7lf6Uo9n7BmZ6M8St19PZf4Tn59NriyboW2x/DZuYAz3ibZ2OkQ3S0SqDIa0HXSEJ1zaExQdmbO+Ux/wsytWZmCczWOVsaszBZSl90q8UnWlSH6P+/YA+RWJm5SFtuV9PtGIhyZgoNuz5kBQ7K139wuQsecdKktISwTakzAAAAFQCzKsO2JhNKlL+wwwLGOcLffoAmkwAAAIBpK7/3xvduajLBD/9vASqBQIHrgK2J+wiQnIb/Wzy0UsVmvfn8A+udRbBo+csM8xrSnlnlJnjkJS3qiM5g+eTwsLIV1IdKPEwmwB+VcP53Cw6lSyWyJcvhFb0N6s08NZysLzvj0N+ZC/FnhKTLzIyMtkHf/IrPCwlM+pV/M/96YgAAAIEAqQcGn9CKgzgPaguIZooTAOQdvBLMI5y0bQjOW6734XOpqQGf/Kra90wpoasLKZjSYKNPjE+FRUOrStLrxcNs4BeVKhy2PYTRnybfYVk1/dmKgH6P1YSRONsGKvTsH6c5IyCRG0ncCgYeF8tXppyd642982daopE7zQ/NPAnJfag=",
-			},
-		},
-	}
-
-	for i, kase := range testCases {
-		s.ID = int64(i) + 20
-		AddPublicKeysBySource(user, s, []string{kase.keyString})
-		keys, err := ListPublicKeysBySource(user.ID, s.ID)
-		assert.NoError(t, err)
-		if err != nil {
-			continue
-		}
-		assert.Len(t, keys, kase.number)
-
-		for _, key := range keys {
-			assert.Contains(t, kase.keyContents, key.Content)
-		}
-		for _, key := range keys {
-			DeletePublicKey(user, key.ID)
-		}
-	}
-}
diff --git a/modules/context/repo.go b/modules/context/repo.go
index 694c483bd4..1725cb724d 100644
--- a/modules/context/repo.go
+++ b/modules/context/repo.go
@@ -25,6 +25,7 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
+	asymkey_service "code.gitea.io/gitea/services/asymkey"
 
 	"github.com/editorconfig/editorconfig-core-go/v2"
 	"github.com/unknwon/com"
@@ -120,7 +121,7 @@ func (r *Repository) CanCommitToBranch(doer *user_model.User) (CanCommitToBranch
 		requireSigned = protectedBranch.RequireSignedCommits
 	}
 
-	sign, keyID, _, err := models.SignCRUDAction(r.Repository, doer, r.Repository.RepoPath(), git.BranchPrefix+r.BranchName)
+	sign, keyID, _, err := asymkey_service.SignCRUDAction(r.Repository.RepoPath(), doer, r.Repository.RepoPath(), git.BranchPrefix+r.BranchName)
 
 	canCommit := r.CanEnableEditor() && userCanPush
 	if requireSigned {
@@ -128,8 +129,8 @@ func (r *Repository) CanCommitToBranch(doer *user_model.User) (CanCommitToBranch
 	}
 	wontSignReason := ""
 	if err != nil {
-		if models.IsErrWontSign(err) {
-			wontSignReason = string(err.(*models.ErrWontSign).Reason)
+		if asymkey_service.IsErrWontSign(err) {
+			wontSignReason = string(err.(*asymkey_service.ErrWontSign).Reason)
 			err = nil
 		} else {
 			wontSignReason = "error"
diff --git a/modules/convert/convert.go b/modules/convert/convert.go
index eda9f23cd6..1c78c35cda 100644
--- a/modules/convert/convert.go
+++ b/modules/convert/convert.go
@@ -12,6 +12,7 @@ import (
 	"time"
 
 	"code.gitea.io/gitea/models"
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/login"
 	"code.gitea.io/gitea/models/perm"
 	repo_model "code.gitea.io/gitea/models/repo"
@@ -152,7 +153,7 @@ func ToTag(repo *repo_model.Repository, t *git.Tag) *api.Tag {
 
 // ToVerification convert a git.Commit.Signature to an api.PayloadCommitVerification
 func ToVerification(c *git.Commit) *api.PayloadCommitVerification {
-	verif := models.ParseCommitWithSignature(c)
+	verif := asymkey_model.ParseCommitWithSignature(c)
 	commitVerification := &api.PayloadCommitVerification{
 		Verified: verif.Verified,
 		Reason:   verif.Reason,
@@ -170,8 +171,8 @@ func ToVerification(c *git.Commit) *api.PayloadCommitVerification {
 	return commitVerification
 }
 
-// ToPublicKey convert models.PublicKey to api.PublicKey
-func ToPublicKey(apiLink string, key *models.PublicKey) *api.PublicKey {
+// ToPublicKey convert asymkey_model.PublicKey to api.PublicKey
+func ToPublicKey(apiLink string, key *asymkey_model.PublicKey) *api.PublicKey {
 	return &api.PublicKey{
 		ID:          key.ID,
 		Key:         key.Content,
@@ -183,7 +184,7 @@ func ToPublicKey(apiLink string, key *models.PublicKey) *api.PublicKey {
 }
 
 // ToGPGKey converts models.GPGKey to api.GPGKey
-func ToGPGKey(key *models.GPGKey) *api.GPGKey {
+func ToGPGKey(key *asymkey_model.GPGKey) *api.GPGKey {
 	subkeys := make([]*api.GPGKey, len(key.SubsKey))
 	for id, k := range key.SubsKey {
 		subkeys[id] = &api.GPGKey{
@@ -264,8 +265,8 @@ func ToGitHook(h *git.Hook) *api.GitHook {
 	}
 }
 
-// ToDeployKey convert models.DeployKey to api.DeployKey
-func ToDeployKey(apiLink string, key *models.DeployKey) *api.DeployKey {
+// ToDeployKey convert asymkey_model.DeployKey to api.DeployKey
+func ToDeployKey(apiLink string, key *asymkey_model.DeployKey) *api.DeployKey {
 	return &api.DeployKey{
 		ID:          key.ID,
 		KeyID:       key.KeyID,
diff --git a/modules/doctor/authorizedkeys.go b/modules/doctor/authorizedkeys.go
index d9bc70bb77..1a9b60e248 100644
--- a/modules/doctor/authorizedkeys.go
+++ b/modules/doctor/authorizedkeys.go
@@ -12,7 +12,7 @@ import (
 	"path/filepath"
 	"strings"
 
-	"code.gitea.io/gitea/models"
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 )
@@ -32,7 +32,7 @@ func checkAuthorizedKeys(logger log.Logger, autofix bool) error {
 			return fmt.Errorf("Unable to open authorized_keys file. ERROR: %v", err)
 		}
 		logger.Warn("Unable to open authorized_keys. (ERROR: %v). Attempting to rewrite...", err)
-		if err = models.RewriteAllPublicKeys(); err != nil {
+		if err = asymkey_model.RewriteAllPublicKeys(); err != nil {
 			logger.Critical("Unable to rewrite authorized_keys file. ERROR: %v", err)
 			return fmt.Errorf("Unable to rewrite authorized_keys file. ERROR: %v", err)
 		}
@@ -53,7 +53,7 @@ func checkAuthorizedKeys(logger log.Logger, autofix bool) error {
 
 	// now we regenerate and check if there are any lines missing
 	regenerated := &bytes.Buffer{}
-	if err := models.RegeneratePublicKeys(regenerated); err != nil {
+	if err := asymkey_model.RegeneratePublicKeys(regenerated); err != nil {
 		logger.Critical("Unable to regenerate authorized_keys file. ERROR: %v", err)
 		return fmt.Errorf("Unable to regenerate authorized_keys file. ERROR: %v", err)
 	}
@@ -75,7 +75,7 @@ func checkAuthorizedKeys(logger log.Logger, autofix bool) error {
 			return fmt.Errorf(`authorized_keys is out of date and should be regenerated with "gitea admin regenerate keys" or "gitea doctor --run authorized_keys --fix"`)
 		}
 		logger.Warn("authorized_keys is out of date. Attempting rewrite...")
-		err = models.RewriteAllPublicKeys()
+		err = asymkey_model.RewriteAllPublicKeys()
 		if err != nil {
 			logger.Critical("Unable to rewrite authorized_keys file. ERROR: %v", err)
 			return fmt.Errorf("Unable to rewrite authorized_keys file. ERROR: %v", err)
diff --git a/modules/gitgraph/graph_models.go b/modules/gitgraph/graph_models.go
index e7280e2adc..4cfa96816a 100644
--- a/modules/gitgraph/graph_models.go
+++ b/modules/gitgraph/graph_models.go
@@ -10,6 +10,7 @@ import (
 	"strings"
 
 	"code.gitea.io/gitea/models"
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
@@ -113,9 +114,11 @@ func (graph *Graph) LoadAndProcessCommits(repository *repo_model.Repository, git
 			}
 		}
 
-		c.Verification = models.ParseCommitWithSignature(c.Commit)
+		c.Verification = asymkey_model.ParseCommitWithSignature(c.Commit)
 
-		_ = models.CalculateTrustStatus(c.Verification, repository, &keyMap)
+		_ = asymkey_model.CalculateTrustStatus(c.Verification, repository.GetTrustModel(), func(user *user_model.User) (bool, error) {
+			return models.IsUserRepoAdmin(repository, user)
+		}, &keyMap)
 
 		statuses, err := models.GetLatestCommitStatus(repository.ID, c.Commit.ID.String(), db.ListOptions{})
 		if err != nil {
@@ -236,7 +239,7 @@ func newRefsFromRefNames(refNames []byte) []git.Reference {
 type Commit struct {
 	Commit       *git.Commit
 	User         *user_model.User
-	Verification *models.CommitVerification
+	Verification *asymkey_model.CommitVerification
 	Status       *models.CommitStatus
 	Flow         int64
 	Row          int
diff --git a/modules/private/serv.go b/modules/private/serv.go
index 4f5b19c597..a7a56df7b3 100644
--- a/modules/private/serv.go
+++ b/modules/private/serv.go
@@ -10,7 +10,7 @@ import (
 	"net/http"
 	"net/url"
 
-	"code.gitea.io/gitea/models"
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/perm"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/json"
@@ -19,12 +19,12 @@ import (
 
 // KeyAndOwner is the response from ServNoCommand
 type KeyAndOwner struct {
-	Key   *models.PublicKey `json:"key"`
-	Owner *user_model.User  `json:"user"`
+	Key   *asymkey_model.PublicKey `json:"key"`
+	Owner *user_model.User         `json:"user"`
 }
 
 // ServNoCommand returns information about the provided key
-func ServNoCommand(ctx context.Context, keyID int64) (*models.PublicKey, *user_model.User, error) {
+func ServNoCommand(ctx context.Context, keyID int64) (*asymkey_model.PublicKey, *user_model.User, error) {
 	reqURL := setting.LocalURL + fmt.Sprintf("api/internal/serv/none/%d",
 		keyID)
 	resp, err := newInternalRequest(ctx, reqURL, "GET").Response()
diff --git a/modules/repository/init.go b/modules/repository/init.go
index cfee1a3215..08c5aac1b8 100644
--- a/modules/repository/init.go
+++ b/modules/repository/init.go
@@ -20,6 +20,7 @@ import (
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
+	asymkey_service "code.gitea.io/gitea/services/asymkey"
 
 	"github.com/unknwon/com"
 )
@@ -134,7 +135,7 @@ func initRepoCommit(tmpPath string, repo *repo_model.Repository, u *user_model.U
 	}
 
 	if git.CheckGitVersionAtLeast("1.7.9") == nil {
-		sign, keyID, signer, _ := models.SignInitialCommit(tmpPath, u)
+		sign, keyID, signer, _ := asymkey_service.SignInitialCommit(tmpPath, u)
 		if sign {
 			args = append(args, "-S"+keyID)
 
diff --git a/modules/ssh/ssh.go b/modules/ssh/ssh.go
index 5f19dd4a5c..a3756fd2af 100644
--- a/modules/ssh/ssh.go
+++ b/modules/ssh/ssh.go
@@ -22,7 +22,7 @@ import (
 	"sync"
 	"syscall"
 
-	"code.gitea.io/gitea/models"
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/util"
@@ -172,9 +172,9 @@ func publicKeyHandler(ctx ssh.Context, key ssh.PublicKey) bool {
 		// look for the exact principal
 	principalLoop:
 		for _, principal := range cert.ValidPrincipals {
-			pkey, err := models.SearchPublicKeyByContentExact(principal)
+			pkey, err := asymkey_model.SearchPublicKeyByContentExact(principal)
 			if err != nil {
-				if models.IsErrKeyNotExist(err) {
+				if asymkey_model.IsErrKeyNotExist(err) {
 					log.Debug("Principal Rejected: %s Unknown Principal: %s", ctx.RemoteAddr(), principal)
 					continue principalLoop
 				}
@@ -232,9 +232,9 @@ func publicKeyHandler(ctx ssh.Context, key ssh.PublicKey) bool {
 		log.Debug("Handle Public Key: %s Fingerprint: %s is not a certificate", ctx.RemoteAddr(), gossh.FingerprintSHA256(key))
 	}
 
-	pkey, err := models.SearchPublicKeyByContent(strings.TrimSpace(string(gossh.MarshalAuthorizedKey(key))))
+	pkey, err := asymkey_model.SearchPublicKeyByContent(strings.TrimSpace(string(gossh.MarshalAuthorizedKey(key))))
 	if err != nil {
-		if models.IsErrKeyNotExist(err) {
+		if asymkey_model.IsErrKeyNotExist(err) {
 			if log.IsWarn() {
 				log.Warn("Unknown public key: %s from %s", gossh.FingerprintSHA256(key), ctx.RemoteAddr())
 				log.Warn("Failed authentication attempt from %s", ctx.RemoteAddr())
diff --git a/routers/api/v1/admin/user.go b/routers/api/v1/admin/user.go
index b93c628072..44358b4bef 100644
--- a/routers/api/v1/admin/user.go
+++ b/routers/api/v1/admin/user.go
@@ -12,6 +12,7 @@ import (
 	"strings"
 
 	"code.gitea.io/gitea/models"
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/login"
 	user_model "code.gitea.io/gitea/models/user"
@@ -23,6 +24,7 @@ import (
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/user"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	asymkey_service "code.gitea.io/gitea/services/asymkey"
 	"code.gitea.io/gitea/services/mailer"
 	user_service "code.gitea.io/gitea/services/user"
 )
@@ -381,10 +383,10 @@ func DeleteUserPublicKey(ctx *context.APIContext) {
 		return
 	}
 
-	if err := models.DeletePublicKey(u, ctx.ParamsInt64(":id")); err != nil {
-		if models.IsErrKeyNotExist(err) {
+	if err := asymkey_service.DeletePublicKey(u, ctx.ParamsInt64(":id")); err != nil {
+		if asymkey_model.IsErrKeyNotExist(err) {
 			ctx.NotFound()
-		} else if models.IsErrKeyAccessDenied(err) {
+		} else if asymkey_model.IsErrKeyAccessDenied(err) {
 			ctx.Error(http.StatusForbidden, "", "You do not have access to this key")
 		} else {
 			ctx.Error(http.StatusInternalServerError, "DeleteUserPublicKey", err)
diff --git a/routers/api/v1/misc/signing.go b/routers/api/v1/misc/signing.go
index 3c3391bfe7..e2833173a3 100644
--- a/routers/api/v1/misc/signing.go
+++ b/routers/api/v1/misc/signing.go
@@ -8,8 +8,8 @@ import (
 	"fmt"
 	"net/http"
 
-	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/modules/context"
+	asymkey_service "code.gitea.io/gitea/services/asymkey"
 )
 
 // SigningKey returns the public key of the default signing key if it exists
@@ -52,7 +52,7 @@ func SigningKey(ctx *context.APIContext) {
 		path = ctx.Repo.Repository.RepoPath()
 	}
 
-	content, err := models.PublicSigningKey(path)
+	content, err := asymkey_service.PublicSigningKey(path)
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "gpg export", err)
 		return
diff --git a/routers/api/v1/repo/key.go b/routers/api/v1/repo/key.go
index 03ebcb8f72..669cc7c51c 100644
--- a/routers/api/v1/repo/key.go
+++ b/routers/api/v1/repo/key.go
@@ -10,7 +10,8 @@ import (
 	"net/http"
 	"net/url"
 
-	"code.gitea.io/gitea/models"
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
+	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/perm"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/modules/context"
@@ -19,10 +20,11 @@ import (
 	api "code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	asymkey_service "code.gitea.io/gitea/services/asymkey"
 )
 
 // appendPrivateInformation appends the owner and key type information to api.PublicKey
-func appendPrivateInformation(apiKey *api.DeployKey, key *models.DeployKey, repository *repo_model.Repository) (*api.DeployKey, error) {
+func appendPrivateInformation(apiKey *api.DeployKey, key *asymkey_model.DeployKey, repository *repo_model.Repository) (*api.DeployKey, error) {
 	apiKey.ReadOnly = key.Mode == perm.AccessModeRead
 	if repository.ID == key.RepoID {
 		apiKey.Repository = convert.ToRepo(repository, key.Mode)
@@ -78,20 +80,20 @@ func ListDeployKeys(ctx *context.APIContext) {
 	//   "200":
 	//     "$ref": "#/responses/DeployKeyList"
 
-	opts := &models.ListDeployKeysOptions{
+	opts := &asymkey_model.ListDeployKeysOptions{
 		ListOptions: utils.GetListOptions(ctx),
 		RepoID:      ctx.Repo.Repository.ID,
 		KeyID:       ctx.FormInt64("key_id"),
 		Fingerprint: ctx.FormString("fingerprint"),
 	}
 
-	keys, err := models.ListDeployKeys(opts)
+	keys, err := asymkey_model.ListDeployKeys(db.DefaultContext, opts)
 	if err != nil {
 		ctx.InternalServerError(err)
 		return
 	}
 
-	count, err := models.CountDeployKeys(opts)
+	count, err := asymkey_model.CountDeployKeys(opts)
 	if err != nil {
 		ctx.InternalServerError(err)
 		return
@@ -142,9 +144,9 @@ func GetDeployKey(ctx *context.APIContext) {
 	//   "200":
 	//     "$ref": "#/responses/DeployKey"
 
-	key, err := models.GetDeployKeyByID(ctx.ParamsInt64(":id"))
+	key, err := asymkey_model.GetDeployKeyByID(db.DefaultContext, ctx.ParamsInt64(":id"))
 	if err != nil {
-		if models.IsErrDeployKeyNotExist(err) {
+		if asymkey_model.IsErrDeployKeyNotExist(err) {
 			ctx.NotFound()
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetDeployKeyByID", err)
@@ -167,9 +169,9 @@ func GetDeployKey(ctx *context.APIContext) {
 
 // HandleCheckKeyStringError handle check key error
 func HandleCheckKeyStringError(ctx *context.APIContext, err error) {
-	if models.IsErrSSHDisabled(err) {
+	if db.IsErrSSHDisabled(err) {
 		ctx.Error(http.StatusUnprocessableEntity, "", "SSH is disabled")
-	} else if models.IsErrKeyUnableVerify(err) {
+	} else if asymkey_model.IsErrKeyUnableVerify(err) {
 		ctx.Error(http.StatusUnprocessableEntity, "", "Unable to verify key content")
 	} else {
 		ctx.Error(http.StatusUnprocessableEntity, "", fmt.Errorf("Invalid key content: %v", err))
@@ -179,13 +181,13 @@ func HandleCheckKeyStringError(ctx *context.APIContext, err error) {
 // HandleAddKeyError handle add key error
 func HandleAddKeyError(ctx *context.APIContext, err error) {
 	switch {
-	case models.IsErrDeployKeyAlreadyExist(err):
+	case asymkey_model.IsErrDeployKeyAlreadyExist(err):
 		ctx.Error(http.StatusUnprocessableEntity, "", "This key has already been added to this repository")
-	case models.IsErrKeyAlreadyExist(err):
+	case asymkey_model.IsErrKeyAlreadyExist(err):
 		ctx.Error(http.StatusUnprocessableEntity, "", "Key content has been used as non-deploy key")
-	case models.IsErrKeyNameAlreadyUsed(err):
+	case asymkey_model.IsErrKeyNameAlreadyUsed(err):
 		ctx.Error(http.StatusUnprocessableEntity, "", "Key title has been used")
-	case models.IsErrDeployKeyNameAlreadyUsed(err):
+	case asymkey_model.IsErrDeployKeyNameAlreadyUsed(err):
 		ctx.Error(http.StatusUnprocessableEntity, "", "A key with the same name already exists")
 	default:
 		ctx.Error(http.StatusInternalServerError, "AddKey", err)
@@ -223,13 +225,13 @@ func CreateDeployKey(ctx *context.APIContext) {
 	//     "$ref": "#/responses/validationError"
 
 	form := web.GetForm(ctx).(*api.CreateKeyOption)
-	content, err := models.CheckPublicKeyString(form.Key)
+	content, err := asymkey_model.CheckPublicKeyString(form.Key)
 	if err != nil {
 		HandleCheckKeyStringError(ctx, err)
 		return
 	}
 
-	key, err := models.AddDeployKey(ctx.Repo.Repository.ID, form.Title, content, form.ReadOnly)
+	key, err := asymkey_model.AddDeployKey(ctx.Repo.Repository.ID, form.Title, content, form.ReadOnly)
 	if err != nil {
 		HandleAddKeyError(ctx, err)
 		return
@@ -268,8 +270,8 @@ func DeleteDeploykey(ctx *context.APIContext) {
 	//   "403":
 	//     "$ref": "#/responses/forbidden"
 
-	if err := models.DeleteDeployKey(ctx.User, ctx.ParamsInt64(":id")); err != nil {
-		if models.IsErrKeyAccessDenied(err) {
+	if err := asymkey_service.DeleteDeployKey(ctx.User, ctx.ParamsInt64(":id")); err != nil {
+		if asymkey_model.IsErrKeyAccessDenied(err) {
 			ctx.Error(http.StatusForbidden, "", "You do not have access to this key")
 		} else {
 			ctx.Error(http.StatusInternalServerError, "DeleteDeployKey", err)
diff --git a/routers/api/v1/repo/pull.go b/routers/api/v1/repo/pull.go
index 3e37da24ec..e593819dac 100644
--- a/routers/api/v1/repo/pull.go
+++ b/routers/api/v1/repo/pull.go
@@ -26,6 +26,7 @@ import (
 	"code.gitea.io/gitea/modules/timeutil"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	asymkey_service "code.gitea.io/gitea/services/asymkey"
 	"code.gitea.io/gitea/services/forms"
 	issue_service "code.gitea.io/gitea/services/issue"
 	pull_service "code.gitea.io/gitea/services/pull"
@@ -810,7 +811,7 @@ func MergePullRequest(ctx *context.APIContext) {
 	}
 
 	if _, err := pull_service.IsSignedIfRequired(pr, ctx.User); err != nil {
-		if !models.IsErrWontSign(err) {
+		if !asymkey_service.IsErrWontSign(err) {
 			ctx.Error(http.StatusInternalServerError, "IsSignedIfRequired", err)
 			return
 		}
diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go
index 4486e33fe8..62f9c37244 100644
--- a/routers/api/v1/repo/repo.go
+++ b/routers/api/v1/repo/repo.go
@@ -1023,7 +1023,7 @@ func Delete(ctx *context.APIContext) {
 		ctx.Repo.GitRepo.Close()
 	}
 
-	if err := repo_service.DeleteRepository(ctx.User, repo); err != nil {
+	if err := repo_service.DeleteRepository(ctx.User, repo, true); err != nil {
 		ctx.Error(http.StatusInternalServerError, "DeleteRepository", err)
 		return
 	}
diff --git a/routers/api/v1/user/gpg_key.go b/routers/api/v1/user/gpg_key.go
index 9066268bba..b0f9f1feb3 100644
--- a/routers/api/v1/user/gpg_key.go
+++ b/routers/api/v1/user/gpg_key.go
@@ -8,7 +8,7 @@ import (
 	"fmt"
 	"net/http"
 
-	"code.gitea.io/gitea/models"
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/convert"
@@ -18,7 +18,7 @@ import (
 )
 
 func listGPGKeys(ctx *context.APIContext, uid int64, listOptions db.ListOptions) {
-	keys, err := models.ListGPGKeys(uid, listOptions)
+	keys, err := asymkey_model.ListGPGKeys(db.DefaultContext, uid, listOptions)
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "ListGPGKeys", err)
 		return
@@ -29,7 +29,7 @@ func listGPGKeys(ctx *context.APIContext, uid int64, listOptions db.ListOptions)
 		apiKeys[i] = convert.ToGPGKey(keys[i])
 	}
 
-	total, err := models.CountUserGPGKeys(uid)
+	total, err := asymkey_model.CountUserGPGKeys(uid)
 	if err != nil {
 		ctx.InternalServerError(err)
 		return
@@ -114,9 +114,9 @@ func GetGPGKey(ctx *context.APIContext) {
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 
-	key, err := models.GetGPGKeyByID(ctx.ParamsInt64(":id"))
+	key, err := asymkey_model.GetGPGKeyByID(ctx.ParamsInt64(":id"))
 	if err != nil {
-		if models.IsErrGPGKeyNotExist(err) {
+		if asymkey_model.IsErrGPGKeyNotExist(err) {
 			ctx.NotFound()
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetGPGKeyByID", err)
@@ -128,12 +128,12 @@ func GetGPGKey(ctx *context.APIContext) {
 
 // CreateUserGPGKey creates new GPG key to given user by ID.
 func CreateUserGPGKey(ctx *context.APIContext, form api.CreateGPGKeyOption, uid int64) {
-	token := models.VerificationToken(ctx.User, 1)
-	lastToken := models.VerificationToken(ctx.User, 0)
+	token := asymkey_model.VerificationToken(ctx.User, 1)
+	lastToken := asymkey_model.VerificationToken(ctx.User, 0)
 
-	keys, err := models.AddGPGKey(uid, form.ArmoredKey, token, form.Signature)
-	if err != nil && models.IsErrGPGInvalidTokenSignature(err) {
-		keys, err = models.AddGPGKey(uid, form.ArmoredKey, lastToken, form.Signature)
+	keys, err := asymkey_model.AddGPGKey(uid, form.ArmoredKey, token, form.Signature)
+	if err != nil && asymkey_model.IsErrGPGInvalidTokenSignature(err) {
+		keys, err = asymkey_model.AddGPGKey(uid, form.ArmoredKey, lastToken, form.Signature)
 	}
 	if err != nil {
 		HandleAddGPGKeyError(ctx, err, token)
@@ -156,7 +156,7 @@ func GetVerificationToken(ctx *context.APIContext) {
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 
-	token := models.VerificationToken(ctx.User, 1)
+	token := asymkey_model.VerificationToken(ctx.User, 1)
 	ctx.PlainText(http.StatusOK, []byte(token))
 }
 
@@ -178,25 +178,25 @@ func VerifyUserGPGKey(ctx *context.APIContext) {
 	//     "$ref": "#/responses/validationError"
 
 	form := web.GetForm(ctx).(*api.VerifyGPGKeyOption)
-	token := models.VerificationToken(ctx.User, 1)
-	lastToken := models.VerificationToken(ctx.User, 0)
+	token := asymkey_model.VerificationToken(ctx.User, 1)
+	lastToken := asymkey_model.VerificationToken(ctx.User, 0)
 
-	_, err := models.VerifyGPGKey(ctx.User.ID, form.KeyID, token, form.Signature)
-	if err != nil && models.IsErrGPGInvalidTokenSignature(err) {
-		_, err = models.VerifyGPGKey(ctx.User.ID, form.KeyID, lastToken, form.Signature)
+	_, err := asymkey_model.VerifyGPGKey(ctx.User.ID, form.KeyID, token, form.Signature)
+	if err != nil && asymkey_model.IsErrGPGInvalidTokenSignature(err) {
+		_, err = asymkey_model.VerifyGPGKey(ctx.User.ID, form.KeyID, lastToken, form.Signature)
 	}
 
 	if err != nil {
-		if models.IsErrGPGInvalidTokenSignature(err) {
+		if asymkey_model.IsErrGPGInvalidTokenSignature(err) {
 			ctx.Error(http.StatusUnprocessableEntity, "GPGInvalidSignature", fmt.Sprintf("The provided GPG key, signature and token do not match or token is out of date. Provide a valid signature for the token: %s", token))
 			return
 		}
 		ctx.Error(http.StatusInternalServerError, "VerifyUserGPGKey", err)
 	}
 
-	key, err := models.GetGPGKeysByKeyID(form.KeyID)
+	key, err := asymkey_model.GetGPGKeysByKeyID(form.KeyID)
 	if err != nil {
-		if models.IsErrGPGKeyNotExist(err) {
+		if asymkey_model.IsErrGPGKeyNotExist(err) {
 			ctx.NotFound()
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetGPGKeysByKeyID", err)
@@ -255,8 +255,8 @@ func DeleteGPGKey(ctx *context.APIContext) {
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 
-	if err := models.DeleteGPGKey(ctx.User, ctx.ParamsInt64(":id")); err != nil {
-		if models.IsErrGPGKeyAccessDenied(err) {
+	if err := asymkey_model.DeleteGPGKey(ctx.User, ctx.ParamsInt64(":id")); err != nil {
+		if asymkey_model.IsErrGPGKeyAccessDenied(err) {
 			ctx.Error(http.StatusForbidden, "", "You do not have access to this key")
 		} else {
 			ctx.Error(http.StatusInternalServerError, "DeleteGPGKey", err)
@@ -270,15 +270,15 @@ func DeleteGPGKey(ctx *context.APIContext) {
 // HandleAddGPGKeyError handle add GPGKey error
 func HandleAddGPGKeyError(ctx *context.APIContext, err error, token string) {
 	switch {
-	case models.IsErrGPGKeyAccessDenied(err):
+	case asymkey_model.IsErrGPGKeyAccessDenied(err):
 		ctx.Error(http.StatusUnprocessableEntity, "GPGKeyAccessDenied", "You do not have access to this GPG key")
-	case models.IsErrGPGKeyIDAlreadyUsed(err):
+	case asymkey_model.IsErrGPGKeyIDAlreadyUsed(err):
 		ctx.Error(http.StatusUnprocessableEntity, "GPGKeyIDAlreadyUsed", "A key with the same id already exists")
-	case models.IsErrGPGKeyParsing(err):
+	case asymkey_model.IsErrGPGKeyParsing(err):
 		ctx.Error(http.StatusUnprocessableEntity, "GPGKeyParsing", err)
-	case models.IsErrGPGNoEmailFound(err):
+	case asymkey_model.IsErrGPGNoEmailFound(err):
 		ctx.Error(http.StatusNotFound, "GPGNoEmailFound", fmt.Sprintf("None of the emails attached to the GPG key could be found. It may still be added if you provide a valid signature for the token: %s", token))
-	case models.IsErrGPGInvalidTokenSignature(err):
+	case asymkey_model.IsErrGPGInvalidTokenSignature(err):
 		ctx.Error(http.StatusUnprocessableEntity, "GPGInvalidSignature", fmt.Sprintf("The provided GPG key, signature and token do not match or token is out of date. Provide a valid signature for the token: %s", token))
 	default:
 		ctx.Error(http.StatusInternalServerError, "AddGPGKey", err)
diff --git a/routers/api/v1/user/key.go b/routers/api/v1/user/key.go
index 32291264bc..e8cc2035e5 100644
--- a/routers/api/v1/user/key.go
+++ b/routers/api/v1/user/key.go
@@ -7,7 +7,7 @@ package user
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/models"
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/perm"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/context"
@@ -17,13 +17,14 @@ import (
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/api/v1/repo"
 	"code.gitea.io/gitea/routers/api/v1/utils"
+	asymkey_service "code.gitea.io/gitea/services/asymkey"
 )
 
 // appendPrivateInformation appends the owner and key type information to api.PublicKey
-func appendPrivateInformation(apiKey *api.PublicKey, key *models.PublicKey, defaultUser *user_model.User) (*api.PublicKey, error) {
-	if key.Type == models.KeyTypeDeploy {
+func appendPrivateInformation(apiKey *api.PublicKey, key *asymkey_model.PublicKey, defaultUser *user_model.User) (*api.PublicKey, error) {
+	if key.Type == asymkey_model.KeyTypeDeploy {
 		apiKey.KeyType = "deploy"
-	} else if key.Type == models.KeyTypeUser {
+	} else if key.Type == asymkey_model.KeyTypeUser {
 		apiKey.KeyType = "user"
 
 		if defaultUser.ID == key.OwnerID {
@@ -47,7 +48,7 @@ func composePublicKeysAPILink() string {
 }
 
 func listPublicKeys(ctx *context.APIContext, user *user_model.User) {
-	var keys []*models.PublicKey
+	var keys []*asymkey_model.PublicKey
 	var err error
 	var count int
 
@@ -58,14 +59,14 @@ func listPublicKeys(ctx *context.APIContext, user *user_model.User) {
 		// Querying not just listing
 		if username != "" {
 			// Restrict to provided uid
-			keys, err = models.SearchPublicKey(user.ID, fingerprint)
+			keys, err = asymkey_model.SearchPublicKey(user.ID, fingerprint)
 		} else {
 			// Unrestricted
-			keys, err = models.SearchPublicKey(0, fingerprint)
+			keys, err = asymkey_model.SearchPublicKey(0, fingerprint)
 		}
 		count = len(keys)
 	} else {
-		total, err2 := models.CountPublicKeys(user.ID)
+		total, err2 := asymkey_model.CountPublicKeys(user.ID)
 		if err2 != nil {
 			ctx.InternalServerError(err)
 			return
@@ -73,7 +74,7 @@ func listPublicKeys(ctx *context.APIContext, user *user_model.User) {
 		count = int(total)
 
 		// Use ListPublicKeys
-		keys, err = models.ListPublicKeys(user.ID, utils.GetListOptions(ctx))
+		keys, err = asymkey_model.ListPublicKeys(user.ID, utils.GetListOptions(ctx))
 	}
 
 	if err != nil {
@@ -177,9 +178,9 @@ func GetPublicKey(ctx *context.APIContext) {
 	//   "404":
 	//     "$ref": "#/responses/notFound"
 
-	key, err := models.GetPublicKeyByID(ctx.ParamsInt64(":id"))
+	key, err := asymkey_model.GetPublicKeyByID(ctx.ParamsInt64(":id"))
 	if err != nil {
-		if models.IsErrKeyNotExist(err) {
+		if asymkey_model.IsErrKeyNotExist(err) {
 			ctx.NotFound()
 		} else {
 			ctx.Error(http.StatusInternalServerError, "GetPublicKeyByID", err)
@@ -197,13 +198,13 @@ func GetPublicKey(ctx *context.APIContext) {
 
 // CreateUserPublicKey creates new public key to given user by ID.
 func CreateUserPublicKey(ctx *context.APIContext, form api.CreateKeyOption, uid int64) {
-	content, err := models.CheckPublicKeyString(form.Key)
+	content, err := asymkey_model.CheckPublicKeyString(form.Key)
 	if err != nil {
 		repo.HandleCheckKeyStringError(ctx, err)
 		return
 	}
 
-	key, err := models.AddPublicKey(uid, form.Title, content, 0)
+	key, err := asymkey_model.AddPublicKey(uid, form.Title, content, 0)
 	if err != nil {
 		repo.HandleAddKeyError(ctx, err)
 		return
@@ -263,7 +264,7 @@ func DeletePublicKey(ctx *context.APIContext) {
 	//     "$ref": "#/responses/notFound"
 
 	id := ctx.ParamsInt64(":id")
-	externallyManaged, err := models.PublicKeyIsExternallyManaged(id)
+	externallyManaged, err := asymkey_model.PublicKeyIsExternallyManaged(id)
 	if err != nil {
 		ctx.Error(http.StatusInternalServerError, "PublicKeyIsExternallyManaged", err)
 	}
@@ -271,10 +272,10 @@ func DeletePublicKey(ctx *context.APIContext) {
 		ctx.Error(http.StatusForbidden, "", "SSH Key is externally managed for this user")
 	}
 
-	if err := models.DeletePublicKey(ctx.User, id); err != nil {
-		if models.IsErrKeyNotExist(err) {
+	if err := asymkey_service.DeletePublicKey(ctx.User, id); err != nil {
+		if asymkey_model.IsErrKeyNotExist(err) {
 			ctx.NotFound()
-		} else if models.IsErrKeyAccessDenied(err) {
+		} else if asymkey_model.IsErrKeyAccessDenied(err) {
 			ctx.Error(http.StatusForbidden, "", "You do not have access to this key")
 		} else {
 			ctx.Error(http.StatusInternalServerError, "DeletePublicKey", err)
diff --git a/routers/init.go b/routers/init.go
index 2143ab476b..804dfd6533 100644
--- a/routers/init.go
+++ b/routers/init.go
@@ -13,6 +13,7 @@ import (
 	"strings"
 
 	"code.gitea.io/gitea/models"
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/modules/appstate"
 	"code.gitea.io/gitea/modules/cache"
 	"code.gitea.io/gitea/modules/eventsource"
@@ -87,7 +88,7 @@ func syncAppPathForGit(ctx context.Context) error {
 		mustInitCtx(ctx, repo_service.SyncRepositoryHooks)
 
 		log.Info("re-write ssh public keys ...")
-		mustInit(models.RewriteAllPublicKeys)
+		mustInit(asymkey_model.RewriteAllPublicKeys)
 
 		runtimeState.LastAppPath = setting.AppPath
 		return appstate.AppState.Set(runtimeState)
diff --git a/routers/private/hook_verification.go b/routers/private/hook_verification.go
index 8c7492ea68..1b44550cff 100644
--- a/routers/private/hook_verification.go
+++ b/routers/private/hook_verification.go
@@ -12,7 +12,7 @@ import (
 	"io"
 	"os"
 
-	"code.gitea.io/gitea/models"
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
 )
@@ -97,7 +97,7 @@ func readAndVerifyCommit(sha string, repo *git.Repository, env []string) error {
 				if err != nil {
 					return err
 				}
-				verification := models.ParseCommitWithSignature(commit)
+				verification := asymkey_model.ParseCommitWithSignature(commit)
 				if !verification.Verified {
 					cancel()
 					return &errUnverifiedCommit{
diff --git a/routers/private/key.go b/routers/private/key.go
index 4f518e41f0..30fde73c8a 100644
--- a/routers/private/key.go
+++ b/routers/private/key.go
@@ -8,7 +8,7 @@ package private
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/models"
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/private"
 	"code.gitea.io/gitea/modules/timeutil"
@@ -18,16 +18,16 @@ import (
 func UpdatePublicKeyInRepo(ctx *context.PrivateContext) {
 	keyID := ctx.ParamsInt64(":id")
 	repoID := ctx.ParamsInt64(":repoid")
-	if err := models.UpdatePublicKeyUpdated(keyID); err != nil {
+	if err := asymkey_model.UpdatePublicKeyUpdated(keyID); err != nil {
 		ctx.JSON(http.StatusInternalServerError, private.Response{
 			Err: err.Error(),
 		})
 		return
 	}
 
-	deployKey, err := models.GetDeployKeyByRepo(keyID, repoID)
+	deployKey, err := asymkey_model.GetDeployKeyByRepo(keyID, repoID)
 	if err != nil {
-		if models.IsErrDeployKeyNotExist(err) {
+		if asymkey_model.IsErrDeployKeyNotExist(err) {
 			ctx.PlainText(http.StatusOK, []byte("success"))
 			return
 		}
@@ -37,7 +37,7 @@ func UpdatePublicKeyInRepo(ctx *context.PrivateContext) {
 		return
 	}
 	deployKey.UpdatedUnix = timeutil.TimeStampNow()
-	if err = models.UpdateDeployKeyCols(deployKey, "updated_unix"); err != nil {
+	if err = asymkey_model.UpdateDeployKeyCols(deployKey, "updated_unix"); err != nil {
 		ctx.JSON(http.StatusInternalServerError, private.Response{
 			Err: err.Error(),
 		})
@@ -52,7 +52,7 @@ func UpdatePublicKeyInRepo(ctx *context.PrivateContext) {
 func AuthorizedPublicKeyByContent(ctx *context.PrivateContext) {
 	content := ctx.FormString("content")
 
-	publicKey, err := models.SearchPublicKeyByContent(content)
+	publicKey, err := asymkey_model.SearchPublicKeyByContent(content)
 	if err != nil {
 		ctx.JSON(http.StatusInternalServerError, private.Response{
 			Err: err.Error(),
diff --git a/routers/private/serv.go b/routers/private/serv.go
index f28d5a7450..e5ebc5aa92 100644
--- a/routers/private/serv.go
+++ b/routers/private/serv.go
@@ -11,6 +11,7 @@ import (
 	"strings"
 
 	"code.gitea.io/gitea/models"
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/perm"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
@@ -34,9 +35,9 @@ func ServNoCommand(ctx *context.PrivateContext) {
 	}
 	results := private.KeyAndOwner{}
 
-	key, err := models.GetPublicKeyByID(keyID)
+	key, err := asymkey_model.GetPublicKeyByID(keyID)
 	if err != nil {
-		if models.IsErrKeyNotExist(err) {
+		if asymkey_model.IsErrKeyNotExist(err) {
 			ctx.JSON(http.StatusUnauthorized, private.Response{
 				Err: fmt.Sprintf("Cannot find key: %d", keyID),
 			})
@@ -50,7 +51,7 @@ func ServNoCommand(ctx *context.PrivateContext) {
 	}
 	results.Key = key
 
-	if key.Type == models.KeyTypeUser || key.Type == models.KeyTypePrincipal {
+	if key.Type == asymkey_model.KeyTypeUser || key.Type == asymkey_model.KeyTypePrincipal {
 		user, err := user_model.GetUserByID(key.OwnerID)
 		if err != nil {
 			if user_model.IsErrUserNotExist(err) {
@@ -184,9 +185,9 @@ func ServCommand(ctx *context.PrivateContext) {
 	}
 
 	// Get the Public Key represented by the keyID
-	key, err := models.GetPublicKeyByID(keyID)
+	key, err := asymkey_model.GetPublicKeyByID(keyID)
 	if err != nil {
-		if models.IsErrKeyNotExist(err) {
+		if asymkey_model.IsErrKeyNotExist(err) {
 			ctx.JSON(http.StatusNotFound, private.ErrServCommand{
 				Results: results,
 				Err:     fmt.Sprintf("Cannot find key: %d", keyID),
@@ -205,7 +206,7 @@ func ServCommand(ctx *context.PrivateContext) {
 	results.UserID = key.OwnerID
 
 	// If repo doesn't exist, deploy key doesn't make sense
-	if !repoExist && key.Type == models.KeyTypeDeploy {
+	if !repoExist && key.Type == asymkey_model.KeyTypeDeploy {
 		ctx.JSON(http.StatusNotFound, private.ErrServCommand{
 			Results: results,
 			Err:     fmt.Sprintf("Cannot find repository %s/%s", results.OwnerName, results.RepoName),
@@ -216,15 +217,15 @@ func ServCommand(ctx *context.PrivateContext) {
 	// Deploy Keys have ownerID set to 0 therefore we can't use the owner
 	// So now we need to check if the key is a deploy key
 	// We'll keep hold of the deploy key here for permissions checking
-	var deployKey *models.DeployKey
+	var deployKey *asymkey_model.DeployKey
 	var user *user_model.User
-	if key.Type == models.KeyTypeDeploy {
+	if key.Type == asymkey_model.KeyTypeDeploy {
 		results.IsDeployKey = true
 
 		var err error
-		deployKey, err = models.GetDeployKeyByRepo(key.ID, repo.ID)
+		deployKey, err = asymkey_model.GetDeployKeyByRepo(key.ID, repo.ID)
 		if err != nil {
-			if models.IsErrDeployKeyNotExist(err) {
+			if asymkey_model.IsErrDeployKeyNotExist(err) {
 				ctx.JSON(http.StatusNotFound, private.ErrServCommand{
 					Results: results,
 					Err:     fmt.Sprintf("Public (Deploy) Key: %d:%s is not authorized to %s %s/%s.", key.ID, key.Name, modeString, results.OwnerName, results.RepoName),
@@ -297,7 +298,7 @@ func ServCommand(ctx *context.PrivateContext) {
 			owner.Visibility.IsPrivate() ||
 			(user != nil && user.IsRestricted) || // user will be nil if the key is a deploykey
 			setting.Service.RequireSignInView) {
-		if key.Type == models.KeyTypeDeploy {
+		if key.Type == asymkey_model.KeyTypeDeploy {
 			if deployKey.Mode < mode {
 				ctx.JSON(http.StatusUnauthorized, private.ErrServCommand{
 					Results: results,
diff --git a/routers/web/admin/repos.go b/routers/web/admin/repos.go
index 7db8b4d337..7e3ec96fce 100644
--- a/routers/web/admin/repos.go
+++ b/routers/web/admin/repos.go
@@ -52,7 +52,7 @@ func DeleteRepo(ctx *context.Context) {
 		ctx.Repo.GitRepo.Close()
 	}
 
-	if err := repo_service.DeleteRepository(ctx.User, repo); err != nil {
+	if err := repo_service.DeleteRepository(ctx.User, repo, true); err != nil {
 		ctx.ServerError("DeleteRepository", err)
 		return
 	}
diff --git a/routers/web/repo/commit.go b/routers/web/repo/commit.go
index 5bc9f9eae9..0a51e43cf7 100644
--- a/routers/web/repo/commit.go
+++ b/routers/web/repo/commit.go
@@ -11,6 +11,7 @@ import (
 	"strings"
 
 	"code.gitea.io/gitea/models"
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
@@ -344,13 +345,15 @@ func Diff(ctx *context.Context) {
 	ctx.Data["CommitStatus"] = models.CalcCommitStatus(statuses)
 	ctx.Data["CommitStatuses"] = statuses
 
-	verification := models.ParseCommitWithSignature(commit)
+	verification := asymkey_model.ParseCommitWithSignature(commit)
 	ctx.Data["Verification"] = verification
 	ctx.Data["Author"] = user_model.ValidateCommitWithEmail(commit)
 	ctx.Data["Parents"] = parents
 	ctx.Data["DiffNotAvailable"] = diff.NumFiles == 0
 
-	if err := models.CalculateTrustStatus(verification, ctx.Repo.Repository, nil); err != nil {
+	if err := asymkey_model.CalculateTrustStatus(verification, ctx.Repo.Repository.GetTrustModel(), func(user *user_model.User) (bool, error) {
+		return models.IsUserRepoAdmin(ctx.Repo.Repository, user)
+	}, nil); err != nil {
 		ctx.ServerError("CalculateTrustStatus", err)
 		return
 	}
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index a5eecd0c33..f03a8dfbc2 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -34,6 +34,7 @@ import (
 	"code.gitea.io/gitea/modules/upload"
 	"code.gitea.io/gitea/modules/util"
 	"code.gitea.io/gitea/modules/web"
+	asymkey_service "code.gitea.io/gitea/services/asymkey"
 	comment_service "code.gitea.io/gitea/services/comments"
 	"code.gitea.io/gitea/services/forms"
 	issue_service "code.gitea.io/gitea/services/issue"
@@ -1583,12 +1584,12 @@ func ViewIssue(ctx *context.Context) {
 		}
 		ctx.Data["WillSign"] = false
 		if ctx.User != nil {
-			sign, key, _, err := pull.SignMerge(ctx.User, pull.BaseRepo.RepoPath(), pull.BaseBranch, pull.GetGitRefName())
+			sign, key, _, err := asymkey_service.SignMerge(pull, ctx.User, pull.BaseRepo.RepoPath(), pull.BaseBranch, pull.GetGitRefName())
 			ctx.Data["WillSign"] = sign
 			ctx.Data["SigningKey"] = key
 			if err != nil {
-				if models.IsErrWontSign(err) {
-					ctx.Data["WontSignReason"] = err.(*models.ErrWontSign).Reason
+				if asymkey_service.IsErrWontSign(err) {
+					ctx.Data["WontSignReason"] = err.(*asymkey_service.ErrWontSign).Reason
 				} else {
 					ctx.Data["WontSignReason"] = "error"
 					log.Error("Error whilst checking if could sign pr %d in repo %s. Error: %v", pull.ID, pull.BaseRepo.FullName(), err)
diff --git a/routers/web/repo/setting.go b/routers/web/repo/setting.go
index 68b53f2e90..bb118a9035 100644
--- a/routers/web/repo/setting.go
+++ b/routers/web/repo/setting.go
@@ -15,6 +15,7 @@ import (
 	"time"
 
 	"code.gitea.io/gitea/models"
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/perm"
 	repo_model "code.gitea.io/gitea/models/repo"
@@ -34,6 +35,7 @@ import (
 	"code.gitea.io/gitea/modules/validation"
 	"code.gitea.io/gitea/modules/web"
 	"code.gitea.io/gitea/routers/utils"
+	asymkey_service "code.gitea.io/gitea/services/asymkey"
 	"code.gitea.io/gitea/services/forms"
 	"code.gitea.io/gitea/services/mailer"
 	"code.gitea.io/gitea/services/migrations"
@@ -62,7 +64,7 @@ func Settings(ctx *context.Context) {
 	ctx.Data["DisableNewPushMirrors"] = setting.Mirror.DisableNewPush
 	ctx.Data["DefaultMirrorInterval"] = setting.Mirror.DefaultInterval
 
-	signing, _ := models.SigningKey(ctx.Repo.Repository.RepoPath())
+	signing, _ := asymkey_service.SigningKey(ctx.Repo.Repository.RepoPath())
 	ctx.Data["SigningKeyAvailable"] = len(signing) > 0
 	ctx.Data["SigningSettings"] = setting.Repository.Signing
 	pushMirrors, err := repo_model.GetPushMirrorsByRepoID(ctx.Repo.Repository.ID)
@@ -476,7 +478,6 @@ func SettingsPost(ctx *context.Context) {
 
 	case "signing":
 		changed := false
-
 		trustModel := repo_model.ToTrustModel(form.TrustModel)
 		if trustModel != repo.TrustModel {
 			repo.TrustModel = trustModel
@@ -673,7 +674,7 @@ func SettingsPost(ctx *context.Context) {
 			ctx.Repo.GitRepo.Close()
 		}
 
-		if err := repo_service.DeleteRepository(ctx.User, ctx.Repo.Repository); err != nil {
+		if err := repo_service.DeleteRepository(ctx.User, ctx.Repo.Repository, true); err != nil {
 			ctx.ServerError("DeleteRepository", err)
 			return
 		}
@@ -1029,7 +1030,7 @@ func DeployKeys(ctx *context.Context) {
 	ctx.Data["PageIsSettingsKeys"] = true
 	ctx.Data["DisableSSH"] = setting.SSH.Disabled
 
-	keys, err := models.ListDeployKeys(&models.ListDeployKeysOptions{RepoID: ctx.Repo.Repository.ID})
+	keys, err := asymkey_model.ListDeployKeys(db.DefaultContext, &asymkey_model.ListDeployKeysOptions{RepoID: ctx.Repo.Repository.ID})
 	if err != nil {
 		ctx.ServerError("ListDeployKeys", err)
 		return
@@ -1045,7 +1046,7 @@ func DeployKeysPost(ctx *context.Context) {
 	ctx.Data["Title"] = ctx.Tr("repo.settings.deploy_keys")
 	ctx.Data["PageIsSettingsKeys"] = true
 
-	keys, err := models.ListDeployKeys(&models.ListDeployKeysOptions{RepoID: ctx.Repo.Repository.ID})
+	keys, err := asymkey_model.ListDeployKeys(db.DefaultContext, &asymkey_model.ListDeployKeysOptions{RepoID: ctx.Repo.Repository.ID})
 	if err != nil {
 		ctx.ServerError("ListDeployKeys", err)
 		return
@@ -1057,11 +1058,11 @@ func DeployKeysPost(ctx *context.Context) {
 		return
 	}
 
-	content, err := models.CheckPublicKeyString(form.Content)
+	content, err := asymkey_model.CheckPublicKeyString(form.Content)
 	if err != nil {
-		if models.IsErrSSHDisabled(err) {
+		if db.IsErrSSHDisabled(err) {
 			ctx.Flash.Info(ctx.Tr("settings.ssh_disabled"))
-		} else if models.IsErrKeyUnableVerify(err) {
+		} else if asymkey_model.IsErrKeyUnableVerify(err) {
 			ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key"))
 		} else {
 			ctx.Data["HasError"] = true
@@ -1072,20 +1073,20 @@ func DeployKeysPost(ctx *context.Context) {
 		return
 	}
 
-	key, err := models.AddDeployKey(ctx.Repo.Repository.ID, form.Title, content, !form.IsWritable)
+	key, err := asymkey_model.AddDeployKey(ctx.Repo.Repository.ID, form.Title, content, !form.IsWritable)
 	if err != nil {
 		ctx.Data["HasError"] = true
 		switch {
-		case models.IsErrDeployKeyAlreadyExist(err):
+		case asymkey_model.IsErrDeployKeyAlreadyExist(err):
 			ctx.Data["Err_Content"] = true
 			ctx.RenderWithErr(ctx.Tr("repo.settings.key_been_used"), tplDeployKeys, &form)
-		case models.IsErrKeyAlreadyExist(err):
+		case asymkey_model.IsErrKeyAlreadyExist(err):
 			ctx.Data["Err_Content"] = true
 			ctx.RenderWithErr(ctx.Tr("settings.ssh_key_been_used"), tplDeployKeys, &form)
-		case models.IsErrKeyNameAlreadyUsed(err):
+		case asymkey_model.IsErrKeyNameAlreadyUsed(err):
 			ctx.Data["Err_Title"] = true
 			ctx.RenderWithErr(ctx.Tr("repo.settings.key_name_used"), tplDeployKeys, &form)
-		case models.IsErrDeployKeyNameAlreadyUsed(err):
+		case asymkey_model.IsErrDeployKeyNameAlreadyUsed(err):
 			ctx.Data["Err_Title"] = true
 			ctx.RenderWithErr(ctx.Tr("repo.settings.key_name_used"), tplDeployKeys, &form)
 		default:
@@ -1101,7 +1102,7 @@ func DeployKeysPost(ctx *context.Context) {
 
 // DeleteDeployKey response for deleting a deploy key
 func DeleteDeployKey(ctx *context.Context) {
-	if err := models.DeleteDeployKey(ctx.User, ctx.FormInt64("id")); err != nil {
+	if err := asymkey_service.DeleteDeployKey(ctx.User, ctx.FormInt64("id")); err != nil {
 		ctx.Flash.Error("DeleteDeployKey: " + err.Error())
 	} else {
 		ctx.Flash.Success(ctx.Tr("repo.settings.deploy_key_deletion_success"))
diff --git a/routers/web/repo/settings_test.go b/routers/web/repo/settings_test.go
index fe6e9b52ba..5b62e1ed16 100644
--- a/routers/web/repo/settings_test.go
+++ b/routers/web/repo/settings_test.go
@@ -10,6 +10,7 @@ import (
 	"testing"
 
 	"code.gitea.io/gitea/models"
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/perm"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unittest"
@@ -61,7 +62,7 @@ func TestAddReadOnlyDeployKey(t *testing.T) {
 	DeployKeysPost(ctx)
 	assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
 
-	unittest.AssertExistsAndLoadBean(t, &models.DeployKey{
+	unittest.AssertExistsAndLoadBean(t, &asymkey_model.DeployKey{
 		Name:    addKeyForm.Title,
 		Content: addKeyForm.Content,
 		Mode:    perm.AccessModeRead,
@@ -91,7 +92,7 @@ func TestAddReadWriteOnlyDeployKey(t *testing.T) {
 	DeployKeysPost(ctx)
 	assert.EqualValues(t, http.StatusFound, ctx.Resp.Status())
 
-	unittest.AssertExistsAndLoadBean(t, &models.DeployKey{
+	unittest.AssertExistsAndLoadBean(t, &asymkey_model.DeployKey{
 		Name:    addKeyForm.Title,
 		Content: addKeyForm.Content,
 		Mode:    perm.AccessModeWrite,
diff --git a/routers/web/repo/view.go b/routers/web/repo/view.go
index bc8396ab79..9e0f670cf9 100644
--- a/routers/web/repo/view.go
+++ b/routers/web/repo/view.go
@@ -20,6 +20,7 @@ import (
 	"time"
 
 	"code.gitea.io/gitea/models"
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	unit_model "code.gitea.io/gitea/models/unit"
@@ -777,9 +778,11 @@ func renderDirectoryFiles(ctx *context.Context, timeout time.Duration) git.Entri
 	ctx.Data["LatestCommit"] = latestCommit
 	if latestCommit != nil {
 
-		verification := models.ParseCommitWithSignature(latestCommit)
+		verification := asymkey_model.ParseCommitWithSignature(latestCommit)
 
-		if err := models.CalculateTrustStatus(verification, ctx.Repo.Repository, nil); err != nil {
+		if err := asymkey_model.CalculateTrustStatus(verification, ctx.Repo.Repository.GetTrustModel(), func(user *user_model.User) (bool, error) {
+			return models.IsUserRepoAdmin(ctx.Repo.Repository, user)
+		}, nil); err != nil {
 			ctx.ServerError("CalculateTrustStatus", err)
 			return nil
 		}
diff --git a/routers/web/user/home.go b/routers/web/user/home.go
index d3883571ed..bad621f91f 100644
--- a/routers/web/user/home.go
+++ b/routers/web/user/home.go
@@ -15,6 +15,7 @@ import (
 	"strings"
 
 	"code.gitea.io/gitea/models"
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/models/unit"
@@ -827,7 +828,7 @@ func repoIDMap(ctxUser *user_model.User, issueCountByRepo map[int64]int64, unitT
 
 // ShowSSHKeys output all the ssh keys of user by uid
 func ShowSSHKeys(ctx *context.Context, uid int64) {
-	keys, err := models.ListPublicKeys(uid, db.ListOptions{})
+	keys, err := asymkey_model.ListPublicKeys(uid, db.ListOptions{})
 	if err != nil {
 		ctx.ServerError("ListPublicKeys", err)
 		return
@@ -843,7 +844,7 @@ func ShowSSHKeys(ctx *context.Context, uid int64) {
 
 // ShowGPGKeys output all the public GPG keys of user by uid
 func ShowGPGKeys(ctx *context.Context, uid int64) {
-	keys, err := models.ListGPGKeys(uid, db.ListOptions{})
+	keys, err := asymkey_model.ListGPGKeys(db.DefaultContext, uid, db.ListOptions{})
 	if err != nil {
 		ctx.ServerError("ListGPGKeys", err)
 		return
@@ -851,9 +852,9 @@ func ShowGPGKeys(ctx *context.Context, uid int64) {
 	entities := make([]*openpgp.Entity, 0)
 	failedEntitiesID := make([]string, 0)
 	for _, k := range keys {
-		e, err := models.GPGKeyToEntity(k)
+		e, err := asymkey_model.GPGKeyToEntity(k)
 		if err != nil {
-			if models.IsErrGPGKeyImportNotExist(err) {
+			if asymkey_model.IsErrGPGKeyImportNotExist(err) {
 				failedEntitiesID = append(failedEntitiesID, k.KeyID)
 				continue //Skip previous import without backup of imported armored key
 			}
diff --git a/routers/web/user/setting/keys.go b/routers/web/user/setting/keys.go
index 22a0fe4741..ec55462149 100644
--- a/routers/web/user/setting/keys.go
+++ b/routers/web/user/setting/keys.go
@@ -8,12 +8,13 @@ package setting
 import (
 	"net/http"
 
-	"code.gitea.io/gitea/models"
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/web"
+	asymkey_service "code.gitea.io/gitea/services/asymkey"
 	"code.gitea.io/gitea/services/forms"
 )
 
@@ -51,9 +52,9 @@ func KeysPost(ctx *context.Context) {
 	}
 	switch form.Type {
 	case "principal":
-		content, err := models.CheckPrincipalKeyString(ctx.User, form.Content)
+		content, err := asymkey_model.CheckPrincipalKeyString(ctx.User, form.Content)
 		if err != nil {
-			if models.IsErrSSHDisabled(err) {
+			if db.IsErrSSHDisabled(err) {
 				ctx.Flash.Info(ctx.Tr("settings.ssh_disabled"))
 			} else {
 				ctx.Flash.Error(ctx.Tr("form.invalid_ssh_principal", err.Error()))
@@ -61,10 +62,10 @@ func KeysPost(ctx *context.Context) {
 			ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
 			return
 		}
-		if _, err = models.AddPrincipalKey(ctx.User.ID, content, 0); err != nil {
+		if _, err = asymkey_model.AddPrincipalKey(ctx.User.ID, content, 0); err != nil {
 			ctx.Data["HasPrincipalError"] = true
 			switch {
-			case models.IsErrKeyAlreadyExist(err), models.IsErrKeyNameAlreadyUsed(err):
+			case asymkey_model.IsErrKeyAlreadyExist(err), asymkey_model.IsErrKeyNameAlreadyUsed(err):
 				loadKeysData(ctx)
 
 				ctx.Data["Err_Content"] = true
@@ -77,36 +78,36 @@ func KeysPost(ctx *context.Context) {
 		ctx.Flash.Success(ctx.Tr("settings.add_principal_success", form.Content))
 		ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
 	case "gpg":
-		token := models.VerificationToken(ctx.User, 1)
-		lastToken := models.VerificationToken(ctx.User, 0)
+		token := asymkey_model.VerificationToken(ctx.User, 1)
+		lastToken := asymkey_model.VerificationToken(ctx.User, 0)
 
-		keys, err := models.AddGPGKey(ctx.User.ID, form.Content, token, form.Signature)
-		if err != nil && models.IsErrGPGInvalidTokenSignature(err) {
-			keys, err = models.AddGPGKey(ctx.User.ID, form.Content, lastToken, form.Signature)
+		keys, err := asymkey_model.AddGPGKey(ctx.User.ID, form.Content, token, form.Signature)
+		if err != nil && asymkey_model.IsErrGPGInvalidTokenSignature(err) {
+			keys, err = asymkey_model.AddGPGKey(ctx.User.ID, form.Content, lastToken, form.Signature)
 		}
 		if err != nil {
 			ctx.Data["HasGPGError"] = true
 			switch {
-			case models.IsErrGPGKeyParsing(err):
+			case asymkey_model.IsErrGPGKeyParsing(err):
 				ctx.Flash.Error(ctx.Tr("form.invalid_gpg_key", err.Error()))
 				ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
-			case models.IsErrGPGKeyIDAlreadyUsed(err):
+			case asymkey_model.IsErrGPGKeyIDAlreadyUsed(err):
 				loadKeysData(ctx)
 
 				ctx.Data["Err_Content"] = true
 				ctx.RenderWithErr(ctx.Tr("settings.gpg_key_id_used"), tplSettingsKeys, &form)
-			case models.IsErrGPGInvalidTokenSignature(err):
+			case asymkey_model.IsErrGPGInvalidTokenSignature(err):
 				loadKeysData(ctx)
 				ctx.Data["Err_Content"] = true
 				ctx.Data["Err_Signature"] = true
-				ctx.Data["KeyID"] = err.(models.ErrGPGInvalidTokenSignature).ID
+				ctx.Data["KeyID"] = err.(asymkey_model.ErrGPGInvalidTokenSignature).ID
 				ctx.RenderWithErr(ctx.Tr("settings.gpg_invalid_token_signature"), tplSettingsKeys, &form)
-			case models.IsErrGPGNoEmailFound(err):
+			case asymkey_model.IsErrGPGNoEmailFound(err):
 				loadKeysData(ctx)
 
 				ctx.Data["Err_Content"] = true
 				ctx.Data["Err_Signature"] = true
-				ctx.Data["KeyID"] = err.(models.ErrGPGNoEmailFound).ID
+				ctx.Data["KeyID"] = err.(asymkey_model.ErrGPGNoEmailFound).ID
 				ctx.RenderWithErr(ctx.Tr("settings.gpg_no_key_email_found"), tplSettingsKeys, &form)
 			default:
 				ctx.ServerError("AddPublicKey", err)
@@ -124,21 +125,21 @@ func KeysPost(ctx *context.Context) {
 		ctx.Flash.Success(ctx.Tr("settings.add_gpg_key_success", keyIDs))
 		ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
 	case "verify_gpg":
-		token := models.VerificationToken(ctx.User, 1)
-		lastToken := models.VerificationToken(ctx.User, 0)
+		token := asymkey_model.VerificationToken(ctx.User, 1)
+		lastToken := asymkey_model.VerificationToken(ctx.User, 0)
 
-		keyID, err := models.VerifyGPGKey(ctx.User.ID, form.KeyID, token, form.Signature)
-		if err != nil && models.IsErrGPGInvalidTokenSignature(err) {
-			keyID, err = models.VerifyGPGKey(ctx.User.ID, form.KeyID, lastToken, form.Signature)
+		keyID, err := asymkey_model.VerifyGPGKey(ctx.User.ID, form.KeyID, token, form.Signature)
+		if err != nil && asymkey_model.IsErrGPGInvalidTokenSignature(err) {
+			keyID, err = asymkey_model.VerifyGPGKey(ctx.User.ID, form.KeyID, lastToken, form.Signature)
 		}
 		if err != nil {
 			ctx.Data["HasGPGVerifyError"] = true
 			switch {
-			case models.IsErrGPGInvalidTokenSignature(err):
+			case asymkey_model.IsErrGPGInvalidTokenSignature(err):
 				loadKeysData(ctx)
 				ctx.Data["VerifyingID"] = form.KeyID
 				ctx.Data["Err_Signature"] = true
-				ctx.Data["KeyID"] = err.(models.ErrGPGInvalidTokenSignature).ID
+				ctx.Data["KeyID"] = err.(asymkey_model.ErrGPGInvalidTokenSignature).ID
 				ctx.RenderWithErr(ctx.Tr("settings.gpg_invalid_token_signature"), tplSettingsKeys, &form)
 			default:
 				ctx.ServerError("VerifyGPG", err)
@@ -147,11 +148,11 @@ func KeysPost(ctx *context.Context) {
 		ctx.Flash.Success(ctx.Tr("settings.verify_gpg_key_success", keyID))
 		ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
 	case "ssh":
-		content, err := models.CheckPublicKeyString(form.Content)
+		content, err := asymkey_model.CheckPublicKeyString(form.Content)
 		if err != nil {
-			if models.IsErrSSHDisabled(err) {
+			if db.IsErrSSHDisabled(err) {
 				ctx.Flash.Info(ctx.Tr("settings.ssh_disabled"))
-			} else if models.IsErrKeyUnableVerify(err) {
+			} else if asymkey_model.IsErrKeyUnableVerify(err) {
 				ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key"))
 			} else {
 				ctx.Flash.Error(ctx.Tr("form.invalid_ssh_key", err.Error()))
@@ -160,20 +161,20 @@ func KeysPost(ctx *context.Context) {
 			return
 		}
 
-		if _, err = models.AddPublicKey(ctx.User.ID, form.Title, content, 0); err != nil {
+		if _, err = asymkey_model.AddPublicKey(ctx.User.ID, form.Title, content, 0); err != nil {
 			ctx.Data["HasSSHError"] = true
 			switch {
-			case models.IsErrKeyAlreadyExist(err):
+			case asymkey_model.IsErrKeyAlreadyExist(err):
 				loadKeysData(ctx)
 
 				ctx.Data["Err_Content"] = true
 				ctx.RenderWithErr(ctx.Tr("settings.ssh_key_been_used"), tplSettingsKeys, &form)
-			case models.IsErrKeyNameAlreadyUsed(err):
+			case asymkey_model.IsErrKeyNameAlreadyUsed(err):
 				loadKeysData(ctx)
 
 				ctx.Data["Err_Title"] = true
 				ctx.RenderWithErr(ctx.Tr("settings.ssh_key_name_used"), tplSettingsKeys, &form)
-			case models.IsErrKeyUnableVerify(err):
+			case asymkey_model.IsErrKeyUnableVerify(err):
 				ctx.Flash.Info(ctx.Tr("form.unable_verify_ssh_key"))
 				ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
 			default:
@@ -196,14 +197,14 @@ func DeleteKey(ctx *context.Context) {
 
 	switch ctx.FormString("type") {
 	case "gpg":
-		if err := models.DeleteGPGKey(ctx.User, ctx.FormInt64("id")); err != nil {
+		if err := asymkey_model.DeleteGPGKey(ctx.User, ctx.FormInt64("id")); err != nil {
 			ctx.Flash.Error("DeleteGPGKey: " + err.Error())
 		} else {
 			ctx.Flash.Success(ctx.Tr("settings.gpg_key_deletion_success"))
 		}
 	case "ssh":
 		keyID := ctx.FormInt64("id")
-		external, err := models.PublicKeyIsExternallyManaged(keyID)
+		external, err := asymkey_model.PublicKeyIsExternallyManaged(keyID)
 		if err != nil {
 			ctx.ServerError("sshKeysExternalManaged", err)
 			return
@@ -213,13 +214,13 @@ func DeleteKey(ctx *context.Context) {
 			ctx.Redirect(setting.AppSubURL + "/user/settings/keys")
 			return
 		}
-		if err := models.DeletePublicKey(ctx.User, keyID); err != nil {
+		if err := asymkey_service.DeletePublicKey(ctx.User, keyID); err != nil {
 			ctx.Flash.Error("DeletePublicKey: " + err.Error())
 		} else {
 			ctx.Flash.Success(ctx.Tr("settings.ssh_key_deletion_success"))
 		}
 	case "principal":
-		if err := models.DeletePublicKey(ctx.User, ctx.FormInt64("id")); err != nil {
+		if err := asymkey_service.DeletePublicKey(ctx.User, ctx.FormInt64("id")); err != nil {
 			ctx.Flash.Error("DeletePublicKey: " + err.Error())
 		} else {
 			ctx.Flash.Success(ctx.Tr("settings.ssh_principal_deletion_success"))
@@ -234,32 +235,32 @@ func DeleteKey(ctx *context.Context) {
 }
 
 func loadKeysData(ctx *context.Context) {
-	keys, err := models.ListPublicKeys(ctx.User.ID, db.ListOptions{})
+	keys, err := asymkey_model.ListPublicKeys(ctx.User.ID, db.ListOptions{})
 	if err != nil {
 		ctx.ServerError("ListPublicKeys", err)
 		return
 	}
 	ctx.Data["Keys"] = keys
 
-	externalKeys, err := models.PublicKeysAreExternallyManaged(keys)
+	externalKeys, err := asymkey_model.PublicKeysAreExternallyManaged(keys)
 	if err != nil {
 		ctx.ServerError("ListPublicKeys", err)
 		return
 	}
 	ctx.Data["ExternalKeys"] = externalKeys
 
-	gpgkeys, err := models.ListGPGKeys(ctx.User.ID, db.ListOptions{})
+	gpgkeys, err := asymkey_model.ListGPGKeys(db.DefaultContext, ctx.User.ID, db.ListOptions{})
 	if err != nil {
 		ctx.ServerError("ListGPGKeys", err)
 		return
 	}
 	ctx.Data["GPGKeys"] = gpgkeys
-	tokenToSign := models.VerificationToken(ctx.User, 1)
+	tokenToSign := asymkey_model.VerificationToken(ctx.User, 1)
 
 	// generate a new aes cipher using the csrfToken
 	ctx.Data["TokenToSign"] = tokenToSign
 
-	principals, err := models.ListPrincipalKeys(ctx.User.ID, db.ListOptions{})
+	principals, err := asymkey_model.ListPrincipalKeys(ctx.User.ID, db.ListOptions{})
 	if err != nil {
 		ctx.ServerError("ListPrincipalKeys", err)
 		return
diff --git a/services/asymkey/deploy_key.go b/services/asymkey/deploy_key.go
new file mode 100644
index 0000000000..aa0925ab13
--- /dev/null
+++ b/services/asymkey/deploy_key.go
@@ -0,0 +1,30 @@
+// Copyright 2021 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 asymkey
+
+import (
+	"code.gitea.io/gitea/models"
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
+	"code.gitea.io/gitea/models/db"
+	user_model "code.gitea.io/gitea/models/user"
+)
+
+// DeleteDeployKey deletes deploy key from its repository authorized_keys file if needed.
+func DeleteDeployKey(doer *user_model.User, id int64) error {
+	ctx, committer, err := db.TxContext()
+	if err != nil {
+		return err
+	}
+	defer committer.Close()
+
+	if err := models.DeleteDeployKey(ctx, doer, id); err != nil {
+		return err
+	}
+	if err := committer.Commit(); err != nil {
+		return err
+	}
+
+	return asymkey_model.RewriteAllPublicKeys()
+}
diff --git a/services/asymkey/main_test.go b/services/asymkey/main_test.go
new file mode 100644
index 0000000000..a891a10cf6
--- /dev/null
+++ b/services/asymkey/main_test.go
@@ -0,0 +1,16 @@
+// 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 asymkey
+
+import (
+	"path/filepath"
+	"testing"
+
+	"code.gitea.io/gitea/models/unittest"
+)
+
+func TestMain(m *testing.M) {
+	unittest.MainTest(m, filepath.Join("..", ".."))
+}
diff --git a/models/repo_sign.go b/services/asymkey/sign.go
similarity index 57%
rename from models/repo_sign.go
rename to services/asymkey/sign.go
index 1c736a62da..4ccc581803 100644
--- a/models/repo_sign.go
+++ b/services/asymkey/sign.go
@@ -1,15 +1,17 @@
-// Copyright 2019 The Gitea Authors. All rights reserved.
+// Copyright 2021 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
+package asymkey
 
 import (
+	"fmt"
 	"strings"
 
+	"code.gitea.io/gitea/models"
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/login"
-	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
@@ -63,6 +65,22 @@ func signingModeFromStrings(modeStrings []string) []signingMode {
 	return returnable
 }
 
+// ErrWontSign explains the first reason why a commit would not be signed
+// There may be other reasons - this is just the first reason found
+type ErrWontSign struct {
+	Reason signingMode
+}
+
+func (e *ErrWontSign) Error() string {
+	return fmt.Sprintf("wont sign: %s", e.Reason)
+}
+
+// IsErrWontSign checks if an error is a ErrWontSign
+func IsErrWontSign(err error) bool {
+	_, ok := err.(*ErrWontSign)
+	return ok
+}
+
 // SigningKey returns the KeyID and git Signature for the repo
 func SigningKey(repoPath string) (string, *git.Signature) {
 	if setting.Repository.Signing.SigningKey == "none" {
@@ -124,7 +142,7 @@ Loop:
 		case always:
 			break Loop
 		case pubkey:
-			keys, err := ListGPGKeys(u.ID, db.ListOptions{})
+			keys, err := asymkey_model.ListGPGKeys(db.DefaultContext, u.ID, db.ListOptions{})
 			if err != nil {
 				return false, "", nil, err
 			}
@@ -145,9 +163,9 @@ Loop:
 }
 
 // SignWikiCommit determines if we should sign the commits to this repository wiki
-func SignWikiCommit(repo *repo_model.Repository, u *user_model.User) (bool, string, *git.Signature, error) {
+func SignWikiCommit(repoWikiPath string, u *user_model.User) (bool, string, *git.Signature, error) {
 	rules := signingModeFromStrings(setting.Repository.Signing.Wiki)
-	signingKey, sig := SigningKey(repo.WikiPath())
+	signingKey, sig := SigningKey(repoWikiPath)
 	if signingKey == "" {
 		return false, "", nil, &ErrWontSign{noKey}
 	}
@@ -160,7 +178,7 @@ Loop:
 		case always:
 			break Loop
 		case pubkey:
-			keys, err := ListGPGKeys(u.ID, db.ListOptions{})
+			keys, err := asymkey_model.ListGPGKeys(db.DefaultContext, u.ID, db.ListOptions{})
 			if err != nil {
 				return false, "", nil, err
 			}
@@ -176,7 +194,7 @@ Loop:
 				return false, "", nil, &ErrWontSign{twofa}
 			}
 		case parentSigned:
-			gitRepo, err := git.OpenRepository(repo.WikiPath())
+			gitRepo, err := git.OpenRepository(repoWikiPath)
 			if err != nil {
 				return false, "", nil, err
 			}
@@ -188,7 +206,7 @@ Loop:
 			if commit.Signature == nil {
 				return false, "", nil, &ErrWontSign{parentSigned}
 			}
-			verification := ParseCommitWithSignature(commit)
+			verification := asymkey_model.ParseCommitWithSignature(commit)
 			if !verification.Verified {
 				return false, "", nil, &ErrWontSign{parentSigned}
 			}
@@ -198,9 +216,9 @@ Loop:
 }
 
 // SignCRUDAction determines if we should sign a CRUD commit to this repository
-func SignCRUDAction(repo *repo_model.Repository, u *user_model.User, tmpBasePath, parentCommit string) (bool, string, *git.Signature, error) {
+func SignCRUDAction(repoPath string, u *user_model.User, tmpBasePath, parentCommit string) (bool, string, *git.Signature, error) {
 	rules := signingModeFromStrings(setting.Repository.Signing.CRUDActions)
-	signingKey, sig := SigningKey(repo.RepoPath())
+	signingKey, sig := SigningKey(repoPath)
 	if signingKey == "" {
 		return false, "", nil, &ErrWontSign{noKey}
 	}
@@ -213,7 +231,7 @@ Loop:
 		case always:
 			break Loop
 		case pubkey:
-			keys, err := ListGPGKeys(u.ID, db.ListOptions{})
+			keys, err := asymkey_model.ListGPGKeys(db.DefaultContext, u.ID, db.ListOptions{})
 			if err != nil {
 				return false, "", nil, err
 			}
@@ -241,7 +259,7 @@ Loop:
 			if commit.Signature == nil {
 				return false, "", nil, &ErrWontSign{parentSigned}
 			}
-			verification := ParseCommitWithSignature(commit)
+			verification := asymkey_model.ParseCommitWithSignature(commit)
 			if !verification.Verified {
 				return false, "", nil, &ErrWontSign{parentSigned}
 			}
@@ -249,3 +267,122 @@ Loop:
 	}
 	return true, signingKey, sig, nil
 }
+
+// SignMerge determines if we should sign a PR merge commit to the base repository
+func SignMerge(pr *models.PullRequest, u *user_model.User, tmpBasePath, baseCommit, headCommit string) (bool, string, *git.Signature, error) {
+	if err := pr.LoadBaseRepo(); err != nil {
+		log.Error("Unable to get Base Repo for pull request")
+		return false, "", nil, err
+	}
+	repo := pr.BaseRepo
+
+	signingKey, signer := SigningKey(repo.RepoPath())
+	if signingKey == "" {
+		return false, "", nil, &ErrWontSign{noKey}
+	}
+	rules := signingModeFromStrings(setting.Repository.Signing.Merges)
+
+	var gitRepo *git.Repository
+	var err error
+
+Loop:
+	for _, rule := range rules {
+		switch rule {
+		case never:
+			return false, "", nil, &ErrWontSign{never}
+		case always:
+			break Loop
+		case pubkey:
+			keys, err := asymkey_model.ListGPGKeys(db.DefaultContext, u.ID, db.ListOptions{})
+			if err != nil {
+				return false, "", nil, err
+			}
+			if len(keys) == 0 {
+				return false, "", nil, &ErrWontSign{pubkey}
+			}
+		case twofa:
+			twofaModel, err := login.GetTwoFactorByUID(u.ID)
+			if err != nil && !login.IsErrTwoFactorNotEnrolled(err) {
+				return false, "", nil, err
+			}
+			if twofaModel == nil {
+				return false, "", nil, &ErrWontSign{twofa}
+			}
+		case approved:
+			protectedBranch, err := models.GetProtectedBranchBy(repo.ID, pr.BaseBranch)
+			if err != nil {
+				return false, "", nil, err
+			}
+			if protectedBranch == nil {
+				return false, "", nil, &ErrWontSign{approved}
+			}
+			if protectedBranch.GetGrantedApprovalsCount(pr) < 1 {
+				return false, "", nil, &ErrWontSign{approved}
+			}
+		case baseSigned:
+			if gitRepo == nil {
+				gitRepo, err = git.OpenRepository(tmpBasePath)
+				if err != nil {
+					return false, "", nil, err
+				}
+				defer gitRepo.Close()
+			}
+			commit, err := gitRepo.GetCommit(baseCommit)
+			if err != nil {
+				return false, "", nil, err
+			}
+			verification := asymkey_model.ParseCommitWithSignature(commit)
+			if !verification.Verified {
+				return false, "", nil, &ErrWontSign{baseSigned}
+			}
+		case headSigned:
+			if gitRepo == nil {
+				gitRepo, err = git.OpenRepository(tmpBasePath)
+				if err != nil {
+					return false, "", nil, err
+				}
+				defer gitRepo.Close()
+			}
+			commit, err := gitRepo.GetCommit(headCommit)
+			if err != nil {
+				return false, "", nil, err
+			}
+			verification := asymkey_model.ParseCommitWithSignature(commit)
+			if !verification.Verified {
+				return false, "", nil, &ErrWontSign{headSigned}
+			}
+		case commitsSigned:
+			if gitRepo == nil {
+				gitRepo, err = git.OpenRepository(tmpBasePath)
+				if err != nil {
+					return false, "", nil, err
+				}
+				defer gitRepo.Close()
+			}
+			commit, err := gitRepo.GetCommit(headCommit)
+			if err != nil {
+				return false, "", nil, err
+			}
+			verification := asymkey_model.ParseCommitWithSignature(commit)
+			if !verification.Verified {
+				return false, "", nil, &ErrWontSign{commitsSigned}
+			}
+			// need to work out merge-base
+			mergeBaseCommit, _, err := gitRepo.GetMergeBase("", baseCommit, headCommit)
+			if err != nil {
+				return false, "", nil, err
+			}
+			commitList, err := commit.CommitsBeforeUntil(mergeBaseCommit)
+			if err != nil {
+				return false, "", nil, err
+			}
+			for _, commit := range commitList {
+				verification := asymkey_model.ParseCommitWithSignature(commit)
+				if !verification.Verified {
+					return false, "", nil, &ErrWontSign{commitsSigned}
+				}
+			}
+		}
+	}
+	return true, signingKey, signer, nil
+}
diff --git a/services/asymkey/ssh_key.go b/services/asymkey/ssh_key.go
new file mode 100644
index 0000000000..1f6b93eb24
--- /dev/null
+++ b/services/asymkey/ssh_key.go
@@ -0,0 +1,49 @@
+// Copyright 2021 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 asymkey
+
+import (
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
+	"code.gitea.io/gitea/models/db"
+	user_model "code.gitea.io/gitea/models/user"
+)
+
+// DeletePublicKey deletes SSH key information both in database and authorized_keys file.
+func DeletePublicKey(doer *user_model.User, id int64) (err error) {
+	key, err := asymkey_model.GetPublicKeyByID(id)
+	if err != nil {
+		return err
+	}
+
+	// Check if user has access to delete this key.
+	if !doer.IsAdmin && doer.ID != key.OwnerID {
+		return asymkey_model.ErrKeyAccessDenied{
+			UserID: doer.ID,
+			KeyID:  key.ID,
+			Note:   "public",
+		}
+	}
+
+	ctx, committer, err := db.TxContext()
+	if err != nil {
+		return err
+	}
+	defer committer.Close()
+
+	if err = asymkey_model.DeletePublicKeys(ctx, id); err != nil {
+		return err
+	}
+
+	if err = committer.Commit(); err != nil {
+		return err
+	}
+	committer.Close()
+
+	if key.Type == asymkey_model.KeyTypePrincipal {
+		return asymkey_model.RewriteAllPrincipalKeys()
+	}
+
+	return asymkey_model.RewriteAllPublicKeys()
+}
diff --git a/services/asymkey/ssh_key_test.go b/services/asymkey/ssh_key_test.go
new file mode 100644
index 0000000000..0ce235f7f6
--- /dev/null
+++ b/services/asymkey/ssh_key_test.go
@@ -0,0 +1,83 @@
+// Copyright 2021 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 asymkey
+
+import (
+	"testing"
+
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
+	"code.gitea.io/gitea/models/login"
+	"code.gitea.io/gitea/models/unittest"
+	user_model "code.gitea.io/gitea/models/user"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestAddLdapSSHPublicKeys(t *testing.T) {
+	assert.NoError(t, unittest.PrepareTestDatabase())
+
+	user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).(*user_model.User)
+	s := &login.Source{ID: 1}
+
+	testCases := []struct {
+		keyString   string
+		number      int
+		keyContents []string
+	}{
+		{
+			keyString: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM= nocomment\n",
+			number:    1,
+			keyContents: []string{
+				"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM=",
+			},
+		},
+		{
+			keyString: `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM= nocomment
+ssh-dss AAAAB3NzaC1kc3MAAACBAOChCC7lf6Uo9n7BmZ6M8St19PZf4Tn59NriyboW2x/DZuYAz3ibZ2OkQ3S0SqDIa0HXSEJ1zaExQdmbO+Ux/wsytWZmCczWOVsaszBZSl90q8UnWlSH6P+/YA+RWJm5SFtuV9PtGIhyZgoNuz5kBQ7K139wuQsecdKktISwTakzAAAAFQCzKsO2JhNKlL+wwwLGOcLffoAmkwAAAIBpK7/3xvduajLBD/9vASqBQIHrgK2J+wiQnIb/Wzy0UsVmvfn8A+udRbBo+csM8xrSnlnlJnjkJS3qiM5g+eTwsLIV1IdKPEwmwB+VcP53Cw6lSyWyJcvhFb0N6s08NZysLzvj0N+ZC/FnhKTLzIyMtkHf/IrPCwlM+pV/M/96YgAAAIEAqQcGn9CKgzgPaguIZooTAOQdvBLMI5y0bQjOW6734XOpqQGf/Kra90wpoasLKZjSYKNPjE+FRUOrStLrxcNs4BeVKhy2PYTRnybfYVk1/dmKgH6P1YSRONsGKvTsH6c5IyCRG0ncCgYeF8tXppyd642982daopE7zQ/NPAnJfag= nocomment`,
+			number: 2,
+			keyContents: []string{
+				"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM=",
+				"ssh-dss AAAAB3NzaC1kc3MAAACBAOChCC7lf6Uo9n7BmZ6M8St19PZf4Tn59NriyboW2x/DZuYAz3ibZ2OkQ3S0SqDIa0HXSEJ1zaExQdmbO+Ux/wsytWZmCczWOVsaszBZSl90q8UnWlSH6P+/YA+RWJm5SFtuV9PtGIhyZgoNuz5kBQ7K139wuQsecdKktISwTakzAAAAFQCzKsO2JhNKlL+wwwLGOcLffoAmkwAAAIBpK7/3xvduajLBD/9vASqBQIHrgK2J+wiQnIb/Wzy0UsVmvfn8A+udRbBo+csM8xrSnlnlJnjkJS3qiM5g+eTwsLIV1IdKPEwmwB+VcP53Cw6lSyWyJcvhFb0N6s08NZysLzvj0N+ZC/FnhKTLzIyMtkHf/IrPCwlM+pV/M/96YgAAAIEAqQcGn9CKgzgPaguIZooTAOQdvBLMI5y0bQjOW6734XOpqQGf/Kra90wpoasLKZjSYKNPjE+FRUOrStLrxcNs4BeVKhy2PYTRnybfYVk1/dmKgH6P1YSRONsGKvTsH6c5IyCRG0ncCgYeF8tXppyd642982daopE7zQ/NPAnJfag=",
+			},
+		},
+		{
+			keyString: `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM= nocomment
+# comment asmdna,ndp
+ssh-dss AAAAB3NzaC1kc3MAAACBAOChCC7lf6Uo9n7BmZ6M8St19PZf4Tn59NriyboW2x/DZuYAz3ibZ2OkQ3S0SqDIa0HXSEJ1zaExQdmbO+Ux/wsytWZmCczWOVsaszBZSl90q8UnWlSH6P+/YA+RWJm5SFtuV9PtGIhyZgoNuz5kBQ7K139wuQsecdKktISwTakzAAAAFQCzKsO2JhNKlL+wwwLGOcLffoAmkwAAAIBpK7/3xvduajLBD/9vASqBQIHrgK2J+wiQnIb/Wzy0UsVmvfn8A+udRbBo+csM8xrSnlnlJnjkJS3qiM5g+eTwsLIV1IdKPEwmwB+VcP53Cw6lSyWyJcvhFb0N6s08NZysLzvj0N+ZC/FnhKTLzIyMtkHf/IrPCwlM+pV/M/96YgAAAIEAqQcGn9CKgzgPaguIZooTAOQdvBLMI5y0bQjOW6734XOpqQGf/Kra90wpoasLKZjSYKNPjE+FRUOrStLrxcNs4BeVKhy2PYTRnybfYVk1/dmKgH6P1YSRONsGKvTsH6c5IyCRG0ncCgYeF8tXppyd642982daopE7zQ/NPAnJfag= nocomment`,
+			number: 2,
+			keyContents: []string{
+				"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM=",
+				"ssh-dss AAAAB3NzaC1kc3MAAACBAOChCC7lf6Uo9n7BmZ6M8St19PZf4Tn59NriyboW2x/DZuYAz3ibZ2OkQ3S0SqDIa0HXSEJ1zaExQdmbO+Ux/wsytWZmCczWOVsaszBZSl90q8UnWlSH6P+/YA+RWJm5SFtuV9PtGIhyZgoNuz5kBQ7K139wuQsecdKktISwTakzAAAAFQCzKsO2JhNKlL+wwwLGOcLffoAmkwAAAIBpK7/3xvduajLBD/9vASqBQIHrgK2J+wiQnIb/Wzy0UsVmvfn8A+udRbBo+csM8xrSnlnlJnjkJS3qiM5g+eTwsLIV1IdKPEwmwB+VcP53Cw6lSyWyJcvhFb0N6s08NZysLzvj0N+ZC/FnhKTLzIyMtkHf/IrPCwlM+pV/M/96YgAAAIEAqQcGn9CKgzgPaguIZooTAOQdvBLMI5y0bQjOW6734XOpqQGf/Kra90wpoasLKZjSYKNPjE+FRUOrStLrxcNs4BeVKhy2PYTRnybfYVk1/dmKgH6P1YSRONsGKvTsH6c5IyCRG0ncCgYeF8tXppyd642982daopE7zQ/NPAnJfag=",
+			},
+		},
+		{
+			keyString: `ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM= nocomment
+382488320jasdj1lasmva/vasodifipi4193-fksma.cm
+ssh-dss AAAAB3NzaC1kc3MAAACBAOChCC7lf6Uo9n7BmZ6M8St19PZf4Tn59NriyboW2x/DZuYAz3ibZ2OkQ3S0SqDIa0HXSEJ1zaExQdmbO+Ux/wsytWZmCczWOVsaszBZSl90q8UnWlSH6P+/YA+RWJm5SFtuV9PtGIhyZgoNuz5kBQ7K139wuQsecdKktISwTakzAAAAFQCzKsO2JhNKlL+wwwLGOcLffoAmkwAAAIBpK7/3xvduajLBD/9vASqBQIHrgK2J+wiQnIb/Wzy0UsVmvfn8A+udRbBo+csM8xrSnlnlJnjkJS3qiM5g+eTwsLIV1IdKPEwmwB+VcP53Cw6lSyWyJcvhFb0N6s08NZysLzvj0N+ZC/FnhKTLzIyMtkHf/IrPCwlM+pV/M/96YgAAAIEAqQcGn9CKgzgPaguIZooTAOQdvBLMI5y0bQjOW6734XOpqQGf/Kra90wpoasLKZjSYKNPjE+FRUOrStLrxcNs4BeVKhy2PYTRnybfYVk1/dmKgH6P1YSRONsGKvTsH6c5IyCRG0ncCgYeF8tXppyd642982daopE7zQ/NPAnJfag= nocomment`,
+			number: 2,
+			keyContents: []string{
+				"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC4cn+iXnA4KvcQYSV88vGn0Yi91vG47t1P7okprVmhNTkipNRIHWr6WdCO4VDr/cvsRkuVJAsLO2enwjGWWueOO6BodiBgyAOZ/5t5nJNMCNuLGT5UIo/RI1b0WRQwxEZTRjt6mFNw6lH14wRd8ulsr9toSWBPMOGWoYs1PDeDL0JuTjL+tr1SZi/EyxCngpYszKdXllJEHyI79KQgeD0Vt3pTrkbNVTOEcCNqZePSVmUH8X8Vhugz3bnE0/iE9Pb5fkWO9c4AnM1FgI/8Bvp27Fw2ShryIXuR6kKvUqhVMTuOSDHwu6A8jLE5Owt3GAYugDpDYuwTVNGrHLXKpPzrGGPE/jPmaLCMZcsdkec95dYeU3zKODEm8UQZFhmJmDeWVJ36nGrGZHL4J5aTTaeFUJmmXDaJYiJ+K2/ioKgXqnXvltu0A9R8/LGy4nrTJRr4JMLuJFoUXvGm1gXQ70w2LSpk6yl71RNC0hCtsBe8BP8IhYCM0EP5jh7eCMQZNvM=",
+				"ssh-dss AAAAB3NzaC1kc3MAAACBAOChCC7lf6Uo9n7BmZ6M8St19PZf4Tn59NriyboW2x/DZuYAz3ibZ2OkQ3S0SqDIa0HXSEJ1zaExQdmbO+Ux/wsytWZmCczWOVsaszBZSl90q8UnWlSH6P+/YA+RWJm5SFtuV9PtGIhyZgoNuz5kBQ7K139wuQsecdKktISwTakzAAAAFQCzKsO2JhNKlL+wwwLGOcLffoAmkwAAAIBpK7/3xvduajLBD/9vASqBQIHrgK2J+wiQnIb/Wzy0UsVmvfn8A+udRbBo+csM8xrSnlnlJnjkJS3qiM5g+eTwsLIV1IdKPEwmwB+VcP53Cw6lSyWyJcvhFb0N6s08NZysLzvj0N+ZC/FnhKTLzIyMtkHf/IrPCwlM+pV/M/96YgAAAIEAqQcGn9CKgzgPaguIZooTAOQdvBLMI5y0bQjOW6734XOpqQGf/Kra90wpoasLKZjSYKNPjE+FRUOrStLrxcNs4BeVKhy2PYTRnybfYVk1/dmKgH6P1YSRONsGKvTsH6c5IyCRG0ncCgYeF8tXppyd642982daopE7zQ/NPAnJfag=",
+			},
+		},
+	}
+
+	for i, kase := range testCases {
+		s.ID = int64(i) + 20
+		asymkey_model.AddPublicKeysBySource(user, s, []string{kase.keyString})
+		keys, err := asymkey_model.ListPublicKeysBySource(user.ID, s.ID)
+		assert.NoError(t, err)
+		if err != nil {
+			continue
+		}
+		assert.Len(t, keys, kase.number)
+
+		for _, key := range keys {
+			assert.Contains(t, kase.keyContents, key.Content)
+		}
+		for _, key := range keys {
+			DeletePublicKey(user, key.ID)
+		}
+	}
+}
diff --git a/services/auth/source/ldap/source_authenticate.go b/services/auth/source/ldap/source_authenticate.go
index 9bb07d244f..9938525c0e 100644
--- a/services/auth/source/ldap/source_authenticate.go
+++ b/services/auth/source/ldap/source_authenticate.go
@@ -8,7 +8,7 @@ import (
 	"fmt"
 	"strings"
 
-	"code.gitea.io/gitea/models"
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/login"
 	user_model "code.gitea.io/gitea/models/user"
@@ -59,8 +59,8 @@ func (source *Source) Authenticate(user *user_model.User, userName, password str
 	}
 
 	if user != nil {
-		if isAttributeSSHPublicKeySet && models.SynchronizePublicKeys(user, source.loginSource, sr.SSHPublicKey) {
-			return user, models.RewriteAllPublicKeys()
+		if isAttributeSSHPublicKeySet && asymkey_model.SynchronizePublicKeys(user, source.loginSource, sr.SSHPublicKey) {
+			return user, asymkey_model.RewriteAllPublicKeys()
 		}
 
 		return user, nil
@@ -95,8 +95,8 @@ func (source *Source) Authenticate(user *user_model.User, userName, password str
 
 	mailer.SendRegisterNotifyMail(user)
 
-	if isAttributeSSHPublicKeySet && models.AddPublicKeysBySource(user, source.loginSource, sr.SSHPublicKey) {
-		err = models.RewriteAllPublicKeys()
+	if isAttributeSSHPublicKeySet && asymkey_model.AddPublicKeysBySource(user, source.loginSource, sr.SSHPublicKey) {
+		err = asymkey_model.RewriteAllPublicKeys()
 	}
 
 	if err == nil && len(source.AttributeAvatar) > 0 {
diff --git a/services/auth/source/ldap/source_sync.go b/services/auth/source/ldap/source_sync.go
index 9c504476c0..fb15b2f046 100644
--- a/services/auth/source/ldap/source_sync.go
+++ b/services/auth/source/ldap/source_sync.go
@@ -10,7 +10,7 @@ import (
 	"sort"
 	"strings"
 
-	"code.gitea.io/gitea/models"
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/db"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/log"
@@ -68,7 +68,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
 			log.Warn("SyncExternalUsers: Cancelled at update of %s before completed update of users", source.loginSource.Name)
 			// Rewrite authorized_keys file if LDAP Public SSH Key attribute is set and any key was added or removed
 			if sshKeysNeedUpdate {
-				err = models.RewriteAllPublicKeys()
+				err = asymkey_model.RewriteAllPublicKeys()
 				if err != nil {
 					log.Error("RewriteAllPublicKeys: %v", err)
 				}
@@ -119,7 +119,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
 
 			if err == nil && isAttributeSSHPublicKeySet {
 				log.Trace("SyncExternalUsers[%s]: Adding LDAP Public SSH Keys for user %s", source.loginSource.Name, usr.Name)
-				if models.AddPublicKeysBySource(usr, source.loginSource, su.SSHPublicKey) {
+				if asymkey_model.AddPublicKeysBySource(usr, source.loginSource, su.SSHPublicKey) {
 					sshKeysNeedUpdate = true
 				}
 			}
@@ -129,7 +129,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
 			}
 		} else if updateExisting {
 			// Synchronize SSH Public Key if that attribute is set
-			if isAttributeSSHPublicKeySet && models.SynchronizePublicKeys(usr, source.loginSource, su.SSHPublicKey) {
+			if isAttributeSSHPublicKeySet && asymkey_model.SynchronizePublicKeys(usr, source.loginSource, su.SSHPublicKey) {
 				sshKeysNeedUpdate = true
 			}
 
@@ -171,7 +171,7 @@ func (source *Source) Sync(ctx context.Context, updateExisting bool) error {
 
 	// Rewrite authorized_keys file if LDAP Public SSH Key attribute is set and any key was added or removed
 	if sshKeysNeedUpdate {
-		err = models.RewriteAllPublicKeys()
+		err = asymkey_model.RewriteAllPublicKeys()
 		if err != nil {
 			log.Error("RewriteAllPublicKeys: %v", err)
 		}
diff --git a/services/cron/tasks_extended.go b/services/cron/tasks_extended.go
index bc6add090b..90b391474f 100644
--- a/services/cron/tasks_extended.go
+++ b/services/cron/tasks_extended.go
@@ -9,6 +9,7 @@ import (
 	"time"
 
 	"code.gitea.io/gitea/models"
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/updatechecker"
@@ -67,7 +68,7 @@ func registerRewriteAllPublicKeys() {
 		RunAtStart: false,
 		Schedule:   "@every 72h",
 	}, func(_ context.Context, _ *user_model.User, _ Config) error {
-		return models.RewriteAllPublicKeys()
+		return asymkey_model.RewriteAllPublicKeys()
 	})
 }
 
@@ -77,7 +78,7 @@ func registerRewriteAllPrincipalKeys() {
 		RunAtStart: false,
 		Schedule:   "@every 72h",
 	}, func(_ context.Context, _ *user_model.User, _ Config) error {
-		return models.RewriteAllPrincipalKeys()
+		return asymkey_model.RewriteAllPrincipalKeys()
 	})
 }
 
diff --git a/services/pull/merge.go b/services/pull/merge.go
index 49050d6bee..ab1f43a50a 100644
--- a/services/pull/merge.go
+++ b/services/pull/merge.go
@@ -27,6 +27,7 @@ import (
 	"code.gitea.io/gitea/modules/references"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/timeutil"
+	asymkey_service "code.gitea.io/gitea/services/asymkey"
 	issue_service "code.gitea.io/gitea/services/issue"
 )
 
@@ -218,7 +219,7 @@ func rawMerge(pr *models.PullRequest, doer *user_model.User, mergeStyle repo_mod
 	// Determine if we should sign
 	signArg := ""
 	if git.CheckGitVersionAtLeast("1.7.9") == nil {
-		sign, keyID, signer, _ := pr.SignMerge(doer, tmpBasePath, "HEAD", trackingBranch)
+		sign, keyID, signer, _ := asymkey_service.SignMerge(pr, doer, tmpBasePath, "HEAD", trackingBranch)
 		if sign {
 			signArg = "-S" + keyID
 			if pr.BaseRepo.GetTrustModel() == repo_model.CommitterTrustModel || pr.BaseRepo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
@@ -553,7 +554,7 @@ func IsSignedIfRequired(pr *models.PullRequest, doer *user_model.User) (bool, er
 		return true, nil
 	}
 
-	sign, _, _, err := pr.SignMerge(doer, pr.BaseRepo.RepoPath(), pr.BaseBranch, pr.GetGitRefName())
+	sign, _, _, err := asymkey_service.SignMerge(pr, doer, pr.BaseRepo.RepoPath(), pr.BaseBranch, pr.GetGitRefName())
 
 	return sign, err
 }
diff --git a/services/repository/archiver/archiver.go b/services/repository/archiver/archiver.go
index 7e886d79cb..fb40c3383d 100644
--- a/services/repository/archiver/archiver.go
+++ b/services/repository/archiver/archiver.go
@@ -14,7 +14,6 @@ import (
 	"strings"
 	"time"
 
-	"code.gitea.io/gitea/models"
 	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/modules/git"
@@ -171,7 +170,7 @@ func doArchive(r *ArchiveRequest) (*repo_model.RepoArchiver, error) {
 		rd.Close()
 	}()
 	var done = make(chan error)
-	repo, err := models.LoadArchiverRepo(archiver)
+	repo, err := repo_model.GetRepositoryByID(archiver.RepoID)
 	if err != nil {
 		return nil, fmt.Errorf("archiver.LoadRepo failed: %v", err)
 	}
diff --git a/services/repository/files/commit.go b/services/repository/files/commit.go
index 6bff1bb97f..4e391ff343 100644
--- a/services/repository/files/commit.go
+++ b/services/repository/files/commit.go
@@ -8,6 +8,7 @@ import (
 	"fmt"
 
 	"code.gitea.io/gitea/models"
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
 	"code.gitea.io/gitea/modules/git"
@@ -55,7 +56,7 @@ func CountDivergingCommits(repo *repo_model.Repository, branch string) (*git.Div
 // GetPayloadCommitVerification returns the verification information of a commit
 func GetPayloadCommitVerification(commit *git.Commit) *structs.PayloadCommitVerification {
 	verification := &structs.PayloadCommitVerification{}
-	commitVerification := models.ParseCommitWithSignature(commit)
+	commitVerification := asymkey_model.ParseCommitWithSignature(commit)
 	if commit.Signature != nil {
 		verification.Signature = commit.Signature.Signature
 		verification.Payload = commit.Signature.Payload
diff --git a/services/repository/files/temp_repo.go b/services/repository/files/temp_repo.go
index 815aa2c69f..52879dd3b0 100644
--- a/services/repository/files/temp_repo.go
+++ b/services/repository/files/temp_repo.go
@@ -20,6 +20,7 @@ import (
 	"code.gitea.io/gitea/modules/git"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
+	asymkey_service "code.gitea.io/gitea/services/asymkey"
 	"code.gitea.io/gitea/services/gitdiff"
 )
 
@@ -217,7 +218,7 @@ func (t *TemporaryUploadRepository) CommitTreeWithDate(author, committer *user_m
 
 	// Determine if we should sign
 	if git.CheckGitVersionAtLeast("1.7.9") == nil {
-		sign, keyID, signer, _ := models.SignCRUDAction(t.repo, author, t.basePath, "HEAD")
+		sign, keyID, signer, _ := asymkey_service.SignCRUDAction(t.repo.RepoPath(), author, t.basePath, "HEAD")
 		if sign {
 			args = append(args, "-S"+keyID)
 			if t.repo.GetTrustModel() == repo_model.CommitterTrustModel || t.repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
diff --git a/services/repository/files/update.go b/services/repository/files/update.go
index 9a069acbfc..4bafa62cc1 100644
--- a/services/repository/files/update.go
+++ b/services/repository/files/update.go
@@ -21,6 +21,7 @@ import (
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
 	"code.gitea.io/gitea/modules/util"
+	asymkey_service "code.gitea.io/gitea/services/asymkey"
 	repo_service "code.gitea.io/gitea/services/repository"
 
 	stdcharset "golang.org/x/net/html/charset"
@@ -458,9 +459,9 @@ func VerifyBranchProtection(repo *repo_model.Repository, doer *user_model.User,
 			}
 		}
 		if protectedBranch.RequireSignedCommits {
-			_, _, _, err := models.SignCRUDAction(repo, doer, repo.RepoPath(), branchName)
+			_, _, _, err := asymkey_service.SignCRUDAction(repo.RepoPath(), doer, repo.RepoPath(), branchName)
 			if err != nil {
-				if !models.IsErrWontSign(err) {
+				if !asymkey_service.IsErrWontSign(err) {
 					return err
 				}
 				return models.ErrUserCannotCommit{
diff --git a/services/repository/repository.go b/services/repository/repository.go
index 0b97148f66..17fab57e19 100644
--- a/services/repository/repository.go
+++ b/services/repository/repository.go
@@ -31,13 +31,15 @@ func CreateRepository(doer, owner *user_model.User, opts models.CreateRepoOption
 }
 
 // DeleteRepository deletes a repository for a user or organization.
-func DeleteRepository(doer *user_model.User, repo *repo_model.Repository) error {
+func DeleteRepository(doer *user_model.User, repo *repo_model.Repository, notify bool) error {
 	if err := pull_service.CloseRepoBranchesPulls(doer, repo); err != nil {
 		log.Error("CloseRepoBranchesPulls failed: %v", err)
 	}
 
-	// If the repo itself has webhooks, we need to trigger them before deleting it...
-	notification.NotifyDeleteRepository(doer, repo)
+	if notify {
+		// If the repo itself has webhooks, we need to trigger them before deleting it...
+		notification.NotifyDeleteRepository(doer, repo)
+	}
 
 	err := models.DeleteRepository(doer, repo.OwnerID, repo.ID)
 	return err
diff --git a/services/user/user.go b/services/user/user.go
index 72052f4ec9..21f1a74f62 100644
--- a/services/user/user.go
+++ b/services/user/user.go
@@ -14,6 +14,7 @@ import (
 
 	"code.gitea.io/gitea/models"
 	admin_model "code.gitea.io/gitea/models/admin"
+	asymkey_model "code.gitea.io/gitea/models/asymkey"
 	"code.gitea.io/gitea/models/db"
 	repo_model "code.gitea.io/gitea/models/repo"
 	user_model "code.gitea.io/gitea/models/user"
@@ -31,11 +32,11 @@ func DeleteUser(u *user_model.User) error {
 		return fmt.Errorf("%s is an organization not a user", u.Name)
 	}
 
-	ctx, commiter, err := db.TxContext()
+	ctx, committer, err := db.TxContext()
 	if err != nil {
 		return err
 	}
-	defer commiter.Close()
+	defer committer.Close()
 
 	// Note: A user owns any repository or belongs to any organization
 	//	cannot perform delete operation.
@@ -60,14 +61,15 @@ func DeleteUser(u *user_model.User) error {
 		return fmt.Errorf("DeleteUser: %v", err)
 	}
 
-	if err := commiter.Commit(); err != nil {
+	if err := committer.Commit(); err != nil {
 		return err
 	}
+	committer.Close()
 
-	if err = models.RewriteAllPublicKeys(); err != nil {
+	if err = asymkey_model.RewriteAllPublicKeys(); err != nil {
 		return err
 	}
-	if err = models.RewriteAllPrincipalKeys(); err != nil {
+	if err = asymkey_model.RewriteAllPrincipalKeys(); err != nil {
 		return err
 	}
 
diff --git a/services/wiki/wiki.go b/services/wiki/wiki.go
index 18e9b26d81..aa683dd44f 100644
--- a/services/wiki/wiki.go
+++ b/services/wiki/wiki.go
@@ -22,6 +22,7 @@ import (
 	repo_module "code.gitea.io/gitea/modules/repository"
 	"code.gitea.io/gitea/modules/sync"
 	"code.gitea.io/gitea/modules/util"
+	asymkey_service "code.gitea.io/gitea/services/asymkey"
 )
 
 var (
@@ -225,7 +226,7 @@ func updateWikiPage(doer *user_model.User, repo *repo_model.Repository, oldWikiN
 
 	committer := doer.NewGitSig()
 
-	sign, signingKey, signer, _ := models.SignWikiCommit(repo, doer)
+	sign, signingKey, signer, _ := asymkey_service.SignWikiCommit(repo.WikiPath(), doer)
 	if sign {
 		commitTreeOpts.KeyID = signingKey
 		if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
@@ -343,7 +344,7 @@ func DeleteWikiPage(doer *user_model.User, repo *repo_model.Repository, wikiName
 
 	committer := doer.NewGitSig()
 
-	sign, signingKey, signer, _ := models.SignWikiCommit(repo, doer)
+	sign, signingKey, signer, _ := asymkey_service.SignWikiCommit(repo.WikiPath(), doer)
 	if sign {
 		commitTreeOpts.KeyID = signingKey
 		if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {