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

Add configurable Trust Models #11712

Merged
merged 16 commits into from
Sep 19, 2020
Merged
Show file tree
Hide file tree
Changes from 15 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
2 changes: 2 additions & 0 deletions custom/conf/app.example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ SIGNING_KEY = default
; by setting the SIGNING_KEY ID to the correct ID.)
SIGNING_NAME =
SIGNING_EMAIL =
; Sets the default trust model for repositories. Options are: collaborator, committer, collaboratorcommitter
DEFAULT_TRUST_MODEL=collaborator
; Determines when gitea should sign the initial commit when creating a repository
; Either:
; - never
Expand Down
4 changes: 4 additions & 0 deletions docs/content/doc/advanced/config-cheat-sheet.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,10 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
- `twofa`: Only sign if the user is logged in with twofa
- `always`: Always sign
- Options other than `never` and `always` can be combined as a comma separated list.
- `DEFAULT_TRUST_MODEL`: **collaborator**: \[collaborator, committer, collaboratorcommitter\]: The default trust model used for verifying commits.
- `collaborator`: Trust signatures signed by keys of collaborators.
- `committer`: Trust signatures that match committers (This matches GitHub and will force Gitea signed commits to have Gitea as the commmitter).
- `collaboratorcommitter`: Trust signatures signed by keys of collaborators which match the commiter.
- `WIKI`: **never**: \[never, pubkey, twofa, always, parentsigned\]: Sign commits to wiki.
- `CRUD_ACTIONS`: **pubkey, twofa, parentsigned**: \[never, pubkey, twofa, parentsigned, always\]: Sign CRUD actions.
- Options as above, with the addition of:
Expand Down
89 changes: 64 additions & 25 deletions models/gpg_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -831,7 +831,7 @@ func ParseCommitsWithSignature(oldCommits *list.List, repository *Repository) *l
newCommits = list.New()
e = oldCommits.Front()
)
memberMap := map[int64]bool{}
keyMap := map[string]bool{}

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

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

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

// 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)
}
func CalculateTrustStatus(verification *CommitVerification, repository *Repository, keyMap *map[string]bool) (err error) {
if !verification.Verified {
return
}

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"
}
}
// There are several trust models in Gitea
trustModel := repository.GetTrustModel()

// In the Committer trust model a signature is trusted if it matches the committer
// - it doesn't matter if they're a collaborator, the owner, Gitea or Github
// NB: This model is commit verification only
if trustModel == CommitterTrustModel {
// default to "unmatched"
verification.TrustStatus = "unmatched"

// We can only verify against users in our database but the default key will match
// against by email if it is not in the db.
if (verification.SigningUser.ID != 0 &&
verification.CommittingUser.ID == verification.SigningUser.ID) ||
(verification.SigningUser.ID == 0 && verification.CommittingUser.ID == 0 &&
verification.SigningUser.Email == verification.CommittingUser.Email) {
verification.TrustStatus = "trusted"
}
return
}

// Now we drop to the more nuanced trust models...
zeripath marked this conversation as resolved.
Show resolved Hide resolved
verification.TrustStatus = "trusted"

if verification.SigningUser.ID == 0 {
// This commit is signed by the default key - but this key is not assigned to a user in the DB.

// However in the CollaboratorCommitterTrustModel we cannot mark this as trusted
// unless the default key matches the email of a non-user.
if trustModel == CollaboratorCommitterTrustModel && (verification.CommittingUser.ID != 0 ||
verification.SigningUser.Email != verification.CommittingUser.Email) {
verification.TrustStatus = "untrusted"
}
return
}

var isMember bool
if keyMap != nil {
var has bool
isMember, has = (*keyMap)[verification.SigningKey.KeyID]
if !has {
isMember, err = repository.IsOwnerMemberCollaborator(verification.SigningUser.ID)
(*keyMap)[verification.SigningKey.KeyID] = 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
// This should be marked as questionable unless the signing user is a collaborator/team member etc.
verification.TrustStatus = "unmatched"
}
} else if trustModel == CollaboratorCommitterTrustModel && verification.CommittingUser.ID != verification.SigningUser.ID {
// The committing user and the signing user are not the same and our trustmodel states that they must match
verification.TrustStatus = "unmatched"
}

return
}
2 changes: 2 additions & 0 deletions models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,8 @@ var migrations = []Migration{
NewMigration("add primary key to repo_topic", addPrimaryKeyToRepoTopic),
// v151 -> v152
NewMigration("set default password algorithm to Argon2", setDefaultPasswordToArgon2),
// v152 -> v153
NewMigration("add TrustModel field to Repository", addTrustModelToRepository),
}

// GetCurrentDBVersion returns the current db version
Expand Down
14 changes: 14 additions & 0 deletions models/migrations/v152.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright 2020 The Gitea Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.

package migrations

import "xorm.io/xorm"

func addTrustModelToRepository(x *xorm.Engine) error {
type Repository struct {
TrustModel int
}
return x.Sync2(new(Repository))
}
50 changes: 25 additions & 25 deletions models/pull_sign.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,16 @@ import (
)

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

signingKey := signingKey(repo.RepoPath())
signingKey, signer := SigningKey(repo.RepoPath())
if signingKey == "" {
return false, "", &ErrWontSign{noKey}
return false, "", nil, &ErrWontSign{noKey}
}
rules := signingModeFromStrings(setting.Repository.Signing.Merges)

Expand All @@ -31,101 +31,101 @@ Loop:
for _, rule := range rules {
switch rule {
case never:
return false, "", &ErrWontSign{never}
return false, "", nil, &ErrWontSign{never}
case always:
break Loop
case pubkey:
keys, err := ListGPGKeys(u.ID, ListOptions{})
if err != nil {
return false, "", err
return false, "", nil, err
}
if len(keys) == 0 {
return false, "", &ErrWontSign{pubkey}
return false, "", nil, &ErrWontSign{pubkey}
}
case twofa:
twofaModel, err := GetTwoFactorByUID(u.ID)
if err != nil && !IsErrTwoFactorNotEnrolled(err) {
return false, "", err
return false, "", nil, err
}
if twofaModel == nil {
return false, "", &ErrWontSign{twofa}
return false, "", nil, &ErrWontSign{twofa}
}
case approved:
protectedBranch, err := GetProtectedBranchBy(repo.ID, pr.BaseBranch)
if err != nil {
return false, "", err
return false, "", nil, err
}
if protectedBranch == nil {
return false, "", &ErrWontSign{approved}
return false, "", nil, &ErrWontSign{approved}
}
if protectedBranch.GetGrantedApprovalsCount(pr) < 1 {
return false, "", &ErrWontSign{approved}
return false, "", nil, &ErrWontSign{approved}
}
case baseSigned:
if gitRepo == nil {
gitRepo, err = git.OpenRepository(tmpBasePath)
if err != nil {
return false, "", err
return false, "", nil, err
}
defer gitRepo.Close()
}
commit, err := gitRepo.GetCommit(baseCommit)
if err != nil {
return false, "", err
return false, "", nil, err
}
verification := ParseCommitWithSignature(commit)
if !verification.Verified {
return false, "", &ErrWontSign{baseSigned}
return false, "", nil, &ErrWontSign{baseSigned}
}
case headSigned:
if gitRepo == nil {
gitRepo, err = git.OpenRepository(tmpBasePath)
if err != nil {
return false, "", err
return false, "", nil, err
}
defer gitRepo.Close()
}
commit, err := gitRepo.GetCommit(headCommit)
if err != nil {
return false, "", err
return false, "", nil, err
}
verification := ParseCommitWithSignature(commit)
if !verification.Verified {
return false, "", &ErrWontSign{headSigned}
return false, "", nil, &ErrWontSign{headSigned}
}
case commitsSigned:
if gitRepo == nil {
gitRepo, err = git.OpenRepository(tmpBasePath)
if err != nil {
return false, "", err
return false, "", nil, err
}
defer gitRepo.Close()
}
commit, err := gitRepo.GetCommit(headCommit)
if err != nil {
return false, "", err
return false, "", nil, err
}
verification := ParseCommitWithSignature(commit)
if !verification.Verified {
return false, "", &ErrWontSign{commitsSigned}
return false, "", nil, &ErrWontSign{commitsSigned}
}
// need to work out merge-base
mergeBaseCommit, _, err := gitRepo.GetMergeBase("", baseCommit, headCommit)
if err != nil {
return false, "", err
return false, "", nil, err
}
commitList, err := commit.CommitsBeforeUntil(mergeBaseCommit)
if err != nil {
return false, "", err
return false, "", nil, err
}
for e := commitList.Front(); e != nil; e = e.Next() {
commit = e.Value.(*git.Commit)
verification := ParseCommitWithSignature(commit)
if !verification.Verified {
return false, "", &ErrWontSign{commitsSigned}
return false, "", nil, &ErrWontSign{commitsSigned}
}
}
}
}
return true, signingKey, nil
return true, signingKey, signer, nil
}
56 changes: 56 additions & 0 deletions models/repo.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,47 @@ const (
RepositoryBeingMigrated // repository is migrating
)

// TrustModelType defines the types of trust model for this repository
type TrustModelType int

// kinds of TrustModel
const (
DefaultTrustModel TrustModelType = iota // default trust model
CommitterTrustModel
CollaboratorTrustModel
CollaboratorCommitterTrustModel
)

// String converts a TrustModelType to a string
func (t TrustModelType) String() string {
switch t {
case DefaultTrustModel:
return "default"
case CommitterTrustModel:
return "committer"
case CollaboratorTrustModel:
return "collaborator"
case CollaboratorCommitterTrustModel:
return "collaboratorcommitter"
}
return "default"
}

// ToTrustModel converts a string to a TrustModelType
func ToTrustModel(model string) TrustModelType {
switch strings.ToLower(strings.TrimSpace(model)) {
case "default":
return DefaultTrustModel
case "collaborator":
return CollaboratorTrustModel
case "committer":
return CommitterTrustModel
case "collaboratorcommitter":
return CollaboratorCommitterTrustModel
}
return DefaultTrustModel
}

// Repository represents a git repository.
type Repository struct {
ID int64 `xorm:"pk autoincr"`
Expand Down Expand Up @@ -198,6 +239,8 @@ type Repository struct {
CloseIssuesViaCommitInAnyBranch bool `xorm:"NOT NULL DEFAULT false"`
Topics []string `xorm:"TEXT JSON"`

TrustModel TrustModelType
zeripath marked this conversation as resolved.
Show resolved Hide resolved

// Avatar: ID(10-20)-md5(32) - must fit into 64 symbols
Avatar string `xorm:"VARCHAR(64)"`

Expand Down Expand Up @@ -1038,6 +1081,7 @@ type CreateRepoOptions struct {
IsMirror bool
AutoInit bool
Status RepositoryStatus
TrustModel TrustModelType
}

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

// GetTrustModel will get the TrustModel for the repo or the default trust model
func (repo *Repository) GetTrustModel() TrustModelType {
trustModel := repo.TrustModel
if trustModel == DefaultTrustModel {
trustModel = ToTrustModel(setting.Repository.Signing.DefaultTrustModel)
if trustModel == DefaultTrustModel {
return CollaboratorTrustModel
}
}
return trustModel
}

// DoctorUserStarNum recalculate Stars number for all user
func DoctorUserStarNum() (err error) {
const batchSize = 100
Expand Down
Loading