Skip to content

Commit 3afb5b0

Browse files
committed
GPG commit validation
1 parent 71d16f6 commit 3afb5b0

File tree

12 files changed

+352
-21
lines changed

12 files changed

+352
-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+
1420
"github.com/go-xorm/xorm"
21+
"github.com/ngaut/log"
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 reads an armored signture 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+
expectedType := "PGP SIGNATURE"
319+
block, err := armor.Decode(r)
320+
if err != nil {
321+
return
322+
}
323+
if block.Type != expectedType {
324+
return nil, fmt.Errorf("expected '" + expectedType + "', got: " + block.Type)
325+
}
326+
return block.Body, nil
327+
}
328+
329+
func extractSignature(s string) (*packet.Signature, error) {
330+
r, err := readArmoredSign(strings.NewReader(s))
331+
if err != nil {
332+
return nil, fmt.Errorf("Failed to read signature armor")
333+
}
334+
p, err := packet.Read(r)
335+
if err != nil {
336+
return nil, fmt.Errorf("Failed to read signature packet")
337+
}
338+
sig, ok := p.(*packet.Signature)
339+
if !ok {
340+
return nil, fmt.Errorf("Packet is not a signature")
341+
}
342+
return sig, nil
343+
}
344+
345+
func verifySign(s *packet.Signature, h hash.Hash, k *GPGKey) error {
346+
//Check if key can sign
347+
if !k.CanSign {
348+
return fmt.Errorf("key can not sign")
349+
}
350+
//Decode key
351+
b, err := readerFromBase64(k.Content)
352+
if err != nil {
353+
return err
354+
}
355+
//Read key
356+
p, err := packet.Read(b)
357+
if err != nil {
358+
return err
359+
}
360+
361+
//Check type
362+
pkey, ok := p.(*packet.PublicKey)
363+
if !ok {
364+
return fmt.Errorf("key is not a public key")
365+
}
366+
367+
return pkey.VerifySignature(h, s)
368+
}
369+
370+
// ParseCommitWithSignature check if signature is good against keystore.
371+
func ParseCommitWithSignature(c *git.Commit) *CommitVerification {
372+
373+
if c.Signature != nil {
374+
375+
//Parsing signature
376+
sig, err := extractSignature(c.Signature.Signature)
377+
if err != nil { //Skipping failed to extract sign
378+
log.Error(3, "SignatureRead err: %v", err)
379+
return &CommitVerification{
380+
Verified: false,
381+
Reason: "Failed to extract signature",
382+
}
383+
}
384+
//Generating hash of commit
385+
hash, err := populateHash(sig.Hash, []byte(c.Signature.Payload))
386+
if err != nil { //Skipping ailed to generate hash
387+
log.Error(3, "PopulateHash: %v", err)
388+
return &CommitVerification{
389+
Verified: false,
390+
Reason: "Failed to generate hash of commit",
391+
}
392+
}
393+
394+
//Find Committer account
395+
committer, err := GetUserByEmail(c.Committer.Email)
396+
if err != nil { //Skipping not user for commiter
397+
log.Error(3, "NoCommitterAccount: %v", err)
398+
return &CommitVerification{
399+
Verified: false,
400+
Reason: "No account linked to committer email",
401+
}
402+
}
403+
404+
keys, err := ListGPGKeys(committer.ID)
405+
if err != nil { //Skipping failed to get gpg keys of user
406+
log.Error(3, "ListGPGKeys: %v", err)
407+
return &CommitVerification{
408+
Verified: false,
409+
Reason: "Failed to retrieve publics keys of committer",
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: "No known key found for this signature in database",
438+
}
439+
}
440+
441+
return &CommitVerification{
442+
Verified: false, //Default value
443+
Reason: "Not a 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+
}

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
}

public/less/_repository.less

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -800,8 +800,31 @@
800800
padding-left: 15px;
801801
}
802802
.sha {
803-
font-size: 13px;
804-
padding: 6px 40px 4px 35px;
803+
text-align: center;
804+
width: 140px;
805+
}
806+
}
807+
td.sha{
808+
.sha.label{
809+
margin: 0;
810+
&.isSigned{
811+
border: 1px solid #BBB;
812+
.detail.icon{
813+
background: #FAFAFA;
814+
margin: -6px -10px -4px 0px;
815+
padding: 5px 3px 5px 6px;
816+
border-left: 1px solid #BBB;
817+
border-top-left-radius: 0;
818+
border-bottom-left-radius: 0;
819+
}
820+
}
821+
&.isSigned.isVerified{
822+
border: 1px solid #21BA45;
823+
background: #21BA4518;
824+
.detail.icon{
825+
border-left: 1px solid #21BA4580;
826+
}
827+
}
805828
}
806829
}
807830
&.ui.basic.striped.table tbody tr:nth-child(2n) {
@@ -1206,6 +1229,18 @@
12061229
}
12071230
}
12081231
}
1232+
.ui.attached.isSigned.isVerified{
1233+
&:not(.positive){
1234+
border-left: 1px solid #A3C293;
1235+
border-right: 1px solid #A3C293;
1236+
}
1237+
&.top:not(.positive){
1238+
border-top: 1px solid #A3C293;
1239+
}
1240+
&:not(.positive):last-child {
1241+
border-bottom: 1px solid #A3C293;
1242+
}
1243+
}
12091244
}
12101245
// End of .repository
12111246

routers/api/v1/convert/convert.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ func ToCommit(c *git.Commit) *api.PayloadCommit {
4444
if err == nil {
4545
committerUsername = committer.Name
4646
}
47+
verif := models.ParseCommitWithSignature(c)
4748
return &api.PayloadCommit{
4849
ID: c.ID.String(),
4950
Message: c.Message(),
@@ -59,6 +60,12 @@ func ToCommit(c *git.Commit) *api.PayloadCommit {
5960
UserName: committerUsername,
6061
},
6162
Timestamp: c.Author.When,
63+
Verification: &api.PayloadCommitVerification{
64+
Verified: verif.Verified,
65+
Reason: verif.Reason,
66+
Signature: c.Signature.Signature,
67+
Payload: c.Signature.Payload,
68+
},
6269
}
6370
}
6471

routers/repo/commit.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ func Commits(ctx *context.Context) {
6868
}
6969
commits = renderIssueLinks(commits, ctx.Repo.RepoLink)
7070
commits = models.ValidateCommitsWithEmails(commits)
71+
commits = models.ParseCommitsWithSignature(commits)
7172
ctx.Data["Commits"] = commits
7273

7374
ctx.Data["Username"] = ctx.Repo.Owner.Name
@@ -121,6 +122,7 @@ func SearchCommits(ctx *context.Context) {
121122
}
122123
commits = renderIssueLinks(commits, ctx.Repo.RepoLink)
123124
commits = models.ValidateCommitsWithEmails(commits)
125+
commits = models.ParseCommitsWithSignature(commits)
124126
ctx.Data["Commits"] = commits
125127

126128
ctx.Data["Keyword"] = keyword
@@ -167,6 +169,7 @@ func FileHistory(ctx *context.Context) {
167169
}
168170
commits = renderIssueLinks(commits, ctx.Repo.RepoLink)
169171
commits = models.ValidateCommitsWithEmails(commits)
172+
commits = models.ParseCommitsWithSignature(commits)
170173
ctx.Data["Commits"] = commits
171174

172175
ctx.Data["Username"] = ctx.Repo.Owner.Name
@@ -222,6 +225,7 @@ func Diff(ctx *context.Context) {
222225
ctx.Data["IsImageFile"] = commit.IsImageFile
223226
ctx.Data["Title"] = commit.Summary() + " · " + base.ShortSha(commitID)
224227
ctx.Data["Commit"] = commit
228+
ctx.Data["Verification"] = models.ParseCommitWithSignature(commit)
225229
ctx.Data["Author"] = models.ValidateCommitWithEmail(commit)
226230
ctx.Data["Diff"] = diff
227231
ctx.Data["Parents"] = parents
@@ -276,6 +280,7 @@ func CompareDiff(ctx *context.Context) {
276280
return
277281
}
278282
commits = models.ValidateCommitsWithEmails(commits)
283+
commits = models.ParseCommitsWithSignature(commits)
279284

280285
ctx.Data["CommitRepoLink"] = ctx.Repo.RepoLink
281286
ctx.Data["Commits"] = commits

templates/repo/commits_table.tmpl

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
<thead>
2222
<tr>
2323
<th class="four wide">{{.i18n.Tr "repo.commits.author"}}</th>
24-
<th class="nine wide message"><span class="sha">SHA1</span> {{.i18n.Tr "repo.commits.message"}}</th>
24+
<th class="two wide sha">SHA1</th>
25+
<th class="seven wide message">{{.i18n.Tr "repo.commits.message"}}</th>
2526
<th class="three wide right aligned">{{.i18n.Tr "repo.commits.date"}}</th>
2627
</tr>
2728
</thead>
@@ -40,9 +41,17 @@
4041
<img class="ui avatar image" src="{{AvatarLink .Author.Email}}" alt=""/>&nbsp;&nbsp;{{.Author.Name}}
4142
{{end}}
4243
</td>
43-
44+
<td class="sha">
45+
<a rel="nofollow" class="ui sha label {{if .Signature}} isSigned {{if .Verification.Verified }} isVerified {{end}}{{end}}" href="{{AppSubUrl}}/{{$.Username}}/{{$.Reponame}}/commit/{{.ID}}">
46+
{{ShortSha .ID.String}}
47+
{{if .Signature}}
48+
<div class="ui detail icon button">
49+
<i title="{{.Verification.Reason}}" class="{{if .Verification.Verified }}lock green{{else}}unlock{{end}} icon"></i>
50+
</div>
51+
{{end}}
52+
</a>
53+
</td>
4454
<td class="message collapsing">
45-
<a rel="nofollow" class="ui sha label" href="{{AppSubUrl}}/{{$.Username}}/{{$.Reponame}}/commit/{{.ID}}">{{ShortSha .ID.String}}</a>
4655
<span class="has-emoji{{if gt .ParentCount 1}} grey text{{end}}">{{RenderCommitMessage false .Summary $.RepoLink $.Repository.ComposeMetas}}</span>
4756
</td>
4857
<td class="grey text right aligned">{{TimeSince .Author.When $.Lang}}</td>

0 commit comments

Comments
 (0)