From 14fe9010ae8aecc0bcd38059b4c71256524b5341 Mon Sep 17 00:00:00 2001
From: Antoine GIRARD <sapk@users.noreply.github.com>
Date: Wed, 22 Mar 2017 11:43:54 +0100
Subject: [PATCH] GPG commit validation (#1150)

* GPG commit validation

* Add translation

+ some little fix

* Move hash calc after retrieving of potential key + missing translation

* Add some little test
---
 models/gpg_key.go                             | 186 ++++++++++++++++++
 models/gpg_key_test.go                        | 116 +++++++++++
 options/locale/locale_en-US.ini               |  10 +
 public/css/index.css                          |  35 +++-
 public/less/_repository.less                  |  39 +++-
 routers/api/v1/convert/convert.go             |   7 +
 routers/repo/commit.go                        |   5 +
 templates/repo/commits_table.tmpl             |  19 +-
 templates/repo/diff/page.tmpl                 |  19 +-
 vendor/code.gitea.io/git/commit.go            |  20 ++
 vendor/code.gitea.io/git/repo_commit.go       |   6 +
 vendor/code.gitea.io/sdk/gitea/hook.go        |  21 +-
 vendor/code.gitea.io/sdk/gitea/user_gpgkey.go |   6 +
 vendor/vendor.json                            |  12 +-
 14 files changed, 480 insertions(+), 21 deletions(-)

diff --git a/models/gpg_key.go b/models/gpg_key.go
index e7aac5162a..1c9d17d0e2 100644
--- a/models/gpg_key.go
+++ b/models/gpg_key.go
@@ -6,13 +6,21 @@ package models
 
 import (
 	"bytes"
+	"container/list"
+	"crypto"
 	"encoding/base64"
 	"fmt"
+	"hash"
+	"io"
 	"strings"
 	"time"
 
+	"code.gitea.io/git"
+	"code.gitea.io/gitea/modules/log"
+
 	"github.com/go-xorm/xorm"
 	"golang.org/x/crypto/openpgp"
+	"golang.org/x/crypto/openpgp/armor"
 	"golang.org/x/crypto/openpgp/packet"
 )
 
@@ -274,3 +282,181 @@ func DeleteGPGKey(doer *User, id int64) (err error) {
 
 	return nil
 }
+
+// CommitVerification represents a commit validation of signature
+type CommitVerification struct {
+	Verified    bool
+	Reason      string
+	SigningUser *User
+	SigningKey  *GPGKey
+}
+
+// SignCommit represents a commit with validation of signature.
+type SignCommit struct {
+	Verification *CommitVerification
+	*UserCommit
+}
+
+func readerFromBase64(s string) (io.Reader, error) {
+	bs, err := base64.StdEncoding.DecodeString(s)
+	if err != nil {
+		return nil, err
+	}
+	return bytes.NewBuffer(bs), nil
+}
+
+func populateHash(hashFunc crypto.Hash, msg []byte) (hash.Hash, error) {
+	h := hashFunc.New()
+	if _, err := h.Write(msg); err != nil {
+		return nil, err
+	}
+	return h, nil
+}
+
+// readArmoredSign read an armored signature block with the given type. https://sourcegraph.com/github.com/golang/crypto/-/blob/openpgp/read.go#L24:6-24:17
+func readArmoredSign(r io.Reader) (body io.Reader, err error) {
+	block, err := armor.Decode(r)
+	if err != nil {
+		return
+	}
+	if block.Type != openpgp.SignatureType {
+		return nil, fmt.Errorf("expected '" + openpgp.SignatureType + "', got: " + block.Type)
+	}
+	return block.Body, nil
+}
+
+func extractSignature(s string) (*packet.Signature, error) {
+	r, err := readArmoredSign(strings.NewReader(s))
+	if err != nil {
+		return nil, fmt.Errorf("Failed to read signature armor")
+	}
+	p, err := packet.Read(r)
+	if err != nil {
+		return nil, fmt.Errorf("Failed to read signature packet")
+	}
+	sig, ok := p.(*packet.Signature)
+	if !ok {
+		return nil, fmt.Errorf("Packet is not a signature")
+	}
+	return sig, nil
+}
+
+func verifySign(s *packet.Signature, h hash.Hash, k *GPGKey) error {
+	//Check if key can sign
+	if !k.CanSign {
+		return fmt.Errorf("key can not sign")
+	}
+	//Decode key
+	b, err := readerFromBase64(k.Content)
+	if err != nil {
+		return err
+	}
+	//Read key
+	p, err := packet.Read(b)
+	if err != nil {
+		return err
+	}
+
+	//Check type
+	pkey, ok := p.(*packet.PublicKey)
+	if !ok {
+		return fmt.Errorf("key is not a public key")
+	}
+
+	return pkey.VerifySignature(h, s)
+}
+
+// ParseCommitWithSignature check if signature is good against keystore.
+func ParseCommitWithSignature(c *git.Commit) *CommitVerification {
+
+	if c.Signature != nil {
+
+		//Parsing signature
+		sig, err := extractSignature(c.Signature.Signature)
+		if err != nil { //Skipping failed to extract sign
+			log.Error(3, "SignatureRead err: %v", err)
+			return &CommitVerification{
+				Verified: false,
+				Reason:   "gpg.error.extract_sign",
+			}
+		}
+
+		//Find Committer account
+		committer, err := GetUserByEmail(c.Committer.Email)
+		if err != nil { //Skipping not user for commiter
+			log.Error(3, "NoCommitterAccount: %v", err)
+			return &CommitVerification{
+				Verified: false,
+				Reason:   "gpg.error.no_committer_account",
+			}
+		}
+
+		keys, err := ListGPGKeys(committer.ID)
+		if err != nil || len(keys) == 0 { //Skipping failed to get gpg keys of user
+			log.Error(3, "ListGPGKeys: %v", err)
+			return &CommitVerification{
+				Verified: false,
+				Reason:   "gpg.error.failed_retrieval_gpg_keys",
+			}
+		}
+
+		//Generating hash of commit
+		hash, err := populateHash(sig.Hash, []byte(c.Signature.Payload))
+		if err != nil { //Skipping ailed to generate hash
+			log.Error(3, "PopulateHash: %v", err)
+			return &CommitVerification{
+				Verified: false,
+				Reason:   "gpg.error.generate_hash",
+			}
+		}
+
+		for _, k := range keys {
+			//We get PK
+			if err := verifySign(sig, hash, k); err == nil {
+				return &CommitVerification{ //Everything is ok
+					Verified:    true,
+					Reason:      fmt.Sprintf("%s <%s> / %s", c.Committer.Name, c.Committer.Email, k.KeyID),
+					SigningUser: committer,
+					SigningKey:  k,
+				}
+			}
+			//And test also SubsKey
+			for _, sk := range k.SubsKey {
+				if err := verifySign(sig, hash, sk); err == nil {
+					return &CommitVerification{ //Everything is ok
+						Verified:    true,
+						Reason:      fmt.Sprintf("%s <%s> / %s", c.Committer.Name, c.Committer.Email, sk.KeyID),
+						SigningUser: committer,
+						SigningKey:  sk,
+					}
+				}
+			}
+		}
+		return &CommitVerification{ //Default at this stage
+			Verified: false,
+			Reason:   "gpg.error.no_gpg_keys_found",
+		}
+	}
+
+	return &CommitVerification{
+		Verified: false,                         //Default value
+		Reason:   "gpg.error.not_signed_commit", //Default value
+	}
+}
+
+// ParseCommitsWithSignature checks if signaute of commits are corresponding to users gpg keys.
+func ParseCommitsWithSignature(oldCommits *list.List) *list.List {
+	var (
+		newCommits = list.New()
+		e          = oldCommits.Front()
+	)
+	for e != nil {
+		c := e.Value.(UserCommit)
+		newCommits.PushBack(SignCommit{
+			UserCommit:   &c,
+			Verification: ParseCommitWithSignature(c.Commit),
+		})
+		e = e.Next()
+	}
+	return newCommits
+}
diff --git a/models/gpg_key_test.go b/models/gpg_key_test.go
index 1ef5838e31..d291ce91c0 100644
--- a/models/gpg_key_test.go
+++ b/models/gpg_key_test.go
@@ -46,3 +46,119 @@ MkM/fdpyc2hY7Dl/+qFmN5MG5yGmMpQcX+RNNR222ibNC1D3wg==
 	assert.Nil(t, err, "Could not parse a valid GPG armored key", key)
 	//TODO verify value of key
 }
+
+func TestExtractSignature(t *testing.T) {
+	testGPGArmor := `-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQENBFh91QoBCADciaDd7aqegYkn4ZIG7J0p1CRwpqMGjxFroJEMg6M1ZiuEVTRv
+z49P4kcr1+98NvFmcNc+x5uJgvPCwr/N8ZW5nqBUs2yrklbFF4MeQomyZJJegP8m
+/dsRT3BwIT8YMUtJuCj0iqD9vuKYfjrztcMgC1sYwcE9E9OlA0pWBvUdU2i0TIB1
+vOq6slWGvHHa5l5gPfm09idlVxfH5+I+L1uIMx5ovbiVVU5x2f1AR1T18f0t2TVN
+0agFTyuoYE1ATmvJHmMcsfgM1Gpd9hIlr9vlupT2kKTPoNzVzsJsOU6Ku/Lf/bac
+mF+TfSbRCtmG7dkYZ4metLj7zG/WkW8IvJARABEBAAG0HUFudG9pbmUgR0lSQVJE
+IDxzYXBrQHNhcGsuZnI+iQFUBBMBCAA+FiEEEIOwJg/1vpF1itJ4roJVuKDYKOQF
+Alh91QoCGwMFCQPCZwAFCwkIBwIGFQgJCgsCBBYCAwECHgECF4AACgkQroJVuKDY
+KORreggAlIkC2QjHP5tb7b0+LksB2JMXdY+UzZBcJxtNmvA7gNQaGvWRrhrbePpa
+MKDP+3A4BPDBsWFbbB7N56vQ5tROpmWbNKuFOVER4S1bj0JZV0E+xkDLqt9QwQtQ
+ojd7oIZJwDUwdud1PvCza2mjgBqqiFE+twbc3i9xjciCGspMniUul1eQYLxRJ0w+
+sbvSOUnujnq5ByMSz9ij00O6aiPfNQS5oB5AALfpjYZDvWAAljLVrtmlQJWZ6dZo
+T/YNwsW2dECPuti8+Nmu5FxPGDTXxdbnRaeJTQ3T6q1oUVAv7yTXBx5NXfXkMa5i
+iEayQIH8Joq5Ev5ja/lRGQQhArMQ2bkBDQRYfdUKAQgAv7B3coLSrOQbuTZSlgWE
+QeT+7DWbmqE1LAQA1pQPcUPXLBUVd60amZJxF9nzUYcY83ylDi0gUNJS+DJGOXpT
+pzX2IOuOMGbtUSeKwg5s9O4SUO7f2yCc3RGaegER5zgESxelmOXG+b/hoNt7JbdU
+JtxcnLr91Jw2PBO/Xf0ZKJ01CQG2Yzdrrj6jnrHyx94seHy0i6xH1o0OuvfVMLfN
+/Vbb/ZHh6ym2wHNqRX62b0VAbchcJXX/MEehXGknKTkO6dDUd+mhRgWMf9ZGRFWx
+ag4qALimkf1FXtAyD0vxFYeyoWUQzrOvUsm2BxIN/986R08fhkBQnp5nz07mrU02
+cQARAQABiQE8BBgBCAAmFiEEEIOwJg/1vpF1itJ4roJVuKDYKOQFAlh91QoCGwwF
+CQPCZwAACgkQroJVuKDYKOT32wf/UZqMdPn5OhyhffFzjQx7wolrf92WkF2JkxtH
+6c3Htjlt/p5RhtKEeErSrNAxB4pqB7dznHaJXiOdWEZtRVXXjlNHjrokGTesqtKk
+lHWtK62/MuyLdr+FdCl68F3ewuT2iu/MDv+D4HPqA47zma9xVgZ9ZNwJOpv3fCOo
+RfY66UjGEnfgYifgtI5S84/mp2jaSc9UNvlZB6RSf8cfbJUL74kS2lq+xzSlf0yP
+Av844q/BfRuVsJsK1NDNG09LC30B0l3LKBqlrRmRTUMHtgchdX2dY+p7GPOoSzlR
+MkM/fdpyc2hY7Dl/+qFmN5MG5yGmMpQcX+RNNR222ibNC1D3wg==
+=i9b7
+-----END PGP PUBLIC KEY BLOCK-----`
+	ekey, err := checkArmoredGPGKeyString(testGPGArmor)
+	assert.Nil(t, err, "Could not parse a valid GPG armored key", ekey)
+
+	pubkey := ekey.PrimaryKey
+	content, err := base64EncPubKey(pubkey)
+	assert.Nil(t, err, "Could not base64 encode a valid PublicKey content", ekey)
+
+	key := &GPGKey{
+		KeyID:             pubkey.KeyIdString(),
+		Content:           content,
+		Created:           pubkey.CreationTime,
+		CanSign:           pubkey.CanSign(),
+		CanEncryptComms:   pubkey.PubKeyAlgo.CanEncrypt(),
+		CanEncryptStorage: pubkey.PubKeyAlgo.CanEncrypt(),
+		CanCertify:        pubkey.PubKeyAlgo.CanSign(),
+	}
+
+	cannotsignkey := &GPGKey{
+		KeyID:             pubkey.KeyIdString(),
+		Content:           content,
+		Created:           pubkey.CreationTime,
+		CanSign:           false,
+		CanEncryptComms:   false,
+		CanEncryptStorage: false,
+		CanCertify:        false,
+	}
+
+	testGoodSigArmor := `-----BEGIN PGP SIGNATURE-----
+
+iQEzBAABCAAdFiEEEIOwJg/1vpF1itJ4roJVuKDYKOQFAljAiQIACgkQroJVuKDY
+KORvCgf6A/Ehh0r7QbO2tFEghT+/Ab+bN7jRN3zP9ed6/q/ophYmkrU0NibtbJH9
+AwFVdHxCmj78SdiRjaTKyevklXw34nvMftmvnOI4lBNUdw6KWl25/n/7wN0l2oZW
+rW3UawYpZgodXiLTYarfEimkDQmT67ArScjRA6lLbkEYKO0VdwDu+Z6yBUH3GWtm
+45RkXpnsF6AXUfuD7YxnfyyDE1A7g7zj4vVYUAfWukJjqow/LsCUgETETJOqj9q3
+52/oQDs04fVkIEtCDulcY+K/fKlukBPJf9WceNDEqiENUzN/Z1y0E+tJ07cSy4bk
+yIJb+d0OAaG8bxloO7nJq4Res1Qa8Q==
+=puvG
+-----END PGP SIGNATURE-----`
+	testGoodPayload := `tree 56ae8d2799882b20381fc11659db06c16c68c61a
+parent c7870c39e4e6b247235ca005797703ec4254613f
+author Antoine GIRARD <sapk@sapk.fr> 1489012989 +0100
+committer Antoine GIRARD <sapk@sapk.fr> 1489012989 +0100
+
+Goog GPG
+`
+
+	testBadSigArmor := `-----BEGIN PGP SIGNATURE-----
+
+iQEzBAABCAAdFiEE5yr4rn9ulbdMxJFiPYI/ySNrtNkFAljAiYkACgkQPYI/ySNr
+tNmDdQf+NXhVRiOGt0GucpjJCGrOnK/qqVUmQyRUfrqzVUdb/1/Ws84V5/wE547I
+6z3oxeBKFsJa1CtIlxYaUyVhYnDzQtphJzub+Aw3UG0E2ywiE+N7RCa1Ufl7pPxJ
+U0SD6gvNaeTDQV/Wctu8v8DkCtEd3N8cMCDWhvy/FQEDztVtzm8hMe0Vdm0ozEH6
+P0W93sDNkLC5/qpWDN44sFlYDstW5VhMrnF0r/ohfaK2kpYHhkPk7WtOoHSUwQSg
+c4gfhjvXIQrWFnII1Kr5jFGlmgNSR02qpb31VGkMzSnBhWVf2OaHS/kI49QHJakq
+AhVDEnoYLCgoDGg9c3p1Ll2452/c6Q==
+=uoGV
+-----END PGP SIGNATURE-----`
+	testBadPayload := `tree 3074ff04951956a974e8b02d57733b0766f7cf6c
+parent fd3577542f7ad1554c7c7c0eb86bb57a1324ad91
+author Antoine GIRARD <sapk@sapk.fr> 1489013107 +0100
+committer Antoine GIRARD <sapk@sapk.fr> 1489013107 +0100
+
+Unkonwn GPG key with good email
+`
+	//Reading Sign
+	goodSig, err := extractSignature(testGoodSigArmor)
+	assert.Nil(t, err, "Could not parse a valid GPG armored signature", testGoodSigArmor)
+	badSig, err := extractSignature(testBadSigArmor)
+	assert.Nil(t, err, "Could not parse a valid GPG armored signature", testBadSigArmor)
+
+	//Generating hash of commit
+	goodHash, err := populateHash(goodSig.Hash, []byte(testGoodPayload))
+	assert.Nil(t, err, "Could not generate a valid hash of payload", testGoodPayload)
+	badHash, err := populateHash(badSig.Hash, []byte(testBadPayload))
+	assert.Nil(t, err, "Could not generate a valid hash of payload", testBadPayload)
+
+	//Verify
+	err = verifySign(goodSig, goodHash, key)
+	assert.Nil(t, err, "Could not validate a good signature")
+	err = verifySign(badSig, badHash, key)
+	assert.NotNil(t, err, "Validate a bad signature")
+	err = verifySign(goodSig, goodHash, cannotsignkey)
+	assert.NotNil(t, err, "Validate a bad signature with a kay that can not sign")
+}
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 829c460619..f8e21b5dff 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1349,3 +1349,13 @@ no_read = You do not have any read notifications.
 pin = Pin notification
 mark_as_read = Mark as read
 mark_as_unread = Mark as unread
+
+
+[gpg]
+error.extract_sign = Failed to extract signature
+error.generate_hash = Failed to generate hash of commit
+error.no_committer_account = No account linked to committer email
+error.no_gpg_keys_found = "Failed to retrieve publics keys of committer"
+error.no_gpg_keys_found = "No known key found for this signature in database"
+error.not_signed_commit = "Not a signed commit"
+error.failed_retrieval_gpg_keys = "Failed to retrieve any key attached to the commiter account"
diff --git a/public/css/index.css b/public/css/index.css
index 3863042bc3..01ffd7e928 100644
--- a/public/css/index.css
+++ b/public/css/index.css
@@ -1924,8 +1924,29 @@ footer .ui.language .menu {
   padding-left: 15px;
 }
 .repository #commits-table thead .sha {
-  font-size: 13px;
-  padding: 6px 40px 4px 35px;
+  text-align: center;
+  width: 140px;
+}
+.repository #commits-table td.sha .sha.label {
+  margin: 0;
+}
+.repository #commits-table td.sha .sha.label.isSigned {
+  border: 1px solid #BBB;
+}
+.repository #commits-table td.sha .sha.label.isSigned .detail.icon {
+  background: #FAFAFA;
+  margin: -6px -10px -4px 0px;
+  padding: 5px 3px 5px 6px;
+  border-left: 1px solid #BBB;
+  border-top-left-radius: 0;
+  border-bottom-left-radius: 0;
+}
+.repository #commits-table td.sha .sha.label.isSigned.isVerified {
+  border: 1px solid #21BA45;
+  background: #21BA4518;
+}
+.repository #commits-table td.sha .sha.label.isSigned.isVerified .detail.icon {
+  border-left: 1px solid #21BA4580;
 }
 .repository #commits-table.ui.basic.striped.table tbody tr:nth-child(2n) {
   background-color: rgba(0, 0, 0, 0.02) !important;
@@ -2239,6 +2260,16 @@ footer .ui.language .menu {
   margin-left: 26px;
   padding-top: 0;
 }
+.repository .ui.attached.isSigned.isVerified:not(.positive) {
+  border-left: 1px solid #A3C293;
+  border-right: 1px solid #A3C293;
+}
+.repository .ui.attached.isSigned.isVerified.top:not(.positive) {
+  border-top: 1px solid #A3C293;
+}
+.repository .ui.attached.isSigned.isVerified:not(.positive):last-child {
+  border-bottom: 1px solid #A3C293;
+}
 .user-cards .list {
   padding: 0;
 }
diff --git a/public/less/_repository.less b/public/less/_repository.less
index 2009847670..406dfb2ac8 100644
--- a/public/less/_repository.less
+++ b/public/less/_repository.less
@@ -800,8 +800,31 @@
 				padding-left: 15px;
 			}
 			.sha {
-				font-size: 13px;
-				padding: 6px 40px 4px 35px;
+				text-align: center;
+				width: 140px;
+			}
+		}
+		td.sha{
+			.sha.label{
+				margin: 0;
+				&.isSigned{
+					border: 1px solid #BBB;
+					.detail.icon{
+						background: #FAFAFA;
+						margin: -6px -10px -4px 0px;
+						padding: 5px 3px 5px 6px;
+						border-left: 1px solid #BBB;
+						border-top-left-radius: 0;
+						border-bottom-left-radius: 0;
+					}
+				}
+				&.isSigned.isVerified{
+					border: 1px solid #21BA45;
+					background: #21BA4518;
+					.detail.icon{
+						border-left: 1px solid #21BA4580;
+					}
+				}
 			}
 		}
 		&.ui.basic.striped.table tbody tr:nth-child(2n) {
@@ -1206,6 +1229,18 @@
 			}
 		}
 	}
+	.ui.attached.isSigned.isVerified{
+        &:not(.positive){
+		    border-left: 1px solid #A3C293;
+		    border-right: 1px solid #A3C293;
+	    }
+	    &.top:not(.positive){
+		    border-top: 1px solid #A3C293;
+	    }
+        &:not(.positive):last-child {
+            border-bottom: 1px solid #A3C293;
+        }
+	}
 }
 // End of .repository
 
diff --git a/routers/api/v1/convert/convert.go b/routers/api/v1/convert/convert.go
index 7485ac9e2a..153993ce82 100644
--- a/routers/api/v1/convert/convert.go
+++ b/routers/api/v1/convert/convert.go
@@ -44,6 +44,7 @@ func ToCommit(c *git.Commit) *api.PayloadCommit {
 	if err == nil {
 		committerUsername = committer.Name
 	}
+	verif := models.ParseCommitWithSignature(c)
 	return &api.PayloadCommit{
 		ID:      c.ID.String(),
 		Message: c.Message(),
@@ -59,6 +60,12 @@ func ToCommit(c *git.Commit) *api.PayloadCommit {
 			UserName: committerUsername,
 		},
 		Timestamp: c.Author.When,
+		Verification: &api.PayloadCommitVerification{
+			Verified:  verif.Verified,
+			Reason:    verif.Reason,
+			Signature: c.Signature.Signature,
+			Payload:   c.Signature.Payload,
+		},
 	}
 }
 
diff --git a/routers/repo/commit.go b/routers/repo/commit.go
index 7f4457c52b..62f55a52e3 100644
--- a/routers/repo/commit.go
+++ b/routers/repo/commit.go
@@ -68,6 +68,7 @@ func Commits(ctx *context.Context) {
 	}
 	commits = renderIssueLinks(commits, ctx.Repo.RepoLink)
 	commits = models.ValidateCommitsWithEmails(commits)
+	commits = models.ParseCommitsWithSignature(commits)
 	ctx.Data["Commits"] = commits
 
 	ctx.Data["Username"] = ctx.Repo.Owner.Name
@@ -121,6 +122,7 @@ func SearchCommits(ctx *context.Context) {
 	}
 	commits = renderIssueLinks(commits, ctx.Repo.RepoLink)
 	commits = models.ValidateCommitsWithEmails(commits)
+	commits = models.ParseCommitsWithSignature(commits)
 	ctx.Data["Commits"] = commits
 
 	ctx.Data["Keyword"] = keyword
@@ -167,6 +169,7 @@ func FileHistory(ctx *context.Context) {
 	}
 	commits = renderIssueLinks(commits, ctx.Repo.RepoLink)
 	commits = models.ValidateCommitsWithEmails(commits)
+	commits = models.ParseCommitsWithSignature(commits)
 	ctx.Data["Commits"] = commits
 
 	ctx.Data["Username"] = ctx.Repo.Owner.Name
@@ -222,6 +225,7 @@ func Diff(ctx *context.Context) {
 	ctx.Data["IsImageFile"] = commit.IsImageFile
 	ctx.Data["Title"] = commit.Summary() + " ยท " + base.ShortSha(commitID)
 	ctx.Data["Commit"] = commit
+	ctx.Data["Verification"] = models.ParseCommitWithSignature(commit)
 	ctx.Data["Author"] = models.ValidateCommitWithEmail(commit)
 	ctx.Data["Diff"] = diff
 	ctx.Data["Parents"] = parents
@@ -276,6 +280,7 @@ func CompareDiff(ctx *context.Context) {
 		return
 	}
 	commits = models.ValidateCommitsWithEmails(commits)
+	commits = models.ParseCommitsWithSignature(commits)
 
 	ctx.Data["CommitRepoLink"] = ctx.Repo.RepoLink
 	ctx.Data["Commits"] = commits
diff --git a/templates/repo/commits_table.tmpl b/templates/repo/commits_table.tmpl
index a2c2c352c7..8e473f22da 100644
--- a/templates/repo/commits_table.tmpl
+++ b/templates/repo/commits_table.tmpl
@@ -21,7 +21,8 @@
 			<thead>
 				<tr>
 					<th class="four wide">{{.i18n.Tr "repo.commits.author"}}</th>
-					<th class="nine wide message"><span class="sha">SHA1</span> {{.i18n.Tr "repo.commits.message"}}</th>
+					<th class="two wide sha">SHA1</th>
+					<th class="seven wide message">{{.i18n.Tr "repo.commits.message"}}</th>
 					<th class="three wide right aligned">{{.i18n.Tr "repo.commits.date"}}</th>
 				</tr>
 			</thead>
@@ -40,9 +41,21 @@
 								<img class="ui avatar image" src="{{AvatarLink .Author.Email}}" alt=""/>&nbsp;&nbsp;{{.Author.Name}}
 							{{end}}
 						</td>
-
+						<td class="sha">
+							<a rel="nofollow" class="ui sha label {{if .Signature}} isSigned {{if .Verification.Verified }} isVerified {{end}}{{end}}" href="{{AppSubUrl}}/{{$.Username}}/{{$.Reponame}}/commit/{{.ID}}">
+								{{ShortSha .ID.String}}
+								{{if .Signature}}
+									<div class="ui detail icon button">
+										{{if .Verification.Verified}}
+											<i title="{{.Verification.Reason}}" class="lock green icon"></i>
+										{{else}}
+											<i title="{{$.i18n.Tr .Verification.Reason}}" class="unlock icon"></i>
+										{{end}}
+									</div>
+								{{end}}
+							</a>
+						</td>
 						<td class="message collapsing">
-							<a rel="nofollow" class="ui sha label" href="{{AppSubUrl}}/{{$.Username}}/{{$.Reponame}}/commit/{{.ID}}">{{ShortSha .ID.String}}</a>
 							<span class="has-emoji{{if gt .ParentCount 1}} grey text{{end}}">{{RenderCommitMessage false .Summary $.RepoLink $.Repository.ComposeMetas}}</span>
 						</td>
 						<td class="grey text right aligned">{{TimeSince .Author.When $.Lang}}</td>
diff --git a/templates/repo/diff/page.tmpl b/templates/repo/diff/page.tmpl
index 90ee680576..d4a9d72dbe 100644
--- a/templates/repo/diff/page.tmpl
+++ b/templates/repo/diff/page.tmpl
@@ -5,13 +5,13 @@
 		{{if .IsDiffCompare }}
 			{{template "repo/commits_table" .}}
 		{{else}}
-			<div class="ui top attached info clearing segment">
+			<div class="ui top attached info clearing segment {{if .Commit.Signature}} isSigned {{if .Verification.Verified }} isVerified {{end}}{{end}}">
 				<a class="ui floated right blue tiny button" href="{{EscapePound .SourcePath}}">
 					{{.i18n.Tr "repo.diff.browse_source"}}
 				</a>
 				{{RenderCommitMessage true .Commit.Message $.RepoLink $.Repository.ComposeMetas}}
 			</div>
-			<div class="ui attached info segment">
+			<div class="ui attached info segment {{if .Commit.Signature}} isSigned {{if .Verification.Verified }} isVerified {{end}}{{end}}">
 				{{if .Author}}
 					<img class="ui avatar image" src="{{.Author.RelAvatarLink}}" />
 				  {{if .Author.FullName}}
@@ -41,6 +41,21 @@
 					</div>
 				</div>
 			</div>
+			{{if .Commit.Signature}}
+				{{if .Verification.Verified }}
+					<div class="ui bottom attached positive message" style="text-align: initial;color: black;">
+					  <i class="green lock icon"></i>
+						<span style="color: #2C662D;">Signed by :</span>
+						<a href="{{.Verification.SigningUser.HomeLink}}"><strong>{{.Commit.Committer.Name}}</strong></a> <{{.Commit.Committer.Email}}>
+						<span class="pull-right"><span style="color: #2C662D;">GPG key ID:</span> {{.Verification.SigningKey.KeyID}}</span>
+					</div>
+				{{else}}
+					<div class="ui bottom attached message" style="text-align: initial;color: black;">
+					  <i class="grey unlock icon"></i>
+					  {{.i18n.Tr .Verification.Reason}}
+					</div>
+				{{end}}
+			{{end}}
 		{{end}}
 
 		{{template "repo/diff/box" .}}
diff --git a/vendor/code.gitea.io/git/commit.go b/vendor/code.gitea.io/git/commit.go
index fa5e185619..28dd264835 100644
--- a/vendor/code.gitea.io/git/commit.go
+++ b/vendor/code.gitea.io/git/commit.go
@@ -6,6 +6,7 @@ package git
 
 import (
 	"bufio"
+	"bytes"
 	"container/list"
 	"fmt"
 	"net/http"
@@ -22,11 +23,30 @@ type Commit struct {
 	Author        *Signature
 	Committer     *Signature
 	CommitMessage string
+	Signature     *CommitGPGSignature
 
 	parents        []SHA1 // SHA1 strings
 	submoduleCache *ObjectCache
 }
 
+// CommitGPGSignature represents a git commit signature part.
+type CommitGPGSignature struct {
+	Signature string
+	Payload   string //TODO check if can be reconstruct from the rest of commit information to not have duplicate data
+}
+
+// similar to https://github.com/git/git/blob/3bc53220cb2dcf709f7a027a3f526befd021d858/commit.c#L1128
+func newGPGSignatureFromCommitline(data []byte, signatureStart int) (*CommitGPGSignature, error) {
+	sig := new(CommitGPGSignature)
+	signatureEnd := bytes.LastIndex(data, []byte("-----END PGP SIGNATURE-----"))
+	if signatureEnd == -1 {
+		return nil, fmt.Errorf("end of commit signature not found")
+	}
+	sig.Signature = strings.Replace(string(data[signatureStart:signatureEnd+27]), "\n ", "\n", -1)
+	sig.Payload = string(data[:signatureStart-8]) + string(data[signatureEnd+27:])
+	return sig, nil
+}
+
 // Message returns the commit message. Same as retrieving CommitMessage directly.
 func (c *Commit) Message() string {
 	return c.CommitMessage
diff --git a/vendor/code.gitea.io/git/repo_commit.go b/vendor/code.gitea.io/git/repo_commit.go
index 97f44abdac..37219734a0 100644
--- a/vendor/code.gitea.io/git/repo_commit.go
+++ b/vendor/code.gitea.io/git/repo_commit.go
@@ -78,6 +78,12 @@ l:
 					return nil, err
 				}
 				commit.Committer = sig
+			case "gpgsig":
+				sig, err := newGPGSignatureFromCommitline(data, nextline+spacepos+1)
+				if err != nil {
+					return nil, err
+				}
+				commit.Signature = sig
 			}
 			nextline += eol + 1
 		case eol == 0:
diff --git a/vendor/code.gitea.io/sdk/gitea/hook.go b/vendor/code.gitea.io/sdk/gitea/hook.go
index c2f88f4a31..4b45068127 100644
--- a/vendor/code.gitea.io/sdk/gitea/hook.go
+++ b/vendor/code.gitea.io/sdk/gitea/hook.go
@@ -137,12 +137,21 @@ type PayloadUser struct {
 
 // PayloadCommit FIXME: consider use same format as API when commits API are added.
 type PayloadCommit struct {
-	ID        string       `json:"id"`
-	Message   string       `json:"message"`
-	URL       string       `json:"url"`
-	Author    *PayloadUser `json:"author"`
-	Committer *PayloadUser `json:"committer"`
-	Timestamp time.Time    `json:"timestamp"`
+	ID           string                     `json:"id"`
+	Message      string                     `json:"message"`
+	URL          string                     `json:"url"`
+	Author       *PayloadUser               `json:"author"`
+	Committer    *PayloadUser               `json:"committer"`
+	Verification *PayloadCommitVerification `json:"verification"`
+	Timestamp    time.Time                  `json:"timestamp"`
+}
+
+// PayloadCommitVerification represent the GPG verification part of a commit. FIXME: like PayloadCommit consider use same format as API when commits API are added.
+type PayloadCommitVerification struct {
+	Verified  bool   `json:"verified"`
+	Reason    string `json:"reason"`
+	Signature string `json:"signature"`
+	Payload   string `json:"payload"`
 }
 
 var (
diff --git a/vendor/code.gitea.io/sdk/gitea/user_gpgkey.go b/vendor/code.gitea.io/sdk/gitea/user_gpgkey.go
index 911e63f1a3..c8afe92c92 100644
--- a/vendor/code.gitea.io/sdk/gitea/user_gpgkey.go
+++ b/vendor/code.gitea.io/sdk/gitea/user_gpgkey.go
@@ -38,6 +38,12 @@ type CreateGPGKeyOption struct {
 	ArmoredKey string `json:"armored_public_key" binding:"Required"`
 }
 
+// ListGPGKeys list all the GPG keys of the user
+func (c *Client) ListGPGKeys(user string) ([]*GPGKey, error) {
+	keys := make([]*GPGKey, 0, 10)
+	return keys, c.getParsedResponse("GET", fmt.Sprintf("/users/%s/gpg_keys", user), nil, nil, &keys)
+}
+
 // ListMyGPGKeys list all the GPG keys of current user
 func (c *Client) ListMyGPGKeys() ([]*GPGKey, error) {
 	keys := make([]*GPGKey, 0, 10)
diff --git a/vendor/vendor.json b/vendor/vendor.json
index 91d889c6fa..4b24848ea7 100644
--- a/vendor/vendor.json
+++ b/vendor/vendor.json
@@ -3,16 +3,16 @@
 	"ignore": "test",
 	"package": [
 		{
-			"checksumSHA1": "nt2y/SNJe3Rl0tzdaEyGQfCc4L4=",
+			"checksumSHA1": "bKoCvndU5ZVC5vqtwYjuU3YPJ6k=",
 			"path": "code.gitea.io/git",
-			"revision": "b4c06a53d0f619e84a99eb042184663d4ad8a32b",
-			"revisionTime": "2017-02-22T02:52:05Z"
+			"revision": "337468881d5961d36de8e950a607d6033e73dcf0",
+			"revisionTime": "2017-03-13T15:07:03Z"
 		},
 		{
-			"checksumSHA1": "qXD1HI8bTn7qNJZJOeZqQgxo354=",
+			"checksumSHA1": "32qRX47gRmdBW4l4hCKGRZbuIJk=",
 			"path": "code.gitea.io/sdk/gitea",
-			"revision": "8807a1d2ced513880b288a5e2add39df6bf72144",
-			"revisionTime": "2017-03-04T10:22:44Z"
+			"revision": "9ceaabb8c70aba1ff73718332db2356356e26ffb",
+			"revisionTime": "2017-03-09T22:08:57Z"
 		},
 		{
 			"checksumSHA1": "IyfS7Rbl6OgR83QR7TOfKdDCq+M=",