Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GPG commit validation #1150

Merged
merged 4 commits into from
Mar 22, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions models/gpg_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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
}
116 changes: 116 additions & 0 deletions models/gpg_key_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 <[email protected]> 1489012989 +0100
committer Antoine GIRARD <[email protected]> 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 <[email protected]> 1489013107 +0100
committer Antoine GIRARD <[email protected]> 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")
}
10 changes: 10 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -1347,3 +1347,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"
35 changes: 33 additions & 2 deletions public/css/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down
Loading