Skip to content

Commit 4979f15

Browse files
zeripath6543lunny
authored
Add configurable Trust Models (#11712)
* Add configurable Trust Models Gitea's default signature verification model differs from GitHub. GitHub uses signatures to verify that the committer is who they say they are - meaning that when GitHub makes a signed commit it must be the committer. The GitHub model prevents re-publishing of commits after revocation of a key and prevents re-signing of other people's commits to create a completely trusted repository signed by one key or a set of trusted keys. The default behaviour of Gitea in contrast is to always display the avatar and information related to a signature. This allows signatures to be decoupled from the committer. That being said, allowing arbitary users to present other peoples commits as theirs is not necessarily desired therefore we have a trust model whereby signatures from collaborators are marked trusted, signatures matching the commit line are marked untrusted and signatures that match a user in the db but not the committer line are marked unmatched. The problem with this model is that this conflicts with Github therefore we need to provide an option to allow users to choose the Github model should they wish to. Signed-off-by: Andrew Thornton <art27@cantab.net> * Adjust locale strings Signed-off-by: Andrew Thornton <art27@cantab.net> * as per @6543 Co-authored-by: 6543 <6543@obermui.de> * Update models/gpg_key.go * Add migration for repository Signed-off-by: Andrew Thornton <art27@cantab.net> Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
1 parent 89c94e2 commit 4979f15

29 files changed

Lines changed: 439 additions & 137 deletions

File tree

custom/conf/app.example.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ SIGNING_KEY = default
124124
; by setting the SIGNING_KEY ID to the correct ID.)
125125
SIGNING_NAME =
126126
SIGNING_EMAIL =
127+
; Sets the default trust model for repositories. Options are: collaborator, committer, collaboratorcommitter
128+
DEFAULT_TRUST_MODEL=collaborator
127129
; Determines when gitea should sign the initial commit when creating a repository
128130
; Either:
129131
; - never

docs/content/doc/advanced/config-cheat-sheet.en-us.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
101101
- `twofa`: Only sign if the user is logged in with twofa
102102
- `always`: Always sign
103103
- Options other than `never` and `always` can be combined as a comma separated list.
104+
- `DEFAULT_TRUST_MODEL`: **collaborator**: \[collaborator, committer, collaboratorcommitter\]: The default trust model used for verifying commits.
105+
- `collaborator`: Trust signatures signed by keys of collaborators.
106+
- `committer`: Trust signatures that match committers (This matches GitHub and will force Gitea signed commits to have Gitea as the commmitter).
107+
- `collaboratorcommitter`: Trust signatures signed by keys of collaborators which match the commiter.
104108
- `WIKI`: **never**: \[never, pubkey, twofa, always, parentsigned\]: Sign commits to wiki.
105109
- `CRUD_ACTIONS`: **pubkey, twofa, parentsigned**: \[never, pubkey, twofa, parentsigned, always\]: Sign CRUD actions.
106110
- Options as above, with the addition of:

models/gpg_key.go

Lines changed: 64 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -831,7 +831,7 @@ func ParseCommitsWithSignature(oldCommits *list.List, repository *Repository) *l
831831
newCommits = list.New()
832832
e = oldCommits.Front()
833833
)
834-
memberMap := map[int64]bool{}
834+
keyMap := map[string]bool{}
835835

836836
for e != nil {
837837
c := e.Value.(UserCommit)
@@ -840,7 +840,7 @@ func ParseCommitsWithSignature(oldCommits *list.List, repository *Repository) *l
840840
Verification: ParseCommitWithSignature(c.Commit),
841841
}
842842

843-
_ = CalculateTrustStatus(signCommit.Verification, repository, &memberMap)
843+
_ = CalculateTrustStatus(signCommit.Verification, repository, &keyMap)
844844

845845
newCommits.PushBack(signCommit)
846846
e = e.Next()
@@ -849,31 +849,70 @@ func ParseCommitsWithSignature(oldCommits *list.List, repository *Repository) *l
849849
}
850850

851851
// CalculateTrustStatus will calculate the TrustStatus for a commit verification within a repository
852-
func CalculateTrustStatus(verification *CommitVerification, repository *Repository, memberMap *map[int64]bool) (err error) {
853-
if verification.Verified {
854-
verification.TrustStatus = "trusted"
855-
if verification.SigningUser.ID != 0 {
856-
var isMember bool
857-
if memberMap != nil {
858-
var has bool
859-
isMember, has = (*memberMap)[verification.SigningUser.ID]
860-
if !has {
861-
isMember, err = repository.IsOwnerMemberCollaborator(verification.SigningUser.ID)
862-
(*memberMap)[verification.SigningUser.ID] = isMember
863-
}
864-
} else {
865-
isMember, err = repository.IsOwnerMemberCollaborator(verification.SigningUser.ID)
866-
}
852+
func CalculateTrustStatus(verification *CommitVerification, repository *Repository, keyMap *map[string]bool) (err error) {
853+
if !verification.Verified {
854+
return
855+
}
867856

868-
if !isMember {
869-
verification.TrustStatus = "untrusted"
870-
if verification.CommittingUser.ID != verification.SigningUser.ID {
871-
// The committing user and the signing user are not the same and are not the default key
872-
// This should be marked as questionable unless the signing user is a collaborator/team member etc.
873-
verification.TrustStatus = "unmatched"
874-
}
875-
}
857+
// There are several trust models in Gitea
858+
trustModel := repository.GetTrustModel()
859+
860+
// In the Committer trust model a signature is trusted if it matches the committer
861+
// - it doesn't matter if they're a collaborator, the owner, Gitea or Github
862+
// NB: This model is commit verification only
863+
if trustModel == CommitterTrustModel {
864+
// default to "unmatched"
865+
verification.TrustStatus = "unmatched"
866+
867+
// We can only verify against users in our database but the default key will match
868+
// against by email if it is not in the db.
869+
if (verification.SigningUser.ID != 0 &&
870+
verification.CommittingUser.ID == verification.SigningUser.ID) ||
871+
(verification.SigningUser.ID == 0 && verification.CommittingUser.ID == 0 &&
872+
verification.SigningUser.Email == verification.CommittingUser.Email) {
873+
verification.TrustStatus = "trusted"
876874
}
875+
return
877876
}
877+
878+
// Now we drop to the more nuanced trust models...
879+
verification.TrustStatus = "trusted"
880+
881+
if verification.SigningUser.ID == 0 {
882+
// This commit is signed by the default key - but this key is not assigned to a user in the DB.
883+
884+
// However in the CollaboratorCommitterTrustModel we cannot mark this as trusted
885+
// unless the default key matches the email of a non-user.
886+
if trustModel == CollaboratorCommitterTrustModel && (verification.CommittingUser.ID != 0 ||
887+
verification.SigningUser.Email != verification.CommittingUser.Email) {
888+
verification.TrustStatus = "untrusted"
889+
}
890+
return
891+
}
892+
893+
var isMember bool
894+
if keyMap != nil {
895+
var has bool
896+
isMember, has = (*keyMap)[verification.SigningKey.KeyID]
897+
if !has {
898+
isMember, err = repository.IsOwnerMemberCollaborator(verification.SigningUser.ID)
899+
(*keyMap)[verification.SigningKey.KeyID] = isMember
900+
}
901+
} else {
902+
isMember, err = repository.IsOwnerMemberCollaborator(verification.SigningUser.ID)
903+
}
904+
905+
if !isMember {
906+
verification.TrustStatus = "untrusted"
907+
if verification.CommittingUser.ID != verification.SigningUser.ID {
908+
// The committing user and the signing user are not the same
909+
// This should be marked as questionable unless the signing user is a collaborator/team member etc.
910+
verification.TrustStatus = "unmatched"
911+
}
912+
} else if trustModel == CollaboratorCommitterTrustModel && verification.CommittingUser.ID != verification.SigningUser.ID {
913+
// The committing user and the signing user are not the same and our trustmodel states that they must match
914+
verification.TrustStatus = "unmatched"
915+
}
916+
878917
return
879918
}

models/migrations/migrations.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,8 @@ var migrations = []Migration{
237237
NewMigration("add primary key to repo_topic", addPrimaryKeyToRepoTopic),
238238
// v151 -> v152
239239
NewMigration("set default password algorithm to Argon2", setDefaultPasswordToArgon2),
240+
// v152 -> v153
241+
NewMigration("add TrustModel field to Repository", addTrustModelToRepository),
240242
}
241243

242244
// GetCurrentDBVersion returns the current db version

models/migrations/v152.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright 2020 The Gitea Authors. All rights reserved.
2+
// Use of this source code is governed by a MIT-style
3+
// license that can be found in the LICENSE file.
4+
5+
package migrations
6+
7+
import "xorm.io/xorm"
8+
9+
func addTrustModelToRepository(x *xorm.Engine) error {
10+
type Repository struct {
11+
TrustModel int
12+
}
13+
return x.Sync2(new(Repository))
14+
}

models/pull_sign.go

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,16 @@ import (
1111
)
1212

1313
// SignMerge determines if we should sign a PR merge commit to the base repository
14-
func (pr *PullRequest) SignMerge(u *User, tmpBasePath, baseCommit, headCommit string) (bool, string, error) {
14+
func (pr *PullRequest) SignMerge(u *User, tmpBasePath, baseCommit, headCommit string) (bool, string, *git.Signature, error) {
1515
if err := pr.LoadBaseRepo(); err != nil {
1616
log.Error("Unable to get Base Repo for pull request")
17-
return false, "", err
17+
return false, "", nil, err
1818
}
1919
repo := pr.BaseRepo
2020

21-
signingKey := signingKey(repo.RepoPath())
21+
signingKey, signer := SigningKey(repo.RepoPath())
2222
if signingKey == "" {
23-
return false, "", &ErrWontSign{noKey}
23+
return false, "", nil, &ErrWontSign{noKey}
2424
}
2525
rules := signingModeFromStrings(setting.Repository.Signing.Merges)
2626

@@ -31,101 +31,101 @@ Loop:
3131
for _, rule := range rules {
3232
switch rule {
3333
case never:
34-
return false, "", &ErrWontSign{never}
34+
return false, "", nil, &ErrWontSign{never}
3535
case always:
3636
break Loop
3737
case pubkey:
3838
keys, err := ListGPGKeys(u.ID, ListOptions{})
3939
if err != nil {
40-
return false, "", err
40+
return false, "", nil, err
4141
}
4242
if len(keys) == 0 {
43-
return false, "", &ErrWontSign{pubkey}
43+
return false, "", nil, &ErrWontSign{pubkey}
4444
}
4545
case twofa:
4646
twofaModel, err := GetTwoFactorByUID(u.ID)
4747
if err != nil && !IsErrTwoFactorNotEnrolled(err) {
48-
return false, "", err
48+
return false, "", nil, err
4949
}
5050
if twofaModel == nil {
51-
return false, "", &ErrWontSign{twofa}
51+
return false, "", nil, &ErrWontSign{twofa}
5252
}
5353
case approved:
5454
protectedBranch, err := GetProtectedBranchBy(repo.ID, pr.BaseBranch)
5555
if err != nil {
56-
return false, "", err
56+
return false, "", nil, err
5757
}
5858
if protectedBranch == nil {
59-
return false, "", &ErrWontSign{approved}
59+
return false, "", nil, &ErrWontSign{approved}
6060
}
6161
if protectedBranch.GetGrantedApprovalsCount(pr) < 1 {
62-
return false, "", &ErrWontSign{approved}
62+
return false, "", nil, &ErrWontSign{approved}
6363
}
6464
case baseSigned:
6565
if gitRepo == nil {
6666
gitRepo, err = git.OpenRepository(tmpBasePath)
6767
if err != nil {
68-
return false, "", err
68+
return false, "", nil, err
6969
}
7070
defer gitRepo.Close()
7171
}
7272
commit, err := gitRepo.GetCommit(baseCommit)
7373
if err != nil {
74-
return false, "", err
74+
return false, "", nil, err
7575
}
7676
verification := ParseCommitWithSignature(commit)
7777
if !verification.Verified {
78-
return false, "", &ErrWontSign{baseSigned}
78+
return false, "", nil, &ErrWontSign{baseSigned}
7979
}
8080
case headSigned:
8181
if gitRepo == nil {
8282
gitRepo, err = git.OpenRepository(tmpBasePath)
8383
if err != nil {
84-
return false, "", err
84+
return false, "", nil, err
8585
}
8686
defer gitRepo.Close()
8787
}
8888
commit, err := gitRepo.GetCommit(headCommit)
8989
if err != nil {
90-
return false, "", err
90+
return false, "", nil, err
9191
}
9292
verification := ParseCommitWithSignature(commit)
9393
if !verification.Verified {
94-
return false, "", &ErrWontSign{headSigned}
94+
return false, "", nil, &ErrWontSign{headSigned}
9595
}
9696
case commitsSigned:
9797
if gitRepo == nil {
9898
gitRepo, err = git.OpenRepository(tmpBasePath)
9999
if err != nil {
100-
return false, "", err
100+
return false, "", nil, err
101101
}
102102
defer gitRepo.Close()
103103
}
104104
commit, err := gitRepo.GetCommit(headCommit)
105105
if err != nil {
106-
return false, "", err
106+
return false, "", nil, err
107107
}
108108
verification := ParseCommitWithSignature(commit)
109109
if !verification.Verified {
110-
return false, "", &ErrWontSign{commitsSigned}
110+
return false, "", nil, &ErrWontSign{commitsSigned}
111111
}
112112
// need to work out merge-base
113113
mergeBaseCommit, _, err := gitRepo.GetMergeBase("", baseCommit, headCommit)
114114
if err != nil {
115-
return false, "", err
115+
return false, "", nil, err
116116
}
117117
commitList, err := commit.CommitsBeforeUntil(mergeBaseCommit)
118118
if err != nil {
119-
return false, "", err
119+
return false, "", nil, err
120120
}
121121
for e := commitList.Front(); e != nil; e = e.Next() {
122122
commit = e.Value.(*git.Commit)
123123
verification := ParseCommitWithSignature(commit)
124124
if !verification.Verified {
125-
return false, "", &ErrWontSign{commitsSigned}
125+
return false, "", nil, &ErrWontSign{commitsSigned}
126126
}
127127
}
128128
}
129129
}
130-
return true, signingKey, nil
130+
return true, signingKey, signer, nil
131131
}

models/repo.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,47 @@ const (
143143
RepositoryBeingMigrated // repository is migrating
144144
)
145145

146+
// TrustModelType defines the types of trust model for this repository
147+
type TrustModelType int
148+
149+
// kinds of TrustModel
150+
const (
151+
DefaultTrustModel TrustModelType = iota // default trust model
152+
CommitterTrustModel
153+
CollaboratorTrustModel
154+
CollaboratorCommitterTrustModel
155+
)
156+
157+
// String converts a TrustModelType to a string
158+
func (t TrustModelType) String() string {
159+
switch t {
160+
case DefaultTrustModel:
161+
return "default"
162+
case CommitterTrustModel:
163+
return "committer"
164+
case CollaboratorTrustModel:
165+
return "collaborator"
166+
case CollaboratorCommitterTrustModel:
167+
return "collaboratorcommitter"
168+
}
169+
return "default"
170+
}
171+
172+
// ToTrustModel converts a string to a TrustModelType
173+
func ToTrustModel(model string) TrustModelType {
174+
switch strings.ToLower(strings.TrimSpace(model)) {
175+
case "default":
176+
return DefaultTrustModel
177+
case "collaborator":
178+
return CollaboratorTrustModel
179+
case "committer":
180+
return CommitterTrustModel
181+
case "collaboratorcommitter":
182+
return CollaboratorCommitterTrustModel
183+
}
184+
return DefaultTrustModel
185+
}
186+
146187
// Repository represents a git repository.
147188
type Repository struct {
148189
ID int64 `xorm:"pk autoincr"`
@@ -198,6 +239,8 @@ type Repository struct {
198239
CloseIssuesViaCommitInAnyBranch bool `xorm:"NOT NULL DEFAULT false"`
199240
Topics []string `xorm:"TEXT JSON"`
200241

242+
TrustModel TrustModelType
243+
201244
// Avatar: ID(10-20)-md5(32) - must fit into 64 symbols
202245
Avatar string `xorm:"VARCHAR(64)"`
203246

@@ -1038,6 +1081,7 @@ type CreateRepoOptions struct {
10381081
IsMirror bool
10391082
AutoInit bool
10401083
Status RepositoryStatus
1084+
TrustModel TrustModelType
10411085
}
10421086

10431087
// GetRepoInitFile returns repository init files
@@ -2383,6 +2427,18 @@ func UpdateRepositoryCols(repo *Repository, cols ...string) error {
23832427
return updateRepositoryCols(x, repo, cols...)
23842428
}
23852429

2430+
// GetTrustModel will get the TrustModel for the repo or the default trust model
2431+
func (repo *Repository) GetTrustModel() TrustModelType {
2432+
trustModel := repo.TrustModel
2433+
if trustModel == DefaultTrustModel {
2434+
trustModel = ToTrustModel(setting.Repository.Signing.DefaultTrustModel)
2435+
if trustModel == DefaultTrustModel {
2436+
return CollaboratorTrustModel
2437+
}
2438+
}
2439+
return trustModel
2440+
}
2441+
23862442
// DoctorUserStarNum recalculate Stars number for all user
23872443
func DoctorUserStarNum() (err error) {
23882444
const batchSize = 100

0 commit comments

Comments
 (0)