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>
This commit is contained in:
zeripath 2020-09-19 17:44:55 +01:00 committed by GitHub
parent 89c94e2f8e
commit 4979f15c3f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 441 additions and 139 deletions

View file

@ -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)
@ -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()
@ -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)
}
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"
}
}
}
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...
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
}

View file

@ -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

14
models/migrations/v152.go Normal file
View file

@ -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))
}

View file

@ -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)
@ -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
}

View file

@ -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"`
@ -198,6 +239,8 @@ type Repository struct {
CloseIssuesViaCommitInAnyBranch bool `xorm:"NOT NULL DEFAULT false"`
Topics []string `xorm:"TEXT JSON"`
TrustModel TrustModelType
// Avatar: ID(10-20)-md5(32) - must fit into 64 symbols
Avatar string `xorm:"VARCHAR(64)"`
@ -1038,6 +1081,7 @@ type CreateRepoOptions struct {
IsMirror bool
AutoInit bool
Status RepositoryStatus
TrustModel TrustModelType
}
// GetRepoInitFile returns repository init files
@ -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

View file

@ -31,7 +31,7 @@ const (
func signingModeFromStrings(modeStrings []string) []signingMode {
returnable := make([]signingMode, 0, len(modeStrings))
for _, mode := range modeStrings {
signMode := signingMode(strings.ToLower(mode))
signMode := signingMode(strings.ToLower(strings.TrimSpace(mode)))
switch signMode {
case never:
return []signingMode{never}
@ -59,9 +59,10 @@ func signingModeFromStrings(modeStrings []string) []signingMode {
return returnable
}
func signingKey(repoPath string) string {
// SigningKey returns the KeyID and git Signature for the repo
func SigningKey(repoPath string) (string, *git.Signature) {
if setting.Repository.Signing.SigningKey == "none" {
return ""
return "", nil
}
if setting.Repository.Signing.SigningKey == "default" || setting.Repository.Signing.SigningKey == "" {
@ -69,19 +70,27 @@ func signingKey(repoPath string) string {
value, _ := git.NewCommand("config", "--get", "commit.gpgsign").RunInDir(repoPath)
sign, valid := git.ParseBool(strings.TrimSpace(value))
if !sign || !valid {
return ""
return "", nil
}
signingKey, _ := git.NewCommand("config", "--get", "user.signingkey").RunInDir(repoPath)
return strings.TrimSpace(signingKey)
signingName, _ := git.NewCommand("config", "--get", "user.name").RunInDir(repoPath)
signingEmail, _ := git.NewCommand("config", "--get", "user.email").RunInDir(repoPath)
return strings.TrimSpace(signingKey), &git.Signature{
Name: strings.TrimSpace(signingName),
Email: strings.TrimSpace(signingEmail),
}
}
return setting.Repository.Signing.SigningKey
return setting.Repository.Signing.SigningKey, &git.Signature{
Name: setting.Repository.Signing.SigningName,
Email: setting.Repository.Signing.SigningEmail,
}
}
// PublicSigningKey gets the public signing key within a provided repository directory
func PublicSigningKey(repoPath string) (string, error) {
signingKey := signingKey(repoPath)
signingKey, _ := SigningKey(repoPath)
if signingKey == "" {
return "", nil
}
@ -96,143 +105,143 @@ func PublicSigningKey(repoPath string) (string, error) {
}
// SignInitialCommit determines if we should sign the initial commit to this repository
func SignInitialCommit(repoPath string, u *User) (bool, string, error) {
func SignInitialCommit(repoPath string, u *User) (bool, string, *git.Signature, error) {
rules := signingModeFromStrings(setting.Repository.Signing.InitialCommit)
signingKey := signingKey(repoPath)
signingKey, sig := SigningKey(repoPath)
if signingKey == "" {
return false, "", &ErrWontSign{noKey}
return false, "", nil, &ErrWontSign{noKey}
}
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}
}
}
}
return true, signingKey, nil
return true, signingKey, sig, nil
}
// SignWikiCommit determines if we should sign the commits to this repository wiki
func (repo *Repository) SignWikiCommit(u *User) (bool, string, error) {
func (repo *Repository) SignWikiCommit(u *User) (bool, string, *git.Signature, error) {
rules := signingModeFromStrings(setting.Repository.Signing.Wiki)
signingKey := signingKey(repo.WikiPath())
signingKey, sig := SigningKey(repo.WikiPath())
if signingKey == "" {
return false, "", &ErrWontSign{noKey}
return false, "", nil, &ErrWontSign{noKey}
}
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 parentSigned:
gitRepo, err := git.OpenRepository(repo.WikiPath())
if err != nil {
return false, "", err
return false, "", nil, err
}
defer gitRepo.Close()
commit, err := gitRepo.GetCommit("HEAD")
if err != nil {
return false, "", err
return false, "", nil, err
}
if commit.Signature == nil {
return false, "", &ErrWontSign{parentSigned}
return false, "", nil, &ErrWontSign{parentSigned}
}
verification := ParseCommitWithSignature(commit)
if !verification.Verified {
return false, "", &ErrWontSign{parentSigned}
return false, "", nil, &ErrWontSign{parentSigned}
}
}
}
return true, signingKey, nil
return true, signingKey, sig, nil
}
// SignCRUDAction determines if we should sign a CRUD commit to this repository
func (repo *Repository) SignCRUDAction(u *User, tmpBasePath, parentCommit string) (bool, string, error) {
func (repo *Repository) SignCRUDAction(u *User, tmpBasePath, parentCommit string) (bool, string, *git.Signature, error) {
rules := signingModeFromStrings(setting.Repository.Signing.CRUDActions)
signingKey := signingKey(repo.RepoPath())
signingKey, sig := SigningKey(repo.RepoPath())
if signingKey == "" {
return false, "", &ErrWontSign{noKey}
return false, "", nil, &ErrWontSign{noKey}
}
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 parentSigned:
gitRepo, err := git.OpenRepository(tmpBasePath)
if err != nil {
return false, "", err
return false, "", nil, err
}
defer gitRepo.Close()
commit, err := gitRepo.GetCommit(parentCommit)
if err != nil {
return false, "", err
return false, "", nil, err
}
if commit.Signature == nil {
return false, "", &ErrWontSign{parentSigned}
return false, "", nil, &ErrWontSign{parentSigned}
}
verification := ParseCommitWithSignature(commit)
if !verification.Verified {
return false, "", &ErrWontSign{parentSigned}
return false, "", nil, &ErrWontSign{parentSigned}
}
}
}
return true, signingKey, nil
return true, signingKey, sig, nil
}