Skip to content

Commit 14fe901

Browse files
sapklunny
authored andcommitted
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
1 parent 9224405 commit 14fe901

File tree

14 files changed

+480
-21
lines changed

14 files changed

+480
-21
lines changed

models/gpg_key.go

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,21 @@ package models
66

77
import (
88
"bytes"
9+
"container/list"
10+
"crypto"
911
"encoding/base64"
1012
"fmt"
13+
"hash"
14+
"io"
1115
"strings"
1216
"time"
1317

18+
"code.gitea.io/git"
19+
"code.gitea.io/gitea/modules/log"
20+
1421
"github.com/go-xorm/xorm"
1522
"golang.org/x/crypto/openpgp"
23+
"golang.org/x/crypto/openpgp/armor"
1624
"golang.org/x/crypto/openpgp/packet"
1725
)
1826

@@ -274,3 +282,181 @@ func DeleteGPGKey(doer *User, id int64) (err error) {
274282

275283
return nil
276284
}
285+
286+
// CommitVerification represents a commit validation of signature
287+
type CommitVerification struct {
288+
Verified bool
289+
Reason string
290+
SigningUser *User
291+
SigningKey *GPGKey
292+
}
293+
294+
// SignCommit represents a commit with validation of signature.
295+
type SignCommit struct {
296+
Verification *CommitVerification
297+
*UserCommit
298+
}
299+
300+
func readerFromBase64(s string) (io.Reader, error) {
301+
bs, err := base64.StdEncoding.DecodeString(s)
302+
if err != nil {
303+
return nil, err
304+
}
305+
return bytes.NewBuffer(bs), nil
306+
}
307+
308+
func populateHash(hashFunc crypto.Hash, msg []byte) (hash.Hash, error) {
309+
h := hashFunc.New()
310+
if _, err := h.Write(msg); err != nil {
311+
return nil, err
312+
}
313+
return h, nil
314+
}
315+
316+
// 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
317+
func readArmoredSign(r io.Reader) (body io.Reader, err error) {
318+
block, err := armor.Decode(r)
319+
if err != nil {
320+
return
321+
}
322+
if block.Type != openpgp.SignatureType {
323+
return nil, fmt.Errorf("expected '" + openpgp.SignatureType + "', got: " + block.Type)
324+
}
325+
return block.Body, nil
326+
}
327+
328+
func extractSignature(s string) (*packet.Signature, error) {
329+
r, err := readArmoredSign(strings.NewReader(s))
330+
if err != nil {
331+
return nil, fmt.Errorf("Failed to read signature armor")
332+
}
333+
p, err := packet.Read(r)
334+
if err != nil {
335+
return nil, fmt.Errorf("Failed to read signature packet")
336+
}
337+
sig, ok := p.(*packet.Signature)
338+
if !ok {
339+
return nil, fmt.Errorf("Packet is not a signature")
340+
}
341+
return sig, nil
342+
}
343+
344+
func verifySign(s *packet.Signature, h hash.Hash, k *GPGKey) error {
345+
//Check if key can sign
346+
if !k.CanSign {
347+
return fmt.Errorf("key can not sign")
348+
}
349+
//Decode key
350+
b, err := readerFromBase64(k.Content)
351+
if err != nil {
352+
return err
353+
}
354+
//Read key
355+
p, err := packet.Read(b)
356+
if err != nil {
357+
return err
358+
}
359+
360+
//Check type
361+
pkey, ok := p.(*packet.PublicKey)
362+
if !ok {
363+
return fmt.Errorf("key is not a public key")
364+
}
365+
366+
return pkey.VerifySignature(h, s)
367+
}
368+
369+
// ParseCommitWithSignature check if signature is good against keystore.
370+
func ParseCommitWithSignature(c *git.Commit) *CommitVerification {
371+
372+
if c.Signature != nil {
373+
374+
//Parsing signature
375+
sig, err := extractSignature(c.Signature.Signature)
376+
if err != nil { //Skipping failed to extract sign
377+
log.Error(3, "SignatureRead err: %v", err)
378+
return &CommitVerification{
379+
Verified: false,
380+
Reason: "gpg.error.extract_sign",
381+
}
382+
}
383+
384+
//Find Committer account
385+
committer, err := GetUserByEmail(c.Committer.Email)
386+
if err != nil { //Skipping not user for commiter
387+
log.Error(3, "NoCommitterAccount: %v", err)
388+
return &CommitVerification{
389+
Verified: false,
390+
Reason: "gpg.error.no_committer_account",
391+
}
392+
}
393+
394+
keys, err := ListGPGKeys(committer.ID)
395+
if err != nil || len(keys) == 0 { //Skipping failed to get gpg keys of user
396+
log.Error(3, "ListGPGKeys: %v", err)
397+
return &CommitVerification{
398+
Verified: false,
399+
Reason: "gpg.error.failed_retrieval_gpg_keys",
400+
}
401+
}
402+
403+
//Generating hash of commit
404+
hash, err := populateHash(sig.Hash, []byte(c.Signature.Payload))
405+
if err != nil { //Skipping ailed to generate hash
406+
log.Error(3, "PopulateHash: %v", err)
407+
return &CommitVerification{
408+
Verified: false,
409+
Reason: "gpg.error.generate_hash",
410+
}
411+
}
412+
413+
for _, k := range keys {
414+
//We get PK
415+
if err := verifySign(sig, hash, k); err == nil {
416+
return &CommitVerification{ //Everything is ok
417+
Verified: true,
418+
Reason: fmt.Sprintf("%s <%s> / %s", c.Committer.Name, c.Committer.Email, k.KeyID),
419+
SigningUser: committer,
420+
SigningKey: k,
421+
}
422+
}
423+
//And test also SubsKey
424+
for _, sk := range k.SubsKey {
425+
if err := verifySign(sig, hash, sk); err == nil {
426+
return &CommitVerification{ //Everything is ok
427+
Verified: true,
428+
Reason: fmt.Sprintf("%s <%s> / %s", c.Committer.Name, c.Committer.Email, sk.KeyID),
429+
SigningUser: committer,
430+
SigningKey: sk,
431+
}
432+
}
433+
}
434+
}
435+
return &CommitVerification{ //Default at this stage
436+
Verified: false,
437+
Reason: "gpg.error.no_gpg_keys_found",
438+
}
439+
}
440+
441+
return &CommitVerification{
442+
Verified: false, //Default value
443+
Reason: "gpg.error.not_signed_commit", //Default value
444+
}
445+
}
446+
447+
// ParseCommitsWithSignature checks if signaute of commits are corresponding to users gpg keys.
448+
func ParseCommitsWithSignature(oldCommits *list.List) *list.List {
449+
var (
450+
newCommits = list.New()
451+
e = oldCommits.Front()
452+
)
453+
for e != nil {
454+
c := e.Value.(UserCommit)
455+
newCommits.PushBack(SignCommit{
456+
UserCommit: &c,
457+
Verification: ParseCommitWithSignature(c.Commit),
458+
})
459+
e = e.Next()
460+
}
461+
return newCommits
462+
}

models/gpg_key_test.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,119 @@ MkM/fdpyc2hY7Dl/+qFmN5MG5yGmMpQcX+RNNR222ibNC1D3wg==
4646
assert.Nil(t, err, "Could not parse a valid GPG armored key", key)
4747
//TODO verify value of key
4848
}
49+
50+
func TestExtractSignature(t *testing.T) {
51+
testGPGArmor := `-----BEGIN PGP PUBLIC KEY BLOCK-----
52+
53+
mQENBFh91QoBCADciaDd7aqegYkn4ZIG7J0p1CRwpqMGjxFroJEMg6M1ZiuEVTRv
54+
z49P4kcr1+98NvFmcNc+x5uJgvPCwr/N8ZW5nqBUs2yrklbFF4MeQomyZJJegP8m
55+
/dsRT3BwIT8YMUtJuCj0iqD9vuKYfjrztcMgC1sYwcE9E9OlA0pWBvUdU2i0TIB1
56+
vOq6slWGvHHa5l5gPfm09idlVxfH5+I+L1uIMx5ovbiVVU5x2f1AR1T18f0t2TVN
57+
0agFTyuoYE1ATmvJHmMcsfgM1Gpd9hIlr9vlupT2kKTPoNzVzsJsOU6Ku/Lf/bac
58+
mF+TfSbRCtmG7dkYZ4metLj7zG/WkW8IvJARABEBAAG0HUFudG9pbmUgR0lSQVJE
59+
IDxzYXBrQHNhcGsuZnI+iQFUBBMBCAA+FiEEEIOwJg/1vpF1itJ4roJVuKDYKOQF
60+
Alh91QoCGwMFCQPCZwAFCwkIBwIGFQgJCgsCBBYCAwECHgECF4AACgkQroJVuKDY
61+
KORreggAlIkC2QjHP5tb7b0+LksB2JMXdY+UzZBcJxtNmvA7gNQaGvWRrhrbePpa
62+
MKDP+3A4BPDBsWFbbB7N56vQ5tROpmWbNKuFOVER4S1bj0JZV0E+xkDLqt9QwQtQ
63+
ojd7oIZJwDUwdud1PvCza2mjgBqqiFE+twbc3i9xjciCGspMniUul1eQYLxRJ0w+
64+
sbvSOUnujnq5ByMSz9ij00O6aiPfNQS5oB5AALfpjYZDvWAAljLVrtmlQJWZ6dZo
65+
T/YNwsW2dECPuti8+Nmu5FxPGDTXxdbnRaeJTQ3T6q1oUVAv7yTXBx5NXfXkMa5i
66+
iEayQIH8Joq5Ev5ja/lRGQQhArMQ2bkBDQRYfdUKAQgAv7B3coLSrOQbuTZSlgWE
67+
QeT+7DWbmqE1LAQA1pQPcUPXLBUVd60amZJxF9nzUYcY83ylDi0gUNJS+DJGOXpT
68+
pzX2IOuOMGbtUSeKwg5s9O4SUO7f2yCc3RGaegER5zgESxelmOXG+b/hoNt7JbdU
69+
JtxcnLr91Jw2PBO/Xf0ZKJ01CQG2Yzdrrj6jnrHyx94seHy0i6xH1o0OuvfVMLfN
70+
/Vbb/ZHh6ym2wHNqRX62b0VAbchcJXX/MEehXGknKTkO6dDUd+mhRgWMf9ZGRFWx
71+
ag4qALimkf1FXtAyD0vxFYeyoWUQzrOvUsm2BxIN/986R08fhkBQnp5nz07mrU02
72+
cQARAQABiQE8BBgBCAAmFiEEEIOwJg/1vpF1itJ4roJVuKDYKOQFAlh91QoCGwwF
73+
CQPCZwAACgkQroJVuKDYKOT32wf/UZqMdPn5OhyhffFzjQx7wolrf92WkF2JkxtH
74+
6c3Htjlt/p5RhtKEeErSrNAxB4pqB7dznHaJXiOdWEZtRVXXjlNHjrokGTesqtKk
75+
lHWtK62/MuyLdr+FdCl68F3ewuT2iu/MDv+D4HPqA47zma9xVgZ9ZNwJOpv3fCOo
76+
RfY66UjGEnfgYifgtI5S84/mp2jaSc9UNvlZB6RSf8cfbJUL74kS2lq+xzSlf0yP
77+
Av844q/BfRuVsJsK1NDNG09LC30B0l3LKBqlrRmRTUMHtgchdX2dY+p7GPOoSzlR
78+
MkM/fdpyc2hY7Dl/+qFmN5MG5yGmMpQcX+RNNR222ibNC1D3wg==
79+
=i9b7
80+
-----END PGP PUBLIC KEY BLOCK-----`
81+
ekey, err := checkArmoredGPGKeyString(testGPGArmor)
82+
assert.Nil(t, err, "Could not parse a valid GPG armored key", ekey)
83+
84+
pubkey := ekey.PrimaryKey
85+
content, err := base64EncPubKey(pubkey)
86+
assert.Nil(t, err, "Could not base64 encode a valid PublicKey content", ekey)
87+
88+
key := &GPGKey{
89+
KeyID: pubkey.KeyIdString(),
90+
Content: content,
91+
Created: pubkey.CreationTime,
92+
CanSign: pubkey.CanSign(),
93+
CanEncryptComms: pubkey.PubKeyAlgo.CanEncrypt(),
94+
CanEncryptStorage: pubkey.PubKeyAlgo.CanEncrypt(),
95+
CanCertify: pubkey.PubKeyAlgo.CanSign(),
96+
}
97+
98+
cannotsignkey := &GPGKey{
99+
KeyID: pubkey.KeyIdString(),
100+
Content: content,
101+
Created: pubkey.CreationTime,
102+
CanSign: false,
103+
CanEncryptComms: false,
104+
CanEncryptStorage: false,
105+
CanCertify: false,
106+
}
107+
108+
testGoodSigArmor := `-----BEGIN PGP SIGNATURE-----
109+
110+
iQEzBAABCAAdFiEEEIOwJg/1vpF1itJ4roJVuKDYKOQFAljAiQIACgkQroJVuKDY
111+
KORvCgf6A/Ehh0r7QbO2tFEghT+/Ab+bN7jRN3zP9ed6/q/ophYmkrU0NibtbJH9
112+
AwFVdHxCmj78SdiRjaTKyevklXw34nvMftmvnOI4lBNUdw6KWl25/n/7wN0l2oZW
113+
rW3UawYpZgodXiLTYarfEimkDQmT67ArScjRA6lLbkEYKO0VdwDu+Z6yBUH3GWtm
114+
45RkXpnsF6AXUfuD7YxnfyyDE1A7g7zj4vVYUAfWukJjqow/LsCUgETETJOqj9q3
115+
52/oQDs04fVkIEtCDulcY+K/fKlukBPJf9WceNDEqiENUzN/Z1y0E+tJ07cSy4bk
116+
yIJb+d0OAaG8bxloO7nJq4Res1Qa8Q==
117+
=puvG
118+
-----END PGP SIGNATURE-----`
119+
testGoodPayload := `tree 56ae8d2799882b20381fc11659db06c16c68c61a
120+
parent c7870c39e4e6b247235ca005797703ec4254613f
121+
author Antoine GIRARD <[email protected]> 1489012989 +0100
122+
committer Antoine GIRARD <[email protected]> 1489012989 +0100
123+
124+
Goog GPG
125+
`
126+
127+
testBadSigArmor := `-----BEGIN PGP SIGNATURE-----
128+
129+
iQEzBAABCAAdFiEE5yr4rn9ulbdMxJFiPYI/ySNrtNkFAljAiYkACgkQPYI/ySNr
130+
tNmDdQf+NXhVRiOGt0GucpjJCGrOnK/qqVUmQyRUfrqzVUdb/1/Ws84V5/wE547I
131+
6z3oxeBKFsJa1CtIlxYaUyVhYnDzQtphJzub+Aw3UG0E2ywiE+N7RCa1Ufl7pPxJ
132+
U0SD6gvNaeTDQV/Wctu8v8DkCtEd3N8cMCDWhvy/FQEDztVtzm8hMe0Vdm0ozEH6
133+
P0W93sDNkLC5/qpWDN44sFlYDstW5VhMrnF0r/ohfaK2kpYHhkPk7WtOoHSUwQSg
134+
c4gfhjvXIQrWFnII1Kr5jFGlmgNSR02qpb31VGkMzSnBhWVf2OaHS/kI49QHJakq
135+
AhVDEnoYLCgoDGg9c3p1Ll2452/c6Q==
136+
=uoGV
137+
-----END PGP SIGNATURE-----`
138+
testBadPayload := `tree 3074ff04951956a974e8b02d57733b0766f7cf6c
139+
parent fd3577542f7ad1554c7c7c0eb86bb57a1324ad91
140+
author Antoine GIRARD <[email protected]> 1489013107 +0100
141+
committer Antoine GIRARD <[email protected]> 1489013107 +0100
142+
143+
Unkonwn GPG key with good email
144+
`
145+
//Reading Sign
146+
goodSig, err := extractSignature(testGoodSigArmor)
147+
assert.Nil(t, err, "Could not parse a valid GPG armored signature", testGoodSigArmor)
148+
badSig, err := extractSignature(testBadSigArmor)
149+
assert.Nil(t, err, "Could not parse a valid GPG armored signature", testBadSigArmor)
150+
151+
//Generating hash of commit
152+
goodHash, err := populateHash(goodSig.Hash, []byte(testGoodPayload))
153+
assert.Nil(t, err, "Could not generate a valid hash of payload", testGoodPayload)
154+
badHash, err := populateHash(badSig.Hash, []byte(testBadPayload))
155+
assert.Nil(t, err, "Could not generate a valid hash of payload", testBadPayload)
156+
157+
//Verify
158+
err = verifySign(goodSig, goodHash, key)
159+
assert.Nil(t, err, "Could not validate a good signature")
160+
err = verifySign(badSig, badHash, key)
161+
assert.NotNil(t, err, "Validate a bad signature")
162+
err = verifySign(goodSig, goodHash, cannotsignkey)
163+
assert.NotNil(t, err, "Validate a bad signature with a kay that can not sign")
164+
}

options/locale/locale_en-US.ini

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1349,3 +1349,13 @@ no_read = You do not have any read notifications.
13491349
pin = Pin notification
13501350
mark_as_read = Mark as read
13511351
mark_as_unread = Mark as unread
1352+
1353+
1354+
[gpg]
1355+
error.extract_sign = Failed to extract signature
1356+
error.generate_hash = Failed to generate hash of commit
1357+
error.no_committer_account = No account linked to committer email
1358+
error.no_gpg_keys_found = "Failed to retrieve publics keys of committer"
1359+
error.no_gpg_keys_found = "No known key found for this signature in database"
1360+
error.not_signed_commit = "Not a signed commit"
1361+
error.failed_retrieval_gpg_keys = "Failed to retrieve any key attached to the commiter account"

public/css/index.css

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1924,8 +1924,29 @@ footer .ui.language .menu {
19241924
padding-left: 15px;
19251925
}
19261926
.repository #commits-table thead .sha {
1927-
font-size: 13px;
1928-
padding: 6px 40px 4px 35px;
1927+
text-align: center;
1928+
width: 140px;
1929+
}
1930+
.repository #commits-table td.sha .sha.label {
1931+
margin: 0;
1932+
}
1933+
.repository #commits-table td.sha .sha.label.isSigned {
1934+
border: 1px solid #BBB;
1935+
}
1936+
.repository #commits-table td.sha .sha.label.isSigned .detail.icon {
1937+
background: #FAFAFA;
1938+
margin: -6px -10px -4px 0px;
1939+
padding: 5px 3px 5px 6px;
1940+
border-left: 1px solid #BBB;
1941+
border-top-left-radius: 0;
1942+
border-bottom-left-radius: 0;
1943+
}
1944+
.repository #commits-table td.sha .sha.label.isSigned.isVerified {
1945+
border: 1px solid #21BA45;
1946+
background: #21BA4518;
1947+
}
1948+
.repository #commits-table td.sha .sha.label.isSigned.isVerified .detail.icon {
1949+
border-left: 1px solid #21BA4580;
19291950
}
19301951
.repository #commits-table.ui.basic.striped.table tbody tr:nth-child(2n) {
19311952
background-color: rgba(0, 0, 0, 0.02) !important;
@@ -2239,6 +2260,16 @@ footer .ui.language .menu {
22392260
margin-left: 26px;
22402261
padding-top: 0;
22412262
}
2263+
.repository .ui.attached.isSigned.isVerified:not(.positive) {
2264+
border-left: 1px solid #A3C293;
2265+
border-right: 1px solid #A3C293;
2266+
}
2267+
.repository .ui.attached.isSigned.isVerified.top:not(.positive) {
2268+
border-top: 1px solid #A3C293;
2269+
}
2270+
.repository .ui.attached.isSigned.isVerified:not(.positive):last-child {
2271+
border-bottom: 1px solid #A3C293;
2272+
}
22422273
.user-cards .list {
22432274
padding: 0;
22442275
}

0 commit comments

Comments
 (0)