diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index af3418f70c..ffc4e40678 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 7f969add2c..1e48ee2597 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 662eac939b..b944fdcbff 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) - } - - 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 } diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 5317cc5743..ea1bf59649 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 0000000000..f71f71e22f --- /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 ab61232d65..10a6522ebe 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 2bbc74f439..25fe3f63d3 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 c9dd5ea4dc..be9309ed4e 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 3ad57085b0..f1130f372b 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 2c77361460..cb2e60d263 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 a662aaab4f..57896318a7 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 2ffc75e7c8..8343776c47 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 ec671a9322..e0d6c9fcb6 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 dcb87ec92b..f7fa5f0289 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 abbec05a37..c180b9b948 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 c5fb0af383..1314464a6e 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 c2038e18d4..d066544a85 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 eb1501d7b8..67dd805353 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 c57702b282..c86b19dfd0 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 376a631252..ea76d4cb6a 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 603187c16d..baf8110a19 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 7c4f2cea9b..be46ddbeb9 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 d12640dd6d..4a088ff9cd 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 8d07bf09a4..d2c20fb03a 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 b430a9080e..7a19649327 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 3616823c5d..fab02bae0c 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 c4b25c73d8..d5c540724b 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 8a490ac645..a8e050c583 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 4b78b40dd2..b687b4c57f 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"