diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index af3418f70..ffc4e4067 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -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 diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md index 7f969add2..1e48ee259 100644 --- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md +++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md @@ -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: diff --git a/models/gpg_key.go b/models/gpg_key.go index 662eac939..b944fdcbf 100644 --- a/models/gpg_key.go +++ b/models/gpg_key.go @@ -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) - } +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... + 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 } diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 5317cc574..ea1bf5964 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -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 diff --git a/models/migrations/v152.go b/models/migrations/v152.go new file mode 100644 index 000000000..f71f71e22 --- /dev/null +++ b/models/migrations/v152.go @@ -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)) +} diff --git a/models/pull_sign.go b/models/pull_sign.go index ab61232d6..10a6522eb 100644 --- a/models/pull_sign.go +++ b/models/pull_sign.go @@ -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 } diff --git a/models/repo.go b/models/repo.go index 2bbc74f43..25fe3f63d 100644 --- a/models/repo.go +++ b/models/repo.go @@ -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 diff --git a/models/repo_sign.go b/models/repo_sign.go index c9dd5ea4d..be9309ed4 100644 --- a/models/repo_sign.go +++ b/models/repo_sign.go @@ -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 } diff --git a/modules/auth/repo_form.go b/modules/auth/repo_form.go index 3ad57085b..f1130f372 100644 --- a/modules/auth/repo_form.go +++ b/modules/auth/repo_form.go @@ -45,6 +45,7 @@ type CreateRepoForm struct { Webhooks bool Avatar bool Labels bool + TrustModel string } // Validate validates the fields @@ -142,6 +143,9 @@ type RepoSettingForm struct { EnableIssueDependencies bool IsArchived bool + // Signing Settings + TrustModel string + // Admin settings EnableHealthCheck bool EnableCloseIssuesViaCommitInAnyBranch bool diff --git a/modules/context/repo.go b/modules/context/repo.go index 2c7736146..cb2e60d26 100644 --- a/modules/context/repo.go +++ b/modules/context/repo.go @@ -114,7 +114,7 @@ func (r *Repository) CanCommitToBranch(doer *models.User) (CanCommitToBranchResu requireSigned = protectedBranch.RequireSignedCommits } - sign, keyID, err := r.Repository.SignCRUDAction(doer, r.Repository.RepoPath(), git.BranchPrefix+r.BranchName) + sign, keyID, _, err := r.Repository.SignCRUDAction(doer, r.Repository.RepoPath(), git.BranchPrefix+r.BranchName) canCommit := r.CanEnableEditor() && userCanPush if requireSigned { diff --git a/modules/git/repo_tree.go b/modules/git/repo_tree.go index a662aaab4..57896318a 100644 --- a/modules/git/repo_tree.go +++ b/modules/git/repo_tree.go @@ -62,7 +62,7 @@ type CommitTreeOpts struct { } // CommitTree creates a commit from a given tree id for the user with provided message -func (repo *Repository) CommitTree(sig *Signature, tree *Tree, opts CommitTreeOpts) (SHA1, error) { +func (repo *Repository) CommitTree(author *Signature, committer *Signature, tree *Tree, opts CommitTreeOpts) (SHA1, error) { err := LoadGitVersion() if err != nil { return SHA1{}, err @@ -72,11 +72,11 @@ func (repo *Repository) CommitTree(sig *Signature, tree *Tree, opts CommitTreeOp // Because this may call hooks we should pass in the environment env := append(os.Environ(), - "GIT_AUTHOR_NAME="+sig.Name, - "GIT_AUTHOR_EMAIL="+sig.Email, + "GIT_AUTHOR_NAME="+author.Name, + "GIT_AUTHOR_EMAIL="+author.Email, "GIT_AUTHOR_DATE="+commitTimeStr, - "GIT_COMMITTER_NAME="+sig.Name, - "GIT_COMMITTER_EMAIL="+sig.Email, + "GIT_COMMITTER_NAME="+committer.Name, + "GIT_COMMITTER_EMAIL="+committer.Email, "GIT_COMMITTER_DATE="+commitTimeStr, ) cmd := NewCommand("commit-tree", tree.ID.String()) diff --git a/modules/repofiles/delete.go b/modules/repofiles/delete.go index 2ffc75e7c..8343776c4 100644 --- a/modules/repofiles/delete.go +++ b/modules/repofiles/delete.go @@ -67,7 +67,7 @@ func DeleteRepoFile(repo *models.Repository, doer *models.User, opts *DeleteRepo } } if protectedBranch.RequireSignedCommits { - _, _, err := repo.SignCRUDAction(doer, repo.RepoPath(), opts.OldBranch) + _, _, _, err := repo.SignCRUDAction(doer, repo.RepoPath(), opts.OldBranch) if err != nil { if !models.IsErrWontSign(err) { return nil, err diff --git a/modules/repofiles/temp_repo.go b/modules/repofiles/temp_repo.go index ec671a932..e0d6c9fcb 100644 --- a/modules/repofiles/temp_repo.go +++ b/modules/repofiles/temp_repo.go @@ -204,8 +204,6 @@ func (t *TemporaryUploadRepository) CommitTreeWithDate(author, committer *models "GIT_AUTHOR_NAME="+authorSig.Name, "GIT_AUTHOR_EMAIL="+authorSig.Email, "GIT_AUTHOR_DATE="+authorDate.Format(time.RFC3339), - "GIT_COMMITTER_NAME="+committerSig.Name, - "GIT_COMMITTER_EMAIL="+committerSig.Email, "GIT_COMMITTER_DATE="+committerDate.Format(time.RFC3339), ) @@ -217,14 +215,32 @@ func (t *TemporaryUploadRepository) CommitTreeWithDate(author, committer *models // Determine if we should sign if git.CheckGitVersionConstraint(">= 1.7.9") == nil { - sign, keyID, _ := t.repo.SignCRUDAction(author, t.basePath, "HEAD") + sign, keyID, signer, _ := t.repo.SignCRUDAction(author, t.basePath, "HEAD") if sign { args = append(args, "-S"+keyID) + if t.repo.GetTrustModel() == models.CommitterTrustModel || t.repo.GetTrustModel() == models.CollaboratorCommitterTrustModel { + if committerSig.Name != authorSig.Name || committerSig.Email != authorSig.Email { + // Add trailers + _, _ = messageBytes.WriteString("\n") + _, _ = messageBytes.WriteString("Co-Authored-By: ") + _, _ = messageBytes.WriteString(committerSig.String()) + _, _ = messageBytes.WriteString("\n") + _, _ = messageBytes.WriteString("Co-Committed-By: ") + _, _ = messageBytes.WriteString(committerSig.String()) + _, _ = messageBytes.WriteString("\n") + } + committerSig = signer + } } else if git.CheckGitVersionConstraint(">= 2.0.0") == nil { args = append(args, "--no-gpg-sign") } } + env = append(env, + "GIT_COMMITTER_NAME="+committerSig.Name, + "GIT_COMMITTER_EMAIL="+committerSig.Email, + ) + stdout := new(bytes.Buffer) stderr := new(bytes.Buffer) if err := git.NewCommand(args...).RunInDirTimeoutEnvFullPipeline(env, -1, t.basePath, stdout, stderr, messageBytes); err != nil { diff --git a/modules/repofiles/update.go b/modules/repofiles/update.go index dcb87ec92..f7fa5f028 100644 --- a/modules/repofiles/update.go +++ b/modules/repofiles/update.go @@ -161,7 +161,7 @@ func CreateOrUpdateRepoFile(repo *models.Repository, doer *models.User, opts *Up } } if protectedBranch.RequireSignedCommits { - _, _, err := repo.SignCRUDAction(doer, repo.RepoPath(), opts.OldBranch) + _, _, _, err := repo.SignCRUDAction(doer, repo.RepoPath(), opts.OldBranch) if err != nil { if !models.IsErrWontSign(err) { return nil, err diff --git a/modules/repository/create.go b/modules/repository/create.go index abbec05a3..c180b9b94 100644 --- a/modules/repository/create.go +++ b/modules/repository/create.go @@ -41,6 +41,7 @@ func CreateRepository(doer, u *models.User, opts models.CreateRepoOptions) (_ *m CloseIssuesViaCommitInAnyBranch: setting.Repository.DefaultCloseIssuesViaCommitsInAnyBranch, Status: opts.Status, IsEmpty: !opts.AutoInit, + TrustModel: opts.TrustModel, } err = models.WithTx(func(ctx models.DBContext) error { diff --git a/modules/repository/generate.go b/modules/repository/generate.go index c5fb0af38..1314464a6 100644 --- a/modules/repository/generate.go +++ b/modules/repository/generate.go @@ -243,6 +243,7 @@ func GenerateRepository(ctx models.DBContext, doer, owner *models.User, template IsEmpty: !opts.GitContent || templateRepo.IsEmpty, IsFsckEnabled: templateRepo.IsFsckEnabled, TemplateID: templateRepo.ID, + TrustModel: templateRepo.TrustModel, } if err = models.CreateRepository(ctx, doer, owner, generateRepo); err != nil { diff --git a/modules/repository/init.go b/modules/repository/init.go index c2038e18d..d066544a8 100644 --- a/modules/repository/init.go +++ b/modules/repository/init.go @@ -109,10 +109,10 @@ func initRepoCommit(tmpPath string, repo *models.Repository, u *models.User, def "GIT_AUTHOR_NAME="+sig.Name, "GIT_AUTHOR_EMAIL="+sig.Email, "GIT_AUTHOR_DATE="+commitTimeStr, - "GIT_COMMITTER_NAME="+sig.Name, - "GIT_COMMITTER_EMAIL="+sig.Email, "GIT_COMMITTER_DATE="+commitTimeStr, ) + committerName := sig.Name + committerEmail := sig.Email if stdout, err := git.NewCommand("add", "--all"). SetDescription(fmt.Sprintf("initRepoCommit (git add): %s", tmpPath)). @@ -132,14 +132,25 @@ func initRepoCommit(tmpPath string, repo *models.Repository, u *models.User, def } if git.CheckGitVersionConstraint(">= 1.7.9") == nil { - sign, keyID, _ := models.SignInitialCommit(tmpPath, u) + sign, keyID, signer, _ := models.SignInitialCommit(tmpPath, u) if sign { args = append(args, "-S"+keyID) + + if repo.GetTrustModel() == models.CommitterTrustModel || repo.GetTrustModel() == models.CollaboratorCommitterTrustModel { + // need to set the committer to the KeyID owner + committerName = signer.Name + committerEmail = signer.Email + } } else if git.CheckGitVersionConstraint(">= 2.0.0") == nil { args = append(args, "--no-gpg-sign") } } + env = append(env, + "GIT_COMMITTER_NAME="+committerName, + "GIT_COMMITTER_EMAIL="+committerEmail, + ) + if stdout, err := git.NewCommand(args...). SetDescription(fmt.Sprintf("initRepoCommit (git commit): %s", tmpPath)). RunInDirWithEnv(tmpPath, env); err != nil { diff --git a/modules/setting/repository.go b/modules/setting/repository.go index eb1501d7b..67dd80535 100644 --- a/modules/setting/repository.go +++ b/modules/setting/repository.go @@ -83,13 +83,14 @@ var ( } `ini:"repository.issue"` Signing struct { - SigningKey string - SigningName string - SigningEmail string - InitialCommit []string - CRUDActions []string `ini:"CRUD_ACTIONS"` - Merges []string - Wiki []string + SigningKey string + SigningName string + SigningEmail string + InitialCommit []string + CRUDActions []string `ini:"CRUD_ACTIONS"` + Merges []string + Wiki []string + DefaultTrustModel string } `ini:"repository.signing"` }{ DetectedCharsetsOrder: []string{ @@ -209,21 +210,23 @@ var ( // Signing settings Signing: struct { - SigningKey string - SigningName string - SigningEmail string - InitialCommit []string - CRUDActions []string `ini:"CRUD_ACTIONS"` - Merges []string - Wiki []string + SigningKey string + SigningName string + SigningEmail string + InitialCommit []string + CRUDActions []string `ini:"CRUD_ACTIONS"` + Merges []string + Wiki []string + DefaultTrustModel string }{ - SigningKey: "default", - SigningName: "", - SigningEmail: "", - InitialCommit: []string{"always"}, - CRUDActions: []string{"pubkey", "twofa", "parentsigned"}, - Merges: []string{"pubkey", "twofa", "basesigned", "commitssigned"}, - Wiki: []string{"never"}, + SigningKey: "default", + SigningName: "", + SigningEmail: "", + InitialCommit: []string{"always"}, + CRUDActions: []string{"pubkey", "twofa", "parentsigned"}, + Merges: []string{"pubkey", "twofa", "basesigned", "commitssigned"}, + Wiki: []string{"never"}, + DefaultTrustModel: "collaborator", }, } RepoRootPath string @@ -268,6 +271,13 @@ func newRepository() { log.Fatal("Failed to map Repository.PullRequest settings: %v", err) } + // Handle default trustmodel settings + Repository.Signing.DefaultTrustModel = strings.ToLower(strings.TrimSpace(Repository.Signing.DefaultTrustModel)) + if Repository.Signing.DefaultTrustModel == "default" { + Repository.Signing.DefaultTrustModel = "collaborator" + } + + // Handle preferred charset orders preferred := make([]string, 0, len(Repository.DetectedCharsetsOrder)) for _, charset := range Repository.DetectedCharsetsOrder { canonicalCharset := strings.ToLower(strings.TrimSpace(charset)) diff --git a/modules/structs/repo.go b/modules/structs/repo.go index c57702b28..c86b19dfd 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -117,6 +117,9 @@ type CreateRepoOption struct { Readme string `json:"readme"` // DefaultBranch of the repository (used when initializes and in template) DefaultBranch string `json:"default_branch" binding:"GitRefName;MaxSize(100)"` + // TrustModel of the repository + // enum: default,collaborator,committer,collaboratorcommitter + TrustModel string `json:"trust_model"` } // EditRepoOption options when editing a repository's properties diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 376a63125..ea76d4cb6 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1464,6 +1464,19 @@ settings.transfer_desc = Transfer this repository to a user or to an organizatio settings.transfer_notices_1 = - You will lose access to the repository if you transfer it to an individual user. settings.transfer_notices_2 = - You will keep access to the repository if you transfer it to an organization that you (co-)own. settings.transfer_form_title = Enter the repository name as confirmation: +settings.signing_settings = Signing Verification Settings +settings.trust_model = Signature Trust Model +settings.trust_model.default = Default Trust Model +settings.trust_model.default.desc= Use the default repository trust model for this installation. +settings.trust_model.collaborator = Collaborator +settings.trust_model.collaborator.long = Collaborator: Trust signatures by collaborators +settings.trust_model.collaborator.desc = Valid signatures by collaborators of this repository will be marked "trusted" - (whether they match the committer or not). Otherwise, valid signatures will be marked "untrusted" if the signature matches the committer and "unmatched" if not. +settings.trust_model.committer = Committer +settings.trust_model.committer.long = Committer: Trust signatures that match committers (This matches GitHub and will force Gitea signed commits to have Gitea as the committer) +settings.trust_model.committer.desc = Valid signatures will only be marked "trusted" if they match the committer, otherwise they will be marked "unmatched". This will force Gitea to be the committer on signed commits with the actual committer marked as Co-Authored-By: and Co-Committed-By: trailer in the commit. The default Gitea key must match a User in the database. +settings.trust_model.collaboratorcommitter = Collaborator+Committer +settings.trust_model.collaboratorcommitter.long = Collaborator+Committer: Trust signatures by collaborators which match the committer +settings.trust_model.collaboratorcommitter.desc = Valid signatures by collaborators of this repository will be marked "trusted" if they match the committer. Otherwise, valid signatures will be marked "untrusted" if the signature matches the committer and "unmatched" otherwise. This will force Gitea to be marked as the committer on signed commits with the actual committer marked as Co-Authored-By: and Co-Committed-By: trailer in the commit. The default Gitea key must match a User in the database, settings.wiki_delete = Delete Wiki Data settings.wiki_delete_desc = Deleting repository wiki data is permanent and cannot be undone. settings.wiki_delete_notices_1 = - This will permanently delete and disable the repository wiki for %s. diff --git a/routers/api/v1/repo/repo.go b/routers/api/v1/repo/repo.go index 603187c16..baf8110a1 100644 --- a/routers/api/v1/repo/repo.go +++ b/routers/api/v1/repo/repo.go @@ -244,6 +244,7 @@ func CreateUserRepo(ctx *context.APIContext, owner *models.User, opt api.CreateR IsPrivate: opt.Private, AutoInit: opt.AutoInit, DefaultBranch: opt.DefaultBranch, + TrustModel: models.ToTrustModel(opt.TrustModel), }) if err != nil { if models.IsErrRepoAlreadyExist(err) { diff --git a/routers/repo/issue.go b/routers/repo/issue.go index 7c4f2cea9..be46ddbeb 100644 --- a/routers/repo/issue.go +++ b/routers/repo/issue.go @@ -1259,7 +1259,7 @@ func ViewIssue(ctx *context.Context) { } ctx.Data["WillSign"] = false if ctx.User != nil { - sign, key, err := pull.SignMerge(ctx.User, pull.BaseRepo.RepoPath(), pull.BaseBranch, pull.GetGitRefName()) + sign, key, _, err := pull.SignMerge(ctx.User, pull.BaseRepo.RepoPath(), pull.BaseBranch, pull.GetGitRefName()) ctx.Data["WillSign"] = sign ctx.Data["SigningKey"] = key if err != nil { diff --git a/routers/repo/repo.go b/routers/repo/repo.go index d12640dd6..4a088ff9c 100644 --- a/routers/repo/repo.go +++ b/routers/repo/repo.go @@ -238,6 +238,7 @@ func CreatePost(ctx *context.Context, form auth.CreateRepoForm) { IsPrivate: form.Private || setting.Repository.ForcePrivate, DefaultBranch: form.DefaultBranch, AutoInit: form.AutoInit, + TrustModel: models.ToTrustModel(form.TrustModel), }) if err == nil { log.Trace("Repository created [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name) diff --git a/routers/repo/setting.go b/routers/repo/setting.go index 8d07bf09a..d2c20fb03 100644 --- a/routers/repo/setting.go +++ b/routers/repo/setting.go @@ -51,6 +51,11 @@ func Settings(ctx *context.Context) { ctx.Data["Title"] = ctx.Tr("repo.settings") ctx.Data["PageIsSettingsOptions"] = true ctx.Data["ForcePrivate"] = setting.Repository.ForcePrivate + + signing, _ := models.SigningKey(ctx.Repo.Repository.RepoPath()) + ctx.Data["SigningKeyAvailable"] = len(signing) > 0 + ctx.Data["SigningSettings"] = setting.Repository.Signing + ctx.HTML(200, tplSettingsOptions) } @@ -318,6 +323,26 @@ func SettingsPost(ctx *context.Context, form auth.RepoSettingForm) { ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) ctx.Redirect(ctx.Repo.RepoLink + "/settings") + case "signing": + changed := false + + trustModel := models.ToTrustModel(form.TrustModel) + if trustModel != repo.TrustModel { + repo.TrustModel = trustModel + changed = true + } + + if changed { + if err := models.UpdateRepository(repo, false); err != nil { + ctx.ServerError("UpdateRepository", err) + return + } + } + log.Trace("Repository signing settings updated: %s/%s", ctx.Repo.Owner.Name, repo.Name) + + ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success")) + ctx.Redirect(ctx.Repo.RepoLink + "/settings") + case "admin": if !ctx.User.IsAdmin { ctx.Error(403) diff --git a/services/pull/merge.go b/services/pull/merge.go index b430a9080..7a1964932 100644 --- a/services/pull/merge.go +++ b/services/pull/merge.go @@ -209,18 +209,23 @@ func rawMerge(pr *models.PullRequest, doer *models.User, mergeStyle models.Merge outbuf.Reset() errbuf.Reset() + sig := doer.NewGitSig() + committer := sig + // Determine if we should sign signArg := "" if git.CheckGitVersionConstraint(">= 1.7.9") == nil { - sign, keyID, _ := pr.SignMerge(doer, tmpBasePath, "HEAD", trackingBranch) + sign, keyID, signer, _ := pr.SignMerge(doer, tmpBasePath, "HEAD", trackingBranch) if sign { signArg = "-S" + keyID + if pr.BaseRepo.GetTrustModel() == models.CommitterTrustModel || pr.BaseRepo.GetTrustModel() == models.CollaboratorCommitterTrustModel { + committer = signer + } } else if git.CheckGitVersionConstraint(">= 2.0.0") == nil { signArg = "--no-gpg-sign" } } - sig := doer.NewGitSig() commitTimeStr := time.Now().Format(time.RFC3339) // Because this may call hooks we should pass in the environment @@ -228,8 +233,8 @@ func rawMerge(pr *models.PullRequest, doer *models.User, mergeStyle models.Merge "GIT_AUTHOR_NAME="+sig.Name, "GIT_AUTHOR_EMAIL="+sig.Email, "GIT_AUTHOR_DATE="+commitTimeStr, - "GIT_COMMITTER_NAME="+sig.Name, - "GIT_COMMITTER_EMAIL="+sig.Email, + "GIT_COMMITTER_NAME="+committer.Name, + "GIT_COMMITTER_EMAIL="+committer.Email, "GIT_COMMITTER_DATE="+commitTimeStr, ) @@ -346,6 +351,10 @@ func rawMerge(pr *models.PullRequest, doer *models.User, mergeStyle models.Merge return "", fmt.Errorf("git commit [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) } } else { + if committer != sig { + // add trailer + message += fmt.Sprintf("\nCo-Authored-By: %s\nCo-Committed-By: %s\n", sig.String(), sig.String()) + } if err := git.NewCommand("commit", signArg, fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email), "-m", message).RunInDirTimeoutEnvPipeline(env, -1, tmpBasePath, &outbuf, &errbuf); err != nil { log.Error("git commit [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) return "", fmt.Errorf("git commit [%s:%s -> %s:%s]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, pr.BaseRepo.FullName(), pr.BaseBranch, err, outbuf.String(), errbuf.String()) @@ -526,7 +535,7 @@ func IsSignedIfRequired(pr *models.PullRequest, doer *models.User) (bool, error) return true, nil } - sign, _, err := pr.SignMerge(doer, pr.BaseRepo.RepoPath(), pr.BaseBranch, pr.GetGitRefName()) + sign, _, _, err := pr.SignMerge(doer, pr.BaseRepo.RepoPath(), pr.BaseBranch, pr.GetGitRefName()) return sign, err } diff --git a/services/wiki/wiki.go b/services/wiki/wiki.go index 3616823c5..fab02bae0 100644 --- a/services/wiki/wiki.go +++ b/services/wiki/wiki.go @@ -185,16 +185,22 @@ func updateWikiPage(doer *models.User, repo *models.Repository, oldWikiName, new Message: message, } - sign, signingKey, _ := repo.SignWikiCommit(doer) + committer := doer.NewGitSig() + + sign, signingKey, signer, _ := repo.SignWikiCommit(doer) if sign { commitTreeOpts.KeyID = signingKey + if repo.GetTrustModel() == models.CommitterTrustModel || repo.GetTrustModel() == models.CollaboratorCommitterTrustModel { + committer = signer + } } else { commitTreeOpts.NoGPGSign = true } if hasMasterBranch { commitTreeOpts.Parents = []string{"HEAD"} } - commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), tree, commitTreeOpts) + + commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), committer, tree, commitTreeOpts) if err != nil { log.Error("%v", err) return err @@ -302,14 +308,19 @@ func DeleteWikiPage(doer *models.User, repo *models.Repository, wikiName string) Parents: []string{"HEAD"}, } - sign, signingKey, _ := repo.SignWikiCommit(doer) + committer := doer.NewGitSig() + + sign, signingKey, signer, _ := repo.SignWikiCommit(doer) if sign { commitTreeOpts.KeyID = signingKey + if repo.GetTrustModel() == models.CommitterTrustModel || repo.GetTrustModel() == models.CollaboratorCommitterTrustModel { + committer = signer + } } else { commitTreeOpts.NoGPGSign = true } - commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), tree, commitTreeOpts) + commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), committer, tree, commitTreeOpts) if err != nil { return err } diff --git a/templates/repo/create.tmpl b/templates/repo/create.tmpl index c4b25c73d..d5c540724 100644 --- a/templates/repo/create.tmpl +++ b/templates/repo/create.tmpl @@ -167,6 +167,19 @@ +
+ +

diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl index 8a490ac64..a8e050c58 100644 --- a/templates/repo/settings/options.tmpl +++ b/templates/repo/settings/options.tmpl @@ -340,6 +340,52 @@ +

+ {{.i18n.Tr "repo.settings.signing_settings"}} +

+
+
+ {{.CsrfTokenHtml}} + +
+ +
+
+ + +

{{.i18n.Tr "repo.settings.trust_model.default.desc"}}

+
+
+
+
+ + +

{{.i18n.Tr "repo.settings.trust_model.collaborator.desc"}}

+
+
+
+
+ + +

{{.i18n.Tr "repo.settings.trust_model.committer.desc"}}

+
+
+
+
+ + +

{{.i18n.Tr "repo.settings.trust_model.collaboratorcommitter.desc"}}

+
+
+
+ +
+
+ +
+
+
+ {{if .IsAdmin}}

{{.i18n.Tr "repo.settings.admin_settings"}} diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 4b78b40dd..b687b4c57 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -11937,6 +11937,17 @@ "description": "Readme of the repository to create", "type": "string", "x-go-name": "Readme" + }, + "trust_model": { + "description": "TrustModel of the repository", + "type": "string", + "enum": [ + "default", + "collaborator", + "committer", + "collaboratorcommitter" + ], + "x-go-name": "TrustModel" } }, "x-go-package": "code.gitea.io/gitea/modules/structs"