* GPG commit validation * Add translation + some little fix * Move hash calc after retrieving of potential key + missing translation * Add some little testtags/v1.21.12.1
| @@ -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" .}} | |||
| @@ -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 | |||
| @@ -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: | |||
| @@ -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 ( | |||
| @@ -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) | |||
| @@ -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=", | |||