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 ( | import ( | ||||||
| 	"bytes" | 	"bytes" | ||||||
|  | 	"container/list" | ||||||
|  | 	"crypto" | ||||||
| 	"encoding/base64" | 	"encoding/base64" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"hash" | ||||||
|  | 	"io" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
| 
 | 
 | ||||||
|  | 	"code.gitea.io/git" | ||||||
|  | 	"code.gitea.io/gitea/modules/log" | ||||||
|  | 
 | ||||||
| 	"github.com/go-xorm/xorm" | 	"github.com/go-xorm/xorm" | ||||||
| 	"golang.org/x/crypto/openpgp" | 	"golang.org/x/crypto/openpgp" | ||||||
|  | 	"golang.org/x/crypto/openpgp/armor" | ||||||
| 	"golang.org/x/crypto/openpgp/packet" | 	"golang.org/x/crypto/openpgp/packet" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| @ -274,3 +282,181 @@ func DeleteGPGKey(doer *User, id int64) (err error) { | |||||||
| 
 | 
 | ||||||
| 	return nil | 	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) | 	assert.Nil(t, err, "Could not parse a valid GPG armored key", key) | ||||||
| 	//TODO verify value of key
 | 	//TODO verify value of key
 | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | func TestExtractSignature(t *testing.T) { | ||||||
|  | 	testGPGArmor := `-----BEGIN PGP PUBLIC KEY BLOCK----- | ||||||
|  | 
 | ||||||
|  | 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 | ||||||
|  | =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 | pin = Pin notification | ||||||
| mark_as_read = Mark as read | mark_as_read = Mark as read | ||||||
| mark_as_unread = Mark as unread | 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; |   padding-left: 15px; | ||||||
| } | } | ||||||
| .repository #commits-table thead .sha { | .repository #commits-table thead .sha { | ||||||
|   font-size: 13px; |   text-align: center; | ||||||
|   padding: 6px 40px 4px 35px; |   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) { | .repository #commits-table.ui.basic.striped.table tbody tr:nth-child(2n) { | ||||||
|   background-color: rgba(0, 0, 0, 0.02) !important; |   background-color: rgba(0, 0, 0, 0.02) !important; | ||||||
| @ -2239,6 +2260,16 @@ footer .ui.language .menu { | |||||||
|   margin-left: 26px; |   margin-left: 26px; | ||||||
|   padding-top: 0; |   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 { | .user-cards .list { | ||||||
|   padding: 0; |   padding: 0; | ||||||
| } | } | ||||||
|  | |||||||
| @ -800,8 +800,31 @@ | |||||||
| 				padding-left: 15px; | 				padding-left: 15px; | ||||||
| 			} | 			} | ||||||
| 			.sha { | 			.sha { | ||||||
| 				font-size: 13px; | 				text-align: center; | ||||||
| 				padding: 6px 40px 4px 35px; | 				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) { | 		&.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 | // End of .repository | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -44,6 +44,7 @@ func ToCommit(c *git.Commit) *api.PayloadCommit { | |||||||
| 	if err == nil { | 	if err == nil { | ||||||
| 		committerUsername = committer.Name | 		committerUsername = committer.Name | ||||||
| 	} | 	} | ||||||
|  | 	verif := models.ParseCommitWithSignature(c) | ||||||
| 	return &api.PayloadCommit{ | 	return &api.PayloadCommit{ | ||||||
| 		ID:      c.ID.String(), | 		ID:      c.ID.String(), | ||||||
| 		Message: c.Message(), | 		Message: c.Message(), | ||||||
| @ -59,6 +60,12 @@ func ToCommit(c *git.Commit) *api.PayloadCommit { | |||||||
| 			UserName: committerUsername, | 			UserName: committerUsername, | ||||||
| 		}, | 		}, | ||||||
| 		Timestamp: c.Author.When, | 		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 = renderIssueLinks(commits, ctx.Repo.RepoLink) | ||||||
| 	commits = models.ValidateCommitsWithEmails(commits) | 	commits = models.ValidateCommitsWithEmails(commits) | ||||||
|  | 	commits = models.ParseCommitsWithSignature(commits) | ||||||
| 	ctx.Data["Commits"] = commits | 	ctx.Data["Commits"] = commits | ||||||
| 
 | 
 | ||||||
| 	ctx.Data["Username"] = ctx.Repo.Owner.Name | 	ctx.Data["Username"] = ctx.Repo.Owner.Name | ||||||
| @ -121,6 +122,7 @@ func SearchCommits(ctx *context.Context) { | |||||||
| 	} | 	} | ||||||
| 	commits = renderIssueLinks(commits, ctx.Repo.RepoLink) | 	commits = renderIssueLinks(commits, ctx.Repo.RepoLink) | ||||||
| 	commits = models.ValidateCommitsWithEmails(commits) | 	commits = models.ValidateCommitsWithEmails(commits) | ||||||
|  | 	commits = models.ParseCommitsWithSignature(commits) | ||||||
| 	ctx.Data["Commits"] = commits | 	ctx.Data["Commits"] = commits | ||||||
| 
 | 
 | ||||||
| 	ctx.Data["Keyword"] = keyword | 	ctx.Data["Keyword"] = keyword | ||||||
| @ -167,6 +169,7 @@ func FileHistory(ctx *context.Context) { | |||||||
| 	} | 	} | ||||||
| 	commits = renderIssueLinks(commits, ctx.Repo.RepoLink) | 	commits = renderIssueLinks(commits, ctx.Repo.RepoLink) | ||||||
| 	commits = models.ValidateCommitsWithEmails(commits) | 	commits = models.ValidateCommitsWithEmails(commits) | ||||||
|  | 	commits = models.ParseCommitsWithSignature(commits) | ||||||
| 	ctx.Data["Commits"] = commits | 	ctx.Data["Commits"] = commits | ||||||
| 
 | 
 | ||||||
| 	ctx.Data["Username"] = ctx.Repo.Owner.Name | 	ctx.Data["Username"] = ctx.Repo.Owner.Name | ||||||
| @ -222,6 +225,7 @@ func Diff(ctx *context.Context) { | |||||||
| 	ctx.Data["IsImageFile"] = commit.IsImageFile | 	ctx.Data["IsImageFile"] = commit.IsImageFile | ||||||
| 	ctx.Data["Title"] = commit.Summary() + " · " + base.ShortSha(commitID) | 	ctx.Data["Title"] = commit.Summary() + " · " + base.ShortSha(commitID) | ||||||
| 	ctx.Data["Commit"] = commit | 	ctx.Data["Commit"] = commit | ||||||
|  | 	ctx.Data["Verification"] = models.ParseCommitWithSignature(commit) | ||||||
| 	ctx.Data["Author"] = models.ValidateCommitWithEmail(commit) | 	ctx.Data["Author"] = models.ValidateCommitWithEmail(commit) | ||||||
| 	ctx.Data["Diff"] = diff | 	ctx.Data["Diff"] = diff | ||||||
| 	ctx.Data["Parents"] = parents | 	ctx.Data["Parents"] = parents | ||||||
| @ -276,6 +280,7 @@ func CompareDiff(ctx *context.Context) { | |||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 	commits = models.ValidateCommitsWithEmails(commits) | 	commits = models.ValidateCommitsWithEmails(commits) | ||||||
|  | 	commits = models.ParseCommitsWithSignature(commits) | ||||||
| 
 | 
 | ||||||
| 	ctx.Data["CommitRepoLink"] = ctx.Repo.RepoLink | 	ctx.Data["CommitRepoLink"] = ctx.Repo.RepoLink | ||||||
| 	ctx.Data["Commits"] = commits | 	ctx.Data["Commits"] = commits | ||||||
|  | |||||||
| @ -21,7 +21,8 @@ | |||||||
| 			<thead> | 			<thead> | ||||||
| 				<tr> | 				<tr> | ||||||
| 					<th class="four wide">{{.i18n.Tr "repo.commits.author"}}</th> | 					<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> | 					<th class="three wide right aligned">{{.i18n.Tr "repo.commits.date"}}</th> | ||||||
| 				</tr> | 				</tr> | ||||||
| 			</thead> | 			</thead> | ||||||
| @ -40,9 +41,21 @@ | |||||||
| 								<img class="ui avatar image" src="{{AvatarLink .Author.Email}}" alt=""/>  {{.Author.Name}} | 								<img class="ui avatar image" src="{{AvatarLink .Author.Email}}" alt=""/>  {{.Author.Name}} | ||||||
| 							{{end}} | 							{{end}} | ||||||
| 						</td> | 						</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"> | 						<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> | 							<span class="has-emoji{{if gt .ParentCount 1}} grey text{{end}}">{{RenderCommitMessage false .Summary $.RepoLink $.Repository.ComposeMetas}}</span> | ||||||
| 						</td> | 						</td> | ||||||
| 						<td class="grey text right aligned">{{TimeSince .Author.When $.Lang}}</td> | 						<td class="grey text right aligned">{{TimeSince .Author.When $.Lang}}</td> | ||||||
|  | |||||||
| @ -5,13 +5,13 @@ | |||||||
| 		{{if .IsDiffCompare }} | 		{{if .IsDiffCompare }} | ||||||
| 			{{template "repo/commits_table" .}} | 			{{template "repo/commits_table" .}} | ||||||
| 		{{else}} | 		{{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}}"> | 				<a class="ui floated right blue tiny button" href="{{EscapePound .SourcePath}}"> | ||||||
| 					{{.i18n.Tr "repo.diff.browse_source"}} | 					{{.i18n.Tr "repo.diff.browse_source"}} | ||||||
| 				</a> | 				</a> | ||||||
| 				{{RenderCommitMessage true .Commit.Message $.RepoLink $.Repository.ComposeMetas}} | 				{{RenderCommitMessage true .Commit.Message $.RepoLink $.Repository.ComposeMetas}} | ||||||
| 			</div> | 			</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}} | 				{{if .Author}} | ||||||
| 					<img class="ui avatar image" src="{{.Author.RelAvatarLink}}" /> | 					<img class="ui avatar image" src="{{.Author.RelAvatarLink}}" /> | ||||||
| 				  {{if .Author.FullName}} | 				  {{if .Author.FullName}} | ||||||
| @ -41,6 +41,21 @@ | |||||||
| 					</div> | 					</div> | ||||||
| 				</div> | 				</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}} | 		{{end}} | ||||||
| 
 | 
 | ||||||
| 		{{template "repo/diff/box" .}} | 		{{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 ( | import ( | ||||||
| 	"bufio" | 	"bufio" | ||||||
|  | 	"bytes" | ||||||
| 	"container/list" | 	"container/list" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| @ -22,11 +23,30 @@ type Commit struct { | |||||||
| 	Author        *Signature | 	Author        *Signature | ||||||
| 	Committer     *Signature | 	Committer     *Signature | ||||||
| 	CommitMessage string | 	CommitMessage string | ||||||
|  | 	Signature     *CommitGPGSignature | ||||||
| 
 | 
 | ||||||
| 	parents        []SHA1 // SHA1 strings
 | 	parents        []SHA1 // SHA1 strings
 | ||||||
| 	submoduleCache *ObjectCache | 	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.
 | // Message returns the commit message. Same as retrieving CommitMessage directly.
 | ||||||
| func (c *Commit) Message() string { | func (c *Commit) Message() string { | ||||||
| 	return c.CommitMessage | 	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 | 					return nil, err | ||||||
| 				} | 				} | ||||||
| 				commit.Committer = sig | 				commit.Committer = sig | ||||||
|  | 			case "gpgsig": | ||||||
|  | 				sig, err := newGPGSignatureFromCommitline(data, nextline+spacepos+1) | ||||||
|  | 				if err != nil { | ||||||
|  | 					return nil, err | ||||||
|  | 				} | ||||||
|  | 				commit.Signature = sig | ||||||
| 			} | 			} | ||||||
| 			nextline += eol + 1 | 			nextline += eol + 1 | ||||||
| 		case eol == 0: | 		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.
 | // PayloadCommit FIXME: consider use same format as API when commits API are added.
 | ||||||
| type PayloadCommit struct { | type PayloadCommit struct { | ||||||
| 	ID        string       `json:"id"` | 	ID           string                     `json:"id"` | ||||||
| 	Message   string       `json:"message"` | 	Message      string                     `json:"message"` | ||||||
| 	URL       string       `json:"url"` | 	URL          string                     `json:"url"` | ||||||
| 	Author    *PayloadUser `json:"author"` | 	Author       *PayloadUser               `json:"author"` | ||||||
| 	Committer *PayloadUser `json:"committer"` | 	Committer    *PayloadUser               `json:"committer"` | ||||||
| 	Timestamp time.Time    `json:"timestamp"` | 	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 ( | 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"` | 	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
 | // ListMyGPGKeys list all the GPG keys of current user
 | ||||||
| func (c *Client) ListMyGPGKeys() ([]*GPGKey, error) { | func (c *Client) ListMyGPGKeys() ([]*GPGKey, error) { | ||||||
| 	keys := make([]*GPGKey, 0, 10) | 	keys := make([]*GPGKey, 0, 10) | ||||||
|  | |||||||
							
								
								
									
										12
									
								
								vendor/vendor.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								vendor/vendor.json
									
									
									
									
										vendored
									
									
								
							| @ -3,16 +3,16 @@ | |||||||
| 	"ignore": "test", | 	"ignore": "test", | ||||||
| 	"package": [ | 	"package": [ | ||||||
| 		{ | 		{ | ||||||
| 			"checksumSHA1": "nt2y/SNJe3Rl0tzdaEyGQfCc4L4=", | 			"checksumSHA1": "bKoCvndU5ZVC5vqtwYjuU3YPJ6k=", | ||||||
| 			"path": "code.gitea.io/git", | 			"path": "code.gitea.io/git", | ||||||
| 			"revision": "b4c06a53d0f619e84a99eb042184663d4ad8a32b", | 			"revision": "337468881d5961d36de8e950a607d6033e73dcf0", | ||||||
| 			"revisionTime": "2017-02-22T02:52:05Z" | 			"revisionTime": "2017-03-13T15:07:03Z" | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			"checksumSHA1": "qXD1HI8bTn7qNJZJOeZqQgxo354=", | 			"checksumSHA1": "32qRX47gRmdBW4l4hCKGRZbuIJk=", | ||||||
| 			"path": "code.gitea.io/sdk/gitea", | 			"path": "code.gitea.io/sdk/gitea", | ||||||
| 			"revision": "8807a1d2ced513880b288a5e2add39df6bf72144", | 			"revision": "9ceaabb8c70aba1ff73718332db2356356e26ffb", | ||||||
| 			"revisionTime": "2017-03-04T10:22:44Z" | 			"revisionTime": "2017-03-09T22:08:57Z" | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			"checksumSHA1": "IyfS7Rbl6OgR83QR7TOfKdDCq+M=", | 			"checksumSHA1": "IyfS7Rbl6OgR83QR7TOfKdDCq+M=", | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user