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-----
|
||||||
|
|
||||||
|
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
|
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" .}}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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