Skip to content

Commit

Permalink
Show Signer in commit lists and add basic trust (#10425)
Browse files Browse the repository at this point in the history
* Show Signer in commit lists and add basic trust

Show the avatar of the signer in the commit list pages as we do not
enforce that the signer is an author or committer. This makes it
clearer who has signed the commit.

Also display commits signed by non-members differently from
members and in particular make it clear when a non-member signer
is different from the committer to help reduce the risk of
spoofing.

Signed-off-by: Andrew Thornton <[email protected]>

* ensure orange text and background is available

Signed-off-by: Andrew Thornton <[email protected]>

* Update gpg_key.go

* Update models/gpg_key.go

* Apply suggestions from code review

* Require team collaborators to have access to UnitTypeCode

* as per @6543

* fix position of sha as per @silverwind

* as per @guillep2k
  • Loading branch information
zeripath committed Feb 27, 2020
1 parent 858aebc commit 90919bb
Show file tree
Hide file tree
Showing 16 changed files with 416 additions and 70 deletions.
2 changes: 1 addition & 1 deletion docs/content/doc/features/comparison.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ _Symbols used in table:_
| Git LFS 2.0 ||||||||
| Group Milestones ||||||||
| Granular user roles (Code, Issues, Wiki etc) ||||||||
| Verified Committer | || ? |||||
| Verified Committer | || ? |||||
| GPG Signed Commits ||||||||
| Reject unsigned commits | [](https://github.com/go-gitea/gitea/pull/9708) |||||||
| Repository Activity page ||||||||
Expand Down
43 changes: 40 additions & 3 deletions models/gpg_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,7 @@ type CommitVerification struct {
CommittingUser *User
SigningEmail string
SigningKey *GPGKey
TrustStatus string
}

// SignCommit represents a commit with validation of signature.
Expand Down Expand Up @@ -759,18 +760,54 @@ func verifyWithGPGSettings(gpgSettings *git.GPGSettings, sig *packet.Signature,
}

// ParseCommitsWithSignature checks if signaute of commits are corresponding to users gpg keys.
func ParseCommitsWithSignature(oldCommits *list.List) *list.List {
func ParseCommitsWithSignature(oldCommits *list.List, repository *Repository) *list.List {
var (
newCommits = list.New()
e = oldCommits.Front()
)
memberMap := map[int64]bool{}

for e != nil {
c := e.Value.(UserCommit)
newCommits.PushBack(SignCommit{
signCommit := SignCommit{
UserCommit: &c,
Verification: ParseCommitWithSignature(c.Commit),
})
}

_ = CalculateTrustStatus(signCommit.Verification, repository, &memberMap)

newCommits.PushBack(signCommit)
e = e.Next()
}
return newCommits
}

// CalculateTrustStatus will calculate the TrustStatus for a commit verification within a repository
func CalculateTrustStatus(verification *CommitVerification, repository *Repository, memberMap *map[int64]bool) (err error) {
if verification.Verified {
verification.TrustStatus = "trusted"
if verification.SigningUser.ID != 0 {
var isMember bool
if memberMap != nil {
var has bool
isMember, has = (*memberMap)[verification.SigningUser.ID]
if !has {
isMember, err = repository.IsOwnerMemberCollaborator(verification.SigningUser.ID)
(*memberMap)[verification.SigningUser.ID] = isMember
}
} else {
isMember, err = repository.IsOwnerMemberCollaborator(verification.SigningUser.ID)
}

if !isMember {
verification.TrustStatus = "untrusted"
if verification.CommittingUser.ID != verification.SigningUser.ID {
// The committing user and the signing user are not the same and are not the default key
// This should be marked as questionable unless the signing user is a collaborator/team member etc.
verification.TrustStatus = "unmatched"
}
}
}
}
return
}
20 changes: 20 additions & 0 deletions models/repo_collaboration.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,3 +210,23 @@ func (repo *Repository) getRepoTeams(e Engine) (teams []*Team, err error) {
func (repo *Repository) GetRepoTeams() ([]*Team, error) {
return repo.getRepoTeams(x)
}

// IsOwnerMemberCollaborator checks if a provided user is the owner, a collaborator or a member of a team in a repository
func (repo *Repository) IsOwnerMemberCollaborator(userID int64) (bool, error) {
if repo.OwnerID == userID {
return true, nil
}
teamMember, err := x.Join("INNER", "team_repo", "team_repo.team_id = team_user.team_id").
Join("INNER", "team_unit", "team_unit.team_id = team_user.team_id").
Where("team_repo.repo_id = ?", repo.ID).
And("team_unit.`type` = ?", UnitTypeCode).
And("team_user.uid = ?", userID).Table("team_user").Exist(&TeamUser{})
if err != nil {
return false, err
}
if teamMember {
return true, nil
}

return x.Get(&Collaboration{RepoID: repo.ID, UserID: userID})
}
2 changes: 2 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,8 @@ commits.date = Date
commits.older = Older
commits.newer = Newer
commits.signed_by = Signed by
commits.signed_by_untrusted_user = Signed by untrusted user
commits.signed_by_untrusted_user_unmatched = Signed by untrusted user who does not match committer
commits.gpg_key_id = GPG Key ID
ext_issues = Ext. Issues
Expand Down
2 changes: 0 additions & 2 deletions routers/private/hook.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,8 @@ func readAndVerifyCommit(sha string, repo *git.Repository, env []string) error {
if err != nil {
return err
}
log.Info("have commit %s", commit.ID.String())
verification := models.ParseCommitWithSignature(commit)
if !verification.Verified {
log.Info("unverified commit %s", commit.ID.String())
cancel()
return &errUnverifiedCommit{
commit.ID.String(),
Expand Down
14 changes: 10 additions & 4 deletions routers/repo/commit.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func Commits(ctx *context.Context) {
return
}
commits = models.ValidateCommitsWithEmails(commits)
commits = models.ParseCommitsWithSignature(commits)
commits = models.ParseCommitsWithSignature(commits, ctx.Repo.Repository)
commits = models.ParseCommitsWithStatus(commits, ctx.Repo.Repository)
ctx.Data["Commits"] = commits

Expand Down Expand Up @@ -139,7 +139,7 @@ func SearchCommits(ctx *context.Context) {
return
}
commits = models.ValidateCommitsWithEmails(commits)
commits = models.ParseCommitsWithSignature(commits)
commits = models.ParseCommitsWithSignature(commits, ctx.Repo.Repository)
commits = models.ParseCommitsWithStatus(commits, ctx.Repo.Repository)
ctx.Data["Commits"] = commits

Expand Down Expand Up @@ -185,7 +185,7 @@ func FileHistory(ctx *context.Context) {
return
}
commits = models.ValidateCommitsWithEmails(commits)
commits = models.ParseCommitsWithSignature(commits)
commits = models.ParseCommitsWithSignature(commits, ctx.Repo.Repository)
commits = models.ParseCommitsWithStatus(commits, ctx.Repo.Repository)
ctx.Data["Commits"] = commits

Expand Down Expand Up @@ -269,12 +269,18 @@ func Diff(ctx *context.Context) {
setPathsCompareContext(ctx, parentCommit, commit, headTarget)
ctx.Data["Title"] = commit.Summary() + " · " + base.ShortSha(commitID)
ctx.Data["Commit"] = commit
ctx.Data["Verification"] = models.ParseCommitWithSignature(commit)
verification := models.ParseCommitWithSignature(commit)
ctx.Data["Verification"] = verification
ctx.Data["Author"] = models.ValidateCommitWithEmail(commit)
ctx.Data["Diff"] = diff
ctx.Data["Parents"] = parents
ctx.Data["DiffNotAvailable"] = diff.NumFiles() == 0

if err := models.CalculateTrustStatus(verification, ctx.Repo.Repository, nil); err != nil {
ctx.ServerError("CalculateTrustStatus", err)
return
}

note := &git.Note{}
err = git.GetNote(ctx.Repo.GitRepo, commitID, note)
if err == nil {
Expand Down
2 changes: 1 addition & 1 deletion routers/repo/compare.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ func PrepareCompareDiff(
}

compareInfo.Commits = models.ValidateCommitsWithEmails(compareInfo.Commits)
compareInfo.Commits = models.ParseCommitsWithSignature(compareInfo.Commits)
compareInfo.Commits = models.ParseCommitsWithSignature(compareInfo.Commits, headRepo)
compareInfo.Commits = models.ParseCommitsWithStatus(compareInfo.Commits, headRepo)
ctx.Data["Commits"] = compareInfo.Commits
ctx.Data["CommitCount"] = compareInfo.Commits.Len()
Expand Down
2 changes: 1 addition & 1 deletion routers/repo/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,7 @@ func ViewPullCommits(ctx *context.Context) {
ctx.Data["Reponame"] = ctx.Repo.Repository.Name
commits = prInfo.Commits
commits = models.ValidateCommitsWithEmails(commits)
commits = models.ParseCommitsWithSignature(commits)
commits = models.ParseCommitsWithSignature(commits, ctx.Repo.Repository)
commits = models.ParseCommitsWithStatus(commits, ctx.Repo.Repository)
ctx.Data["Commits"] = commits
ctx.Data["CommitCount"] = commits.Len()
Expand Down
9 changes: 8 additions & 1 deletion routers/repo/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,14 @@ func renderDirectory(ctx *context.Context, treeLink string) {
// Show latest commit info of repository in table header,
// or of directory if not in root directory.
ctx.Data["LatestCommit"] = latestCommit
ctx.Data["LatestCommitVerification"] = models.ParseCommitWithSignature(latestCommit)
verification := models.ParseCommitWithSignature(latestCommit)

if err := models.CalculateTrustStatus(verification, ctx.Repo.Repository, nil); err != nil {
ctx.ServerError("CalculateTrustStatus", err)
return
}
ctx.Data["LatestCommitVerification"] = verification

ctx.Data["LatestCommitUser"] = models.ValidateCommitWithEmail(latestCommit)

statuses, err := models.GetLatestCommitStatus(ctx.Repo.Repository, ctx.Repo.Commit.ID.String(), 0)
Expand Down
2 changes: 1 addition & 1 deletion routers/repo/wiki.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry)
return nil, nil
}
commitsHistory = models.ValidateCommitsWithEmails(commitsHistory)
commitsHistory = models.ParseCommitsWithSignature(commitsHistory)
commitsHistory = models.ParseCommitsWithSignature(commitsHistory, ctx.Repo.Repository)

ctx.Data["Commits"] = commitsHistory

Expand Down
71 changes: 44 additions & 27 deletions templates/repo/commit_page.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,22 @@
<div class="repository diff">
{{template "repo/header" .}}
<div class="ui container {{if .IsSplitStyle}}fluid padded{{end}}">
<div class="ui top attached info clearing segment {{if .Commit.Signature}} isSigned {{if .Verification.Verified }} isVerified {{end}}{{end}}">
{{$class := ""}}
{{if .Commit.Signature}}
{{$class = (printf "%s%s" $class " isSigned")}}
{{if .Verification.Verified}}
{{if eq .Verification.TrustStatus "trusted"}}
{{$class = (printf "%s%s" $class " isVerified")}}
{{else if eq .Verification.TrustStatus "untrusted"}}
{{$class = (printf "%s%s" $class " isVerifiedUntrusted")}}
{{else}}
{{$class = (printf "%s%s" $class " isVerifiedUnmatched")}}
{{end}}
{{else if .Verification.Warning}}
{{$class = (printf "%s%s" $class " isWarning")}}
{{end}}
{{end}}
<div class="ui top attached info clearing segment {{$class}}">
<a class="ui floated right blue tiny button" href="{{EscapePound .SourcePath}}">
{{.i18n.Tr "repo.diff.browse_source"}}
</a>
Expand All @@ -12,15 +27,15 @@
{{end}}
<span class="text grey">{{svg "octicon-git-branch" 16}}{{.BranchName}}</span>
</div>
<div class="ui attached info segment {{if .Commit.Signature}} isSigned {{if .Verification.Verified }} isVerified {{end}}{{end}}">
<div class="ui attached info segment {{$class}}">
<div class="ui stackable grid">
<div class="nine wide column">
{{if .Author}}
<img class="ui avatar image" src="{{.Author.RelAvatarLink}}" />
{{if .Author.FullName}}
<a href="{{.Author.HomeLink}}"><strong>{{.Author.FullName}}</strong></a> {{if .IsSigned}}<{{.Commit.Author.Email}}>{{end}}
<a href="{{.Author.HomeLink}}"><strong>{{.Author.FullName}}</strong> {{if .IsSigned}}&lt;{{.Commit.Author.Email}}&gt;{{end}}</a>
{{else}}
<a href="{{.Author.HomeLink}}"><strong>{{.Commit.Author.Name}}</strong></a> {{if .IsSigned}}<{{.Commit.Author.Email}}>{{end}}
<a href="{{.Author.HomeLink}}"><strong>{{.Commit.Author.Name}}</strong> {{if .IsSigned}}&lt;{{.Commit.Author.Email}}&gt;{{end}}</a>
{{end}}
{{else}}
<img class="ui avatar image" src="{{AvatarLink .Commit.Author.Email}}" />
Expand All @@ -30,7 +45,7 @@
<span> </span>
{{if ne .Verification.CommittingUser.ID 0}}
<img class="ui avatar image" src="{{.Verification.CommittingUser.RelAvatarLink}}" />
<a href="{{.Verification.CommittingUser.HomeLink}}"><strong>{{.Commit.Committer.Name}}</strong></a> <{{.Commit.Committer.Email}}>
<a href="{{.Verification.CommittingUser.HomeLink}}"><strong>{{.Commit.Committer.Name}}</strong> &lt;{{.Commit.Committer.Email}}&gt;</a>
{{else}}
<img class="ui avatar image" src="{{AvatarLink .Commit.Committer.Email}}" />
<strong>{{.Commit.Committer.Name}}</strong>
Expand Down Expand Up @@ -58,40 +73,42 @@
</div><!-- end grid -->
</div>
{{if .Commit.Signature}}
{{if .Verification.Verified }}
<div class="ui bottom attached positive message">
<div class="ui bottom attached message {{$class}}">
{{if .Verification.Verified }}
{{if ne .Verification.SigningUser.ID 0}}
<i class="green lock icon"></i>
<span>{{.i18n.Tr "repo.commits.signed_by"}}:</span>
<i class="lock icon"></i>
{{if eq .Verification.TrustStatus "trusted"}}
<span class="ui text">{{.i18n.Tr "repo.commits.signed_by"}}:</span>
{{else if eq .Verification.TrustStatus "untrusted"}}
<span class="ui text">{{.i18n.Tr "repo.commits.signed_by_untrusted_user"}}:</span>
{{else}}
<span class="ui text">{{.i18n.Tr "repo.commits.signed_by_untrusted_user_unmatched"}}:</span>
{{end}}
<img class="ui avatar image" src="{{.Verification.SigningUser.RelAvatarLink}}" />
<a href="{{.Verification.SigningUser.HomeLink}}"><strong>{{.Verification.SigningUser.Name}}</strong></a> <{{.Verification.SigningEmail}}>
<span class="pull-right"><span>{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span> {{.Verification.SigningKey.KeyID}}</span>
<a href="{{.Verification.SigningUser.HomeLink}}"><strong>{{.Verification.SigningUser.Name}}</strong> <{{.Verification.SigningEmail}}></a>
<span class="pull-right"><span class="ui text">{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span> {{.Verification.SigningKey.KeyID}}</span>
{{else}}
<i class="icons" title="{{.i18n.Tr "gpg.default_key"}}">
<i class="green lock icon"></i>
<i class="lock icon"></i>
<i class="tiny inverted cog icon centerlock"></i>
</i>
<span>{{.i18n.Tr "repo.commits.signed_by"}}:</span>
<span class="ui text">{{.i18n.Tr "repo.commits.signed_by"}}:</span>
<img class="ui avatar image" src="{{AvatarLink .Verification.SigningEmail}}" />
<strong>{{.Verification.SigningUser.Name}}</strong> <{{.Verification.SigningEmail}}>
<span class="pull-right"><span>{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span> <i class="cogs icon" title="{{.i18n.Tr "gpg.default_key"}}"></i>{{.Verification.SigningKey.KeyID}}</span>
<span class="pull-right"><span class="ui text">{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span> <i class="cogs icon" title="{{.i18n.Tr "gpg.default_key"}}"></i>{{.Verification.SigningKey.KeyID}}</span>
{{end}}
</div>
{{else if .Verification.Warning}}
<div class="ui bottom attached message">
<i class="red unlock icon"></i>
<span class="red text">{{.i18n.Tr .Verification.Reason}}</span>
<span class="pull-right"><span class="red text">{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span> <i class="red warning icon"></i>{{.Verification.SigningKey.KeyID}}</span>
</div>
{{else}}
<div class="ui bottom attached message">
<i class="grey unlock icon"></i>
{{else if .Verification.Warning}}
<i class="unlock icon"></i>
<span class="ui text">{{.i18n.Tr .Verification.Reason}}</span>
<span class="pull-right"><span class="ui text">{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span> <i class="warning icon"></i>{{.Verification.SigningKey.KeyID}}</span>
{{else}}
<i class="unlock icon"></i>
{{.i18n.Tr .Verification.Reason}}
{{if and .Verification.SigningKey (ne .Verification.SigningKey.KeyID "")}}
<span class="pull-right"><span class="red text">{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span> <i class="red warning icon"></i>{{.Verification.SigningKey.KeyID}}</span>
<span class="pull-right"><span class="ui text">{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span> <i class="warning icon"></i>{{.Verification.SigningKey.KeyID}}</span>
{{end}}
</div>
{{end}}
{{end}}
</div>
{{end}}
{{if .Note}}
<div class="ui top attached info segment message git-notes">
Expand Down
32 changes: 20 additions & 12 deletions templates/repo/commits_list.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,13 @@
{{if .Signature}}
{{$class = (printf "%s%s" $class " isSigned")}}
{{if .Verification.Verified}}
{{$class = (printf "%s%s" $class " isVerified")}}
{{if eq .Verification.TrustStatus "trusted"}}
{{$class = (printf "%s%s" $class " isVerified")}}
{{else if eq .Verification.TrustStatus "untrusted"}}
{{$class = (printf "%s%s" $class " isVerifiedUntrusted")}}
{{else}}
{{$class = (printf "%s%s" $class " isVerifiedUnmatched")}}
{{end}}
{{else if .Verification.Warning}}
{{$class = (printf "%s%s" $class " isWarning")}}
{{end}}
Expand All @@ -38,20 +44,22 @@
{{else}}
<span class="{{$class}}">
{{end}}
{{ShortSha .ID.String}}
<span class="shortsha">{{ShortSha .ID.String}}</span>
{{if .Signature}}
<div class="ui detail icon button">
{{if .Verification.Verified}}
{{if ne .Verification.SigningUser.ID 0}}
<i title="{{.Verification.Reason}}" class="lock green icon"></i>
{{else}}
<i title="{{.Verification.Reason}}" class="icons">
<i class="green lock icon"></i>
<i class="tiny inverted cog icon centerlock"></i>
</i>
{{end}}
{{else if .Verification.Warning}}
<i title="{{$.i18n.Tr .Verification.Reason}}" class="red unlock icon"></i>
<div title="{{if eq .Verification.TrustStatus "trusted"}}{{else if eq .Verification.TrustStatus "untrusted"}}{{$.i18n.Tr "repo.commits.signed_by_untrusted_user"}}: {{else}}{{$.i18n.Tr "repo.commits.signed_by_untrusted_user_unmatched"}}: {{end}}{{.Verification.Reason}}">
{{if ne .Verification.SigningUser.ID 0}}
<i class="lock icon"></i>
<img class="ui signature avatar image" src="{{.Verification.SigningUser.RelAvatarLink}}" />
{{else}}
<i title="{{.Verification.Reason}}" class="icons">
<i class="lock icon"></i>
<i class="tiny inverted cog icon centerlock"></i>
</i>
<img class="ui signature avatar image" src="{{AvatarLink .Verification.SigningEmail}}" />
{{end}}
</div>
{{else}}
<i title="{{$.i18n.Tr .Verification.Reason}}" class="unlock icon"></i>
{{end}}
Expand Down
Loading

0 comments on commit 90919bb

Please sign in to comment.