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

@ -124,6 +124,8 @@ SIGNING_KEY = default
; by setting the SIGNING_KEY ID to the correct ID.) ; by setting the SIGNING_KEY ID to the correct ID.)
SIGNING_NAME = SIGNING_NAME =
SIGNING_EMAIL = 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 ; Determines when gitea should sign the initial commit when creating a repository
; Either: ; Either:
; - never ; - never

View file

@ -101,6 +101,10 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
- `twofa`: Only sign if the user is logged in with twofa - `twofa`: Only sign if the user is logged in with twofa
- `always`: Always sign - `always`: Always sign
- Options other than `never` and `always` can be combined as a comma separated list. - 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. - `WIKI`: **never**: \[never, pubkey, twofa, always, parentsigned\]: Sign commits to wiki.
- `CRUD_ACTIONS`: **pubkey, twofa, parentsigned**: \[never, pubkey, twofa, parentsigned, always\]: Sign CRUD actions. - `CRUD_ACTIONS`: **pubkey, twofa, parentsigned**: \[never, pubkey, twofa, parentsigned, always\]: Sign CRUD actions.
- Options as above, with the addition of: - Options as above, with the addition of:

View file

@ -831,7 +831,7 @@ func ParseCommitsWithSignature(oldCommits *list.List, repository *Repository) *l
newCommits = list.New() newCommits = list.New()
e = oldCommits.Front() e = oldCommits.Front()
) )
memberMap := map[int64]bool{} keyMap := map[string]bool{}
for e != nil { for e != nil {
c := e.Value.(UserCommit) c := e.Value.(UserCommit)
@ -840,7 +840,7 @@ func ParseCommitsWithSignature(oldCommits *list.List, repository *Repository) *l
Verification: ParseCommitWithSignature(c.Commit), Verification: ParseCommitWithSignature(c.Commit),
} }
_ = CalculateTrustStatus(signCommit.Verification, repository, &memberMap) _ = CalculateTrustStatus(signCommit.Verification, repository, &keyMap)
newCommits.PushBack(signCommit) newCommits.PushBack(signCommit)
e = e.Next() 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 // CalculateTrustStatus will calculate the TrustStatus for a commit verification within a repository
func CalculateTrustStatus(verification *CommitVerification, repository *Repository, memberMap *map[int64]bool) (err error) { func CalculateTrustStatus(verification *CommitVerification, repository *Repository, keyMap *map[string]bool) (err error) {
if verification.Verified { if !verification.Verified {
verification.TrustStatus = "trusted" return
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"
}
}
}
} }
// 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 return
} }

View file

@ -237,6 +237,8 @@ var migrations = []Migration{
NewMigration("add primary key to repo_topic", addPrimaryKeyToRepoTopic), NewMigration("add primary key to repo_topic", addPrimaryKeyToRepoTopic),
// v151 -> v152 // v151 -> v152
NewMigration("set default password algorithm to Argon2", setDefaultPasswordToArgon2), NewMigration("set default password algorithm to Argon2", setDefaultPasswordToArgon2),
// v152 -> v153
NewMigration("add TrustModel field to Repository", addTrustModelToRepository),
} }
// GetCurrentDBVersion returns the current db version // 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 // 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 { if err := pr.LoadBaseRepo(); err != nil {
log.Error("Unable to get Base Repo for pull request") log.Error("Unable to get Base Repo for pull request")
return false, "", err return false, "", nil, err
} }
repo := pr.BaseRepo repo := pr.BaseRepo
signingKey := signingKey(repo.RepoPath()) signingKey, signer := SigningKey(repo.RepoPath())
if signingKey == "" { if signingKey == "" {
return false, "", &ErrWontSign{noKey} return false, "", nil, &ErrWontSign{noKey}
} }
rules := signingModeFromStrings(setting.Repository.Signing.Merges) rules := signingModeFromStrings(setting.Repository.Signing.Merges)
@ -31,101 +31,101 @@ Loop:
for _, rule := range rules { for _, rule := range rules {
switch rule { switch rule {
case never: case never:
return false, "", &ErrWontSign{never} return false, "", nil, &ErrWontSign{never}
case always: case always:
break Loop break Loop
case pubkey: case pubkey:
keys, err := ListGPGKeys(u.ID, ListOptions{}) keys, err := ListGPGKeys(u.ID, ListOptions{})
if err != nil { if err != nil {
return false, "", err return false, "", nil, err
} }
if len(keys) == 0 { if len(keys) == 0 {
return false, "", &ErrWontSign{pubkey} return false, "", nil, &ErrWontSign{pubkey}
} }
case twofa: case twofa:
twofaModel, err := GetTwoFactorByUID(u.ID) twofaModel, err := GetTwoFactorByUID(u.ID)
if err != nil && !IsErrTwoFactorNotEnrolled(err) { if err != nil && !IsErrTwoFactorNotEnrolled(err) {
return false, "", err return false, "", nil, err
} }
if twofaModel == nil { if twofaModel == nil {
return false, "", &ErrWontSign{twofa} return false, "", nil, &ErrWontSign{twofa}
} }
case approved: case approved:
protectedBranch, err := GetProtectedBranchBy(repo.ID, pr.BaseBranch) protectedBranch, err := GetProtectedBranchBy(repo.ID, pr.BaseBranch)
if err != nil { if err != nil {
return false, "", err return false, "", nil, err
} }
if protectedBranch == nil { if protectedBranch == nil {
return false, "", &ErrWontSign{approved} return false, "", nil, &ErrWontSign{approved}
} }
if protectedBranch.GetGrantedApprovalsCount(pr) < 1 { if protectedBranch.GetGrantedApprovalsCount(pr) < 1 {
return false, "", &ErrWontSign{approved} return false, "", nil, &ErrWontSign{approved}
} }
case baseSigned: case baseSigned:
if gitRepo == nil { if gitRepo == nil {
gitRepo, err = git.OpenRepository(tmpBasePath) gitRepo, err = git.OpenRepository(tmpBasePath)
if err != nil { if err != nil {
return false, "", err return false, "", nil, err
} }
defer gitRepo.Close() defer gitRepo.Close()
} }
commit, err := gitRepo.GetCommit(baseCommit) commit, err := gitRepo.GetCommit(baseCommit)
if err != nil { if err != nil {
return false, "", err return false, "", nil, err
} }
verification := ParseCommitWithSignature(commit) verification := ParseCommitWithSignature(commit)
if !verification.Verified { if !verification.Verified {
return false, "", &ErrWontSign{baseSigned} return false, "", nil, &ErrWontSign{baseSigned}
} }
case headSigned: case headSigned:
if gitRepo == nil { if gitRepo == nil {
gitRepo, err = git.OpenRepository(tmpBasePath) gitRepo, err = git.OpenRepository(tmpBasePath)
if err != nil { if err != nil {
return false, "", err return false, "", nil, err
} }
defer gitRepo.Close() defer gitRepo.Close()
} }
commit, err := gitRepo.GetCommit(headCommit) commit, err := gitRepo.GetCommit(headCommit)
if err != nil { if err != nil {
return false, "", err return false, "", nil, err
} }
verification := ParseCommitWithSignature(commit) verification := ParseCommitWithSignature(commit)
if !verification.Verified { if !verification.Verified {
return false, "", &ErrWontSign{headSigned} return false, "", nil, &ErrWontSign{headSigned}
} }
case commitsSigned: case commitsSigned:
if gitRepo == nil { if gitRepo == nil {
gitRepo, err = git.OpenRepository(tmpBasePath) gitRepo, err = git.OpenRepository(tmpBasePath)
if err != nil { if err != nil {
return false, "", err return false, "", nil, err
} }
defer gitRepo.Close() defer gitRepo.Close()
} }
commit, err := gitRepo.GetCommit(headCommit) commit, err := gitRepo.GetCommit(headCommit)
if err != nil { if err != nil {
return false, "", err return false, "", nil, err
} }
verification := ParseCommitWithSignature(commit) verification := ParseCommitWithSignature(commit)
if !verification.Verified { if !verification.Verified {
return false, "", &ErrWontSign{commitsSigned} return false, "", nil, &ErrWontSign{commitsSigned}
} }
// need to work out merge-base // need to work out merge-base
mergeBaseCommit, _, err := gitRepo.GetMergeBase("", baseCommit, headCommit) mergeBaseCommit, _, err := gitRepo.GetMergeBase("", baseCommit, headCommit)
if err != nil { if err != nil {
return false, "", err return false, "", nil, err
} }
commitList, err := commit.CommitsBeforeUntil(mergeBaseCommit) commitList, err := commit.CommitsBeforeUntil(mergeBaseCommit)
if err != nil { if err != nil {
return false, "", err return false, "", nil, err
} }
for e := commitList.Front(); e != nil; e = e.Next() { for e := commitList.Front(); e != nil; e = e.Next() {
commit = e.Value.(*git.Commit) commit = e.Value.(*git.Commit)
verification := ParseCommitWithSignature(commit) verification := ParseCommitWithSignature(commit)
if !verification.Verified { 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 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. // Repository represents a git repository.
type Repository struct { type Repository struct {
ID int64 `xorm:"pk autoincr"` ID int64 `xorm:"pk autoincr"`
@ -198,6 +239,8 @@ type Repository struct {
CloseIssuesViaCommitInAnyBranch bool `xorm:"NOT NULL DEFAULT false"` CloseIssuesViaCommitInAnyBranch bool `xorm:"NOT NULL DEFAULT false"`
Topics []string `xorm:"TEXT JSON"` Topics []string `xorm:"TEXT JSON"`
TrustModel TrustModelType
// Avatar: ID(10-20)-md5(32) - must fit into 64 symbols // Avatar: ID(10-20)-md5(32) - must fit into 64 symbols
Avatar string `xorm:"VARCHAR(64)"` Avatar string `xorm:"VARCHAR(64)"`
@ -1038,6 +1081,7 @@ type CreateRepoOptions struct {
IsMirror bool IsMirror bool
AutoInit bool AutoInit bool
Status RepositoryStatus Status RepositoryStatus
TrustModel TrustModelType
} }
// GetRepoInitFile returns repository init files // GetRepoInitFile returns repository init files
@ -2383,6 +2427,18 @@ func UpdateRepositoryCols(repo *Repository, cols ...string) error {
return updateRepositoryCols(x, repo, cols...) 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 // DoctorUserStarNum recalculate Stars number for all user
func DoctorUserStarNum() (err error) { func DoctorUserStarNum() (err error) {
const batchSize = 100 const batchSize = 100

View file

@ -31,7 +31,7 @@ const (
func signingModeFromStrings(modeStrings []string) []signingMode { func signingModeFromStrings(modeStrings []string) []signingMode {
returnable := make([]signingMode, 0, len(modeStrings)) returnable := make([]signingMode, 0, len(modeStrings))
for _, mode := range modeStrings { for _, mode := range modeStrings {
signMode := signingMode(strings.ToLower(mode)) signMode := signingMode(strings.ToLower(strings.TrimSpace(mode)))
switch signMode { switch signMode {
case never: case never:
return []signingMode{never} return []signingMode{never}
@ -59,9 +59,10 @@ func signingModeFromStrings(modeStrings []string) []signingMode {
return returnable 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" { if setting.Repository.Signing.SigningKey == "none" {
return "" return "", nil
} }
if setting.Repository.Signing.SigningKey == "default" || setting.Repository.Signing.SigningKey == "" { 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) value, _ := git.NewCommand("config", "--get", "commit.gpgsign").RunInDir(repoPath)
sign, valid := git.ParseBool(strings.TrimSpace(value)) sign, valid := git.ParseBool(strings.TrimSpace(value))
if !sign || !valid { if !sign || !valid {
return "" return "", nil
} }
signingKey, _ := git.NewCommand("config", "--get", "user.signingkey").RunInDir(repoPath) 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 // PublicSigningKey gets the public signing key within a provided repository directory
func PublicSigningKey(repoPath string) (string, error) { func PublicSigningKey(repoPath string) (string, error) {
signingKey := signingKey(repoPath) signingKey, _ := SigningKey(repoPath)
if signingKey == "" { if signingKey == "" {
return "", nil return "", nil
} }
@ -96,143 +105,143 @@ func PublicSigningKey(repoPath string) (string, error) {
} }
// SignInitialCommit determines if we should sign the initial commit to this repository // 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) rules := signingModeFromStrings(setting.Repository.Signing.InitialCommit)
signingKey := signingKey(repoPath) signingKey, sig := SigningKey(repoPath)
if signingKey == "" { if signingKey == "" {
return false, "", &ErrWontSign{noKey} return false, "", nil, &ErrWontSign{noKey}
} }
Loop: Loop:
for _, rule := range rules { for _, rule := range rules {
switch rule { switch rule {
case never: case never:
return false, "", &ErrWontSign{never} return false, "", nil, &ErrWontSign{never}
case always: case always:
break Loop break Loop
case pubkey: case pubkey:
keys, err := ListGPGKeys(u.ID, ListOptions{}) keys, err := ListGPGKeys(u.ID, ListOptions{})
if err != nil { if err != nil {
return false, "", err return false, "", nil, err
} }
if len(keys) == 0 { if len(keys) == 0 {
return false, "", &ErrWontSign{pubkey} return false, "", nil, &ErrWontSign{pubkey}
} }
case twofa: case twofa:
twofaModel, err := GetTwoFactorByUID(u.ID) twofaModel, err := GetTwoFactorByUID(u.ID)
if err != nil && !IsErrTwoFactorNotEnrolled(err) { if err != nil && !IsErrTwoFactorNotEnrolled(err) {
return false, "", err return false, "", nil, err
} }
if twofaModel == nil { 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 // 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) rules := signingModeFromStrings(setting.Repository.Signing.Wiki)
signingKey := signingKey(repo.WikiPath()) signingKey, sig := SigningKey(repo.WikiPath())
if signingKey == "" { if signingKey == "" {
return false, "", &ErrWontSign{noKey} return false, "", nil, &ErrWontSign{noKey}
} }
Loop: Loop:
for _, rule := range rules { for _, rule := range rules {
switch rule { switch rule {
case never: case never:
return false, "", &ErrWontSign{never} return false, "", nil, &ErrWontSign{never}
case always: case always:
break Loop break Loop
case pubkey: case pubkey:
keys, err := ListGPGKeys(u.ID, ListOptions{}) keys, err := ListGPGKeys(u.ID, ListOptions{})
if err != nil { if err != nil {
return false, "", err return false, "", nil, err
} }
if len(keys) == 0 { if len(keys) == 0 {
return false, "", &ErrWontSign{pubkey} return false, "", nil, &ErrWontSign{pubkey}
} }
case twofa: case twofa:
twofaModel, err := GetTwoFactorByUID(u.ID) twofaModel, err := GetTwoFactorByUID(u.ID)
if err != nil && !IsErrTwoFactorNotEnrolled(err) { if err != nil && !IsErrTwoFactorNotEnrolled(err) {
return false, "", err return false, "", nil, err
} }
if twofaModel == nil { if twofaModel == nil {
return false, "", &ErrWontSign{twofa} return false, "", nil, &ErrWontSign{twofa}
} }
case parentSigned: case parentSigned:
gitRepo, err := git.OpenRepository(repo.WikiPath()) gitRepo, err := git.OpenRepository(repo.WikiPath())
if err != nil { if err != nil {
return false, "", err return false, "", nil, err
} }
defer gitRepo.Close() defer gitRepo.Close()
commit, err := gitRepo.GetCommit("HEAD") commit, err := gitRepo.GetCommit("HEAD")
if err != nil { if err != nil {
return false, "", err return false, "", nil, err
} }
if commit.Signature == nil { if commit.Signature == nil {
return false, "", &ErrWontSign{parentSigned} return false, "", nil, &ErrWontSign{parentSigned}
} }
verification := ParseCommitWithSignature(commit) verification := ParseCommitWithSignature(commit)
if !verification.Verified { 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 // 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) rules := signingModeFromStrings(setting.Repository.Signing.CRUDActions)
signingKey := signingKey(repo.RepoPath()) signingKey, sig := SigningKey(repo.RepoPath())
if signingKey == "" { if signingKey == "" {
return false, "", &ErrWontSign{noKey} return false, "", nil, &ErrWontSign{noKey}
} }
Loop: Loop:
for _, rule := range rules { for _, rule := range rules {
switch rule { switch rule {
case never: case never:
return false, "", &ErrWontSign{never} return false, "", nil, &ErrWontSign{never}
case always: case always:
break Loop break Loop
case pubkey: case pubkey:
keys, err := ListGPGKeys(u.ID, ListOptions{}) keys, err := ListGPGKeys(u.ID, ListOptions{})
if err != nil { if err != nil {
return false, "", err return false, "", nil, err
} }
if len(keys) == 0 { if len(keys) == 0 {
return false, "", &ErrWontSign{pubkey} return false, "", nil, &ErrWontSign{pubkey}
} }
case twofa: case twofa:
twofaModel, err := GetTwoFactorByUID(u.ID) twofaModel, err := GetTwoFactorByUID(u.ID)
if err != nil && !IsErrTwoFactorNotEnrolled(err) { if err != nil && !IsErrTwoFactorNotEnrolled(err) {
return false, "", err return false, "", nil, err
} }
if twofaModel == nil { if twofaModel == nil {
return false, "", &ErrWontSign{twofa} return false, "", nil, &ErrWontSign{twofa}
} }
case parentSigned: case parentSigned:
gitRepo, err := git.OpenRepository(tmpBasePath) gitRepo, err := git.OpenRepository(tmpBasePath)
if err != nil { if err != nil {
return false, "", err return false, "", nil, err
} }
defer gitRepo.Close() defer gitRepo.Close()
commit, err := gitRepo.GetCommit(parentCommit) commit, err := gitRepo.GetCommit(parentCommit)
if err != nil { if err != nil {
return false, "", err return false, "", nil, err
} }
if commit.Signature == nil { if commit.Signature == nil {
return false, "", &ErrWontSign{parentSigned} return false, "", nil, &ErrWontSign{parentSigned}
} }
verification := ParseCommitWithSignature(commit) verification := ParseCommitWithSignature(commit)
if !verification.Verified { if !verification.Verified {
return false, "", &ErrWontSign{parentSigned} return false, "", nil, &ErrWontSign{parentSigned}
} }
} }
} }
return true, signingKey, nil return true, signingKey, sig, nil
} }

View file

@ -45,6 +45,7 @@ type CreateRepoForm struct {
Webhooks bool Webhooks bool
Avatar bool Avatar bool
Labels bool Labels bool
TrustModel string
} }
// Validate validates the fields // Validate validates the fields
@ -142,6 +143,9 @@ type RepoSettingForm struct {
EnableIssueDependencies bool EnableIssueDependencies bool
IsArchived bool IsArchived bool
// Signing Settings
TrustModel string
// Admin settings // Admin settings
EnableHealthCheck bool EnableHealthCheck bool
EnableCloseIssuesViaCommitInAnyBranch bool EnableCloseIssuesViaCommitInAnyBranch bool

View file

@ -114,7 +114,7 @@ func (r *Repository) CanCommitToBranch(doer *models.User) (CanCommitToBranchResu
requireSigned = protectedBranch.RequireSignedCommits 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 canCommit := r.CanEnableEditor() && userCanPush
if requireSigned { if requireSigned {

View file

@ -62,7 +62,7 @@ type CommitTreeOpts struct {
} }
// CommitTree creates a commit from a given tree id for the user with provided message // 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() err := LoadGitVersion()
if err != nil { if err != nil {
return SHA1{}, err 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 // Because this may call hooks we should pass in the environment
env := append(os.Environ(), env := append(os.Environ(),
"GIT_AUTHOR_NAME="+sig.Name, "GIT_AUTHOR_NAME="+author.Name,
"GIT_AUTHOR_EMAIL="+sig.Email, "GIT_AUTHOR_EMAIL="+author.Email,
"GIT_AUTHOR_DATE="+commitTimeStr, "GIT_AUTHOR_DATE="+commitTimeStr,
"GIT_COMMITTER_NAME="+sig.Name, "GIT_COMMITTER_NAME="+committer.Name,
"GIT_COMMITTER_EMAIL="+sig.Email, "GIT_COMMITTER_EMAIL="+committer.Email,
"GIT_COMMITTER_DATE="+commitTimeStr, "GIT_COMMITTER_DATE="+commitTimeStr,
) )
cmd := NewCommand("commit-tree", tree.ID.String()) cmd := NewCommand("commit-tree", tree.ID.String())

View file

@ -67,7 +67,7 @@ func DeleteRepoFile(repo *models.Repository, doer *models.User, opts *DeleteRepo
} }
} }
if protectedBranch.RequireSignedCommits { if protectedBranch.RequireSignedCommits {
_, _, err := repo.SignCRUDAction(doer, repo.RepoPath(), opts.OldBranch) _, _, _, err := repo.SignCRUDAction(doer, repo.RepoPath(), opts.OldBranch)
if err != nil { if err != nil {
if !models.IsErrWontSign(err) { if !models.IsErrWontSign(err) {
return nil, err return nil, err

View file

@ -204,8 +204,6 @@ func (t *TemporaryUploadRepository) CommitTreeWithDate(author, committer *models
"GIT_AUTHOR_NAME="+authorSig.Name, "GIT_AUTHOR_NAME="+authorSig.Name,
"GIT_AUTHOR_EMAIL="+authorSig.Email, "GIT_AUTHOR_EMAIL="+authorSig.Email,
"GIT_AUTHOR_DATE="+authorDate.Format(time.RFC3339), "GIT_AUTHOR_DATE="+authorDate.Format(time.RFC3339),
"GIT_COMMITTER_NAME="+committerSig.Name,
"GIT_COMMITTER_EMAIL="+committerSig.Email,
"GIT_COMMITTER_DATE="+committerDate.Format(time.RFC3339), "GIT_COMMITTER_DATE="+committerDate.Format(time.RFC3339),
) )
@ -217,14 +215,32 @@ func (t *TemporaryUploadRepository) CommitTreeWithDate(author, committer *models
// Determine if we should sign // Determine if we should sign
if git.CheckGitVersionConstraint(">= 1.7.9") == nil { 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 { if sign {
args = append(args, "-S"+keyID) 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 { } else if git.CheckGitVersionConstraint(">= 2.0.0") == nil {
args = append(args, "--no-gpg-sign") args = append(args, "--no-gpg-sign")
} }
} }
env = append(env,
"GIT_COMMITTER_NAME="+committerSig.Name,
"GIT_COMMITTER_EMAIL="+committerSig.Email,
)
stdout := new(bytes.Buffer) stdout := new(bytes.Buffer)
stderr := new(bytes.Buffer) stderr := new(bytes.Buffer)
if err := git.NewCommand(args...).RunInDirTimeoutEnvFullPipeline(env, -1, t.basePath, stdout, stderr, messageBytes); err != nil { if err := git.NewCommand(args...).RunInDirTimeoutEnvFullPipeline(env, -1, t.basePath, stdout, stderr, messageBytes); err != nil {

View file

@ -161,7 +161,7 @@ func CreateOrUpdateRepoFile(repo *models.Repository, doer *models.User, opts *Up
} }
} }
if protectedBranch.RequireSignedCommits { if protectedBranch.RequireSignedCommits {
_, _, err := repo.SignCRUDAction(doer, repo.RepoPath(), opts.OldBranch) _, _, _, err := repo.SignCRUDAction(doer, repo.RepoPath(), opts.OldBranch)
if err != nil { if err != nil {
if !models.IsErrWontSign(err) { if !models.IsErrWontSign(err) {
return nil, err return nil, err

View file

@ -41,6 +41,7 @@ func CreateRepository(doer, u *models.User, opts models.CreateRepoOptions) (_ *m
CloseIssuesViaCommitInAnyBranch: setting.Repository.DefaultCloseIssuesViaCommitsInAnyBranch, CloseIssuesViaCommitInAnyBranch: setting.Repository.DefaultCloseIssuesViaCommitsInAnyBranch,
Status: opts.Status, Status: opts.Status,
IsEmpty: !opts.AutoInit, IsEmpty: !opts.AutoInit,
TrustModel: opts.TrustModel,
} }
err = models.WithTx(func(ctx models.DBContext) error { err = models.WithTx(func(ctx models.DBContext) error {

View file

@ -243,6 +243,7 @@ func GenerateRepository(ctx models.DBContext, doer, owner *models.User, template
IsEmpty: !opts.GitContent || templateRepo.IsEmpty, IsEmpty: !opts.GitContent || templateRepo.IsEmpty,
IsFsckEnabled: templateRepo.IsFsckEnabled, IsFsckEnabled: templateRepo.IsFsckEnabled,
TemplateID: templateRepo.ID, TemplateID: templateRepo.ID,
TrustModel: templateRepo.TrustModel,
} }
if err = models.CreateRepository(ctx, doer, owner, generateRepo); err != nil { if err = models.CreateRepository(ctx, doer, owner, generateRepo); err != nil {

View file

@ -109,10 +109,10 @@ func initRepoCommit(tmpPath string, repo *models.Repository, u *models.User, def
"GIT_AUTHOR_NAME="+sig.Name, "GIT_AUTHOR_NAME="+sig.Name,
"GIT_AUTHOR_EMAIL="+sig.Email, "GIT_AUTHOR_EMAIL="+sig.Email,
"GIT_AUTHOR_DATE="+commitTimeStr, "GIT_AUTHOR_DATE="+commitTimeStr,
"GIT_COMMITTER_NAME="+sig.Name,
"GIT_COMMITTER_EMAIL="+sig.Email,
"GIT_COMMITTER_DATE="+commitTimeStr, "GIT_COMMITTER_DATE="+commitTimeStr,
) )
committerName := sig.Name
committerEmail := sig.Email
if stdout, err := git.NewCommand("add", "--all"). if stdout, err := git.NewCommand("add", "--all").
SetDescription(fmt.Sprintf("initRepoCommit (git add): %s", tmpPath)). 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 { if git.CheckGitVersionConstraint(">= 1.7.9") == nil {
sign, keyID, _ := models.SignInitialCommit(tmpPath, u) sign, keyID, signer, _ := models.SignInitialCommit(tmpPath, u)
if sign { if sign {
args = append(args, "-S"+keyID) 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 { } else if git.CheckGitVersionConstraint(">= 2.0.0") == nil {
args = append(args, "--no-gpg-sign") args = append(args, "--no-gpg-sign")
} }
} }
env = append(env,
"GIT_COMMITTER_NAME="+committerName,
"GIT_COMMITTER_EMAIL="+committerEmail,
)
if stdout, err := git.NewCommand(args...). if stdout, err := git.NewCommand(args...).
SetDescription(fmt.Sprintf("initRepoCommit (git commit): %s", tmpPath)). SetDescription(fmt.Sprintf("initRepoCommit (git commit): %s", tmpPath)).
RunInDirWithEnv(tmpPath, env); err != nil { RunInDirWithEnv(tmpPath, env); err != nil {

View file

@ -83,13 +83,14 @@ var (
} `ini:"repository.issue"` } `ini:"repository.issue"`
Signing struct { Signing struct {
SigningKey string SigningKey string
SigningName string SigningName string
SigningEmail string SigningEmail string
InitialCommit []string InitialCommit []string
CRUDActions []string `ini:"CRUD_ACTIONS"` CRUDActions []string `ini:"CRUD_ACTIONS"`
Merges []string Merges []string
Wiki []string Wiki []string
DefaultTrustModel string
} `ini:"repository.signing"` } `ini:"repository.signing"`
}{ }{
DetectedCharsetsOrder: []string{ DetectedCharsetsOrder: []string{
@ -209,21 +210,23 @@ var (
// Signing settings // Signing settings
Signing: struct { Signing: struct {
SigningKey string SigningKey string
SigningName string SigningName string
SigningEmail string SigningEmail string
InitialCommit []string InitialCommit []string
CRUDActions []string `ini:"CRUD_ACTIONS"` CRUDActions []string `ini:"CRUD_ACTIONS"`
Merges []string Merges []string
Wiki []string Wiki []string
DefaultTrustModel string
}{ }{
SigningKey: "default", SigningKey: "default",
SigningName: "", SigningName: "",
SigningEmail: "", SigningEmail: "",
InitialCommit: []string{"always"}, InitialCommit: []string{"always"},
CRUDActions: []string{"pubkey", "twofa", "parentsigned"}, CRUDActions: []string{"pubkey", "twofa", "parentsigned"},
Merges: []string{"pubkey", "twofa", "basesigned", "commitssigned"}, Merges: []string{"pubkey", "twofa", "basesigned", "commitssigned"},
Wiki: []string{"never"}, Wiki: []string{"never"},
DefaultTrustModel: "collaborator",
}, },
} }
RepoRootPath string RepoRootPath string
@ -268,6 +271,13 @@ func newRepository() {
log.Fatal("Failed to map Repository.PullRequest settings: %v", err) 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)) preferred := make([]string, 0, len(Repository.DetectedCharsetsOrder))
for _, charset := range Repository.DetectedCharsetsOrder { for _, charset := range Repository.DetectedCharsetsOrder {
canonicalCharset := strings.ToLower(strings.TrimSpace(charset)) canonicalCharset := strings.ToLower(strings.TrimSpace(charset))

View file

@ -117,6 +117,9 @@ type CreateRepoOption struct {
Readme string `json:"readme"` Readme string `json:"readme"`
// DefaultBranch of the repository (used when initializes and in template) // DefaultBranch of the repository (used when initializes and in template)
DefaultBranch string `json:"default_branch" binding:"GitRefName;MaxSize(100)"` 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 // EditRepoOption options when editing a repository's properties

View file

@ -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_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_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.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 = Delete Wiki Data
settings.wiki_delete_desc = Deleting repository wiki data is permanent and cannot be undone. 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. settings.wiki_delete_notices_1 = - This will permanently delete and disable the repository wiki for %s.

View file

@ -244,6 +244,7 @@ func CreateUserRepo(ctx *context.APIContext, owner *models.User, opt api.CreateR
IsPrivate: opt.Private, IsPrivate: opt.Private,
AutoInit: opt.AutoInit, AutoInit: opt.AutoInit,
DefaultBranch: opt.DefaultBranch, DefaultBranch: opt.DefaultBranch,
TrustModel: models.ToTrustModel(opt.TrustModel),
}) })
if err != nil { if err != nil {
if models.IsErrRepoAlreadyExist(err) { if models.IsErrRepoAlreadyExist(err) {

View file

@ -1259,7 +1259,7 @@ func ViewIssue(ctx *context.Context) {
} }
ctx.Data["WillSign"] = false ctx.Data["WillSign"] = false
if ctx.User != nil { 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["WillSign"] = sign
ctx.Data["SigningKey"] = key ctx.Data["SigningKey"] = key
if err != nil { if err != nil {

View file

@ -238,6 +238,7 @@ func CreatePost(ctx *context.Context, form auth.CreateRepoForm) {
IsPrivate: form.Private || setting.Repository.ForcePrivate, IsPrivate: form.Private || setting.Repository.ForcePrivate,
DefaultBranch: form.DefaultBranch, DefaultBranch: form.DefaultBranch,
AutoInit: form.AutoInit, AutoInit: form.AutoInit,
TrustModel: models.ToTrustModel(form.TrustModel),
}) })
if err == nil { if err == nil {
log.Trace("Repository created [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name) log.Trace("Repository created [%d]: %s/%s", repo.ID, ctxUser.Name, repo.Name)

View file

@ -51,6 +51,11 @@ func Settings(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings") ctx.Data["Title"] = ctx.Tr("repo.settings")
ctx.Data["PageIsSettingsOptions"] = true ctx.Data["PageIsSettingsOptions"] = true
ctx.Data["ForcePrivate"] = setting.Repository.ForcePrivate 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) 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.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings") 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": case "admin":
if !ctx.User.IsAdmin { if !ctx.User.IsAdmin {
ctx.Error(403) ctx.Error(403)

View file

@ -209,18 +209,23 @@ func rawMerge(pr *models.PullRequest, doer *models.User, mergeStyle models.Merge
outbuf.Reset() outbuf.Reset()
errbuf.Reset() errbuf.Reset()
sig := doer.NewGitSig()
committer := sig
// Determine if we should sign // Determine if we should sign
signArg := "" signArg := ""
if git.CheckGitVersionConstraint(">= 1.7.9") == nil { 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 { if sign {
signArg = "-S" + keyID signArg = "-S" + keyID
if pr.BaseRepo.GetTrustModel() == models.CommitterTrustModel || pr.BaseRepo.GetTrustModel() == models.CollaboratorCommitterTrustModel {
committer = signer
}
} else if git.CheckGitVersionConstraint(">= 2.0.0") == nil { } else if git.CheckGitVersionConstraint(">= 2.0.0") == nil {
signArg = "--no-gpg-sign" signArg = "--no-gpg-sign"
} }
} }
sig := doer.NewGitSig()
commitTimeStr := time.Now().Format(time.RFC3339) commitTimeStr := time.Now().Format(time.RFC3339)
// Because this may call hooks we should pass in the environment // 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_NAME="+sig.Name,
"GIT_AUTHOR_EMAIL="+sig.Email, "GIT_AUTHOR_EMAIL="+sig.Email,
"GIT_AUTHOR_DATE="+commitTimeStr, "GIT_AUTHOR_DATE="+commitTimeStr,
"GIT_COMMITTER_NAME="+sig.Name, "GIT_COMMITTER_NAME="+committer.Name,
"GIT_COMMITTER_EMAIL="+sig.Email, "GIT_COMMITTER_EMAIL="+committer.Email,
"GIT_COMMITTER_DATE="+commitTimeStr, "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()) 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 { } 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 { 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()) 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()) 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 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 return sign, err
} }

View file

@ -185,16 +185,22 @@ func updateWikiPage(doer *models.User, repo *models.Repository, oldWikiName, new
Message: message, Message: message,
} }
sign, signingKey, _ := repo.SignWikiCommit(doer) committer := doer.NewGitSig()
sign, signingKey, signer, _ := repo.SignWikiCommit(doer)
if sign { if sign {
commitTreeOpts.KeyID = signingKey commitTreeOpts.KeyID = signingKey
if repo.GetTrustModel() == models.CommitterTrustModel || repo.GetTrustModel() == models.CollaboratorCommitterTrustModel {
committer = signer
}
} else { } else {
commitTreeOpts.NoGPGSign = true commitTreeOpts.NoGPGSign = true
} }
if hasMasterBranch { if hasMasterBranch {
commitTreeOpts.Parents = []string{"HEAD"} commitTreeOpts.Parents = []string{"HEAD"}
} }
commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), tree, commitTreeOpts)
commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), committer, tree, commitTreeOpts)
if err != nil { if err != nil {
log.Error("%v", err) log.Error("%v", err)
return err return err
@ -302,14 +308,19 @@ func DeleteWikiPage(doer *models.User, repo *models.Repository, wikiName string)
Parents: []string{"HEAD"}, Parents: []string{"HEAD"},
} }
sign, signingKey, _ := repo.SignWikiCommit(doer) committer := doer.NewGitSig()
sign, signingKey, signer, _ := repo.SignWikiCommit(doer)
if sign { if sign {
commitTreeOpts.KeyID = signingKey commitTreeOpts.KeyID = signingKey
if repo.GetTrustModel() == models.CommitterTrustModel || repo.GetTrustModel() == models.CollaboratorCommitterTrustModel {
committer = signer
}
} else { } else {
commitTreeOpts.NoGPGSign = true commitTreeOpts.NoGPGSign = true
} }
commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), tree, commitTreeOpts) commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), committer, tree, commitTreeOpts)
if err != nil { if err != nil {
return err return err
} }

View file

@ -167,6 +167,19 @@
<label for="default_branch">{{.i18n.Tr "repo.default_branch"}}</label> <label for="default_branch">{{.i18n.Tr "repo.default_branch"}}</label>
<input id="default_branch" name="default_branch" value="{{.default_branch}}" placeholder="{{.default_branch}}"> <input id="default_branch" name="default_branch" value="{{.default_branch}}" placeholder="{{.default_branch}}">
</div> </div>
<div class="inline field">
<label>{{.i18n.Tr "repo.settings.trust_model"}}</label>
<div class="ui selection owner dropdown">
<input type="hidden" id="trust_model" name="trust_model" value="default" required>
<div class="default text">{{.i18n.Tr "repo.settings.trust_model"}}</div>
<i class="dropdown icon"></i>
<div class="menu">
<div class="item" data-value="default">{{.i18n.Tr "repo.settings.trust_model.default"}}</div>
<div class="item" data-value="collaborator">{{.i18n.Tr "repo.settings.trust_model.collaborator"}}</div>
<div class="item" data-value="committer">{{.i18n.Tr "repo.settings.trust_model.committer"}}</div>
<div class="item" data-value="collaboratorcommitter">{{.i18n.Tr "repo.settings.trust_model.collaboratorcommitter"}}</div>
</div>
</div>
</div> </div>
<br/> <br/>

View file

@ -340,6 +340,52 @@
</form> </form>
</div> </div>
<h4 class="ui top attached header">
{{.i18n.Tr "repo.settings.signing_settings"}}
</h4>
<div class="ui attached segment">
<form class="ui form" method="post">
{{.CsrfTokenHtml}}
<input type="hidden" name="action" value="signing">
<div class="field">
<label>{{.i18n.Tr "repo.settings.trust_model"}}</label>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" id="trust_model_default" name="trust_model" {{if eq .Repository.TrustModel.String "default"}}checked="checked"{{end}} value="default">
<label for="trust_model_default">{{.i18n.Tr "repo.settings.trust_model.default"}}</label>
<p class="help">{{.i18n.Tr "repo.settings.trust_model.default.desc"}}</p>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" id="trust_model_collaborator" name="trust_model" {{if eq .Repository.TrustModel.String "collaborator"}}checked="checked"{{end}} value="collaborator">
<label for="trust_model_collaborator">{{.i18n.Tr "repo.settings.trust_model.collaborator.long"}}</label>
<p class="help">{{.i18n.Tr "repo.settings.trust_model.collaborator.desc"}}</p>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="trust_model" id="trust_model_committer" {{if eq .Repository.TrustModel.String "committer"}}checked="checked"{{end}} value="committer">
<label for="trust_model_committer">{{.i18n.Tr "repo.settings.trust_model.committer.long"}}</label>
<p class="help">{{.i18n.Tr "repo.settings.trust_model.committer.desc"}}</p>
</div>
</div>
<div class="field">
<div class="ui radio checkbox">
<input type="radio" name="trust_model" id="trust_model_collaboratorcommitter" {{if eq .Repository.TrustModel.String "collaboratorcommitter"}}checked="checked"{{end}} value="collaboratorcommitter">
<label for="trust_model_collaboratorcommitter">{{.i18n.Tr "repo.settings.trust_model.collaboratorcommitter.long"}}</label>
<p class="help">{{.i18n.Tr "repo.settings.trust_model.collaboratorcommitter.desc"}}</p>
</div>
</div>
</div>
<div class="ui divider"></div>
<div class="field">
<button class="ui green button">{{$.i18n.Tr "repo.settings.update_settings"}}</button>
</div>
</form>
</div>
{{if .IsAdmin}} {{if .IsAdmin}}
<h4 class="ui top attached header"> <h4 class="ui top attached header">
{{.i18n.Tr "repo.settings.admin_settings"}} {{.i18n.Tr "repo.settings.admin_settings"}}

View file

@ -11937,6 +11937,17 @@
"description": "Readme of the repository to create", "description": "Readme of the repository to create",
"type": "string", "type": "string",
"x-go-name": "Readme" "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" "x-go-package": "code.gitea.io/gitea/modules/structs"