Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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.ini.sample
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,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
Comment thread
zeripath marked this conversation as resolved.
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 @@ -98,6 +98,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: 65 additions & 24 deletions models/gpg_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -765,7 +765,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 @@ -774,7 +774,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 @@ -783,31 +783,72 @@ 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 {
func CalculateTrustStatus(verification *CommitVerification, repository *Repository, keyMap *map[string]bool) (err error) {
if !verification.Verified {
return
}

// 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...
Comment thread
zeripath marked this conversation as resolved.

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.
verification.TrustStatus = "trusted"
Comment thread
zeripath marked this conversation as resolved.
Outdated
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"
}
}
// 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
}

verification.TrustStatus = "trusted"
Comment thread
zeripath marked this conversation as resolved.
Outdated

Comment thread
zeripath marked this conversation as resolved.
Outdated
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
}
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 @@ -30,101 +30,101 @@ func (pr *PullRequest) SignMerge(u *User, tmpBasePath, baseCommit, headCommit st
for _, rule := range rules {
switch rule {
case never:
return false, "", &ErrWontSign{never}
return false, "", nil, &ErrWontSign{never}
case always:
break
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 @@ -139,6 +139,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 @@ -191,6 +232,8 @@ type Repository struct {
CloseIssuesViaCommitInAnyBranch bool `xorm:"NOT NULL DEFAULT false"`
Topics []string `xorm:"TEXT JSON"`

TrustModel TrustModelType
Comment thread
zeripath marked this conversation as resolved.

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

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

// GetRepoInitFile returns repository init files
Expand Down Expand Up @@ -2335,3 +2379,15 @@ func updateRepositoryCols(e Engine, repo *Repository, cols ...string) error {
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
}
Loading