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
This commit is contained in:
		
							parent
							
								
									9224405155
								
							
						
					
					
						commit
						14fe9010ae
					
				| @ -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 | ||||
| } | ||||
|  | ||||
| @ -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") | ||||
| } | ||||
|  | ||||
| @ -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" | ||||
|  | ||||
| @ -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; | ||||
| } | ||||
|  | ||||
| @ -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 | ||||
| 
 | ||||
|  | ||||
| @ -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, | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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=""/>  {{.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> | ||||
|  | ||||
| @ -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" .}} | ||||
|  | ||||
							
								
								
									
										20
									
								
								vendor/code.gitea.io/git/commit.go
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										20
									
								
								vendor/code.gitea.io/git/commit.go
									
									
									
										generated
									
									
										vendored
									
									
								
							| @ -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 | ||||
|  | ||||
							
								
								
									
										6
									
								
								vendor/code.gitea.io/git/repo_commit.go
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								vendor/code.gitea.io/git/repo_commit.go
									
									
									
										generated
									
									
										vendored
									
									
								
							| @ -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: | ||||
|  | ||||
							
								
								
									
										21
									
								
								vendor/code.gitea.io/sdk/gitea/hook.go
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										21
									
								
								vendor/code.gitea.io/sdk/gitea/hook.go
									
									
									
										generated
									
									
										vendored
									
									
								
							| @ -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 ( | ||||
|  | ||||
							
								
								
									
										6
									
								
								vendor/code.gitea.io/sdk/gitea/user_gpgkey.go
									
									
									
										generated
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								vendor/code.gitea.io/sdk/gitea/user_gpgkey.go
									
									
									
										generated
									
									
										vendored
									
									
								
							| @ -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) | ||||
|  | ||||
							
								
								
									
										12
									
								
								vendor/vendor.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								vendor/vendor.json
									
									
									
									
										vendored
									
									
								
							| @ -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=", | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user