code.gitea.io/gitea@v1.22.3/services/asymkey/sign.go (about)

     1  // Copyright 2021 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package asymkey
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"strings"
    10  
    11  	asymkey_model "code.gitea.io/gitea/models/asymkey"
    12  	"code.gitea.io/gitea/models/auth"
    13  	"code.gitea.io/gitea/models/db"
    14  	git_model "code.gitea.io/gitea/models/git"
    15  	issues_model "code.gitea.io/gitea/models/issues"
    16  	repo_model "code.gitea.io/gitea/models/repo"
    17  	user_model "code.gitea.io/gitea/models/user"
    18  	"code.gitea.io/gitea/modules/git"
    19  	"code.gitea.io/gitea/modules/gitrepo"
    20  	"code.gitea.io/gitea/modules/log"
    21  	"code.gitea.io/gitea/modules/process"
    22  	"code.gitea.io/gitea/modules/setting"
    23  )
    24  
    25  type signingMode string
    26  
    27  const (
    28  	never         signingMode = "never"
    29  	always        signingMode = "always"
    30  	pubkey        signingMode = "pubkey"
    31  	twofa         signingMode = "twofa"
    32  	parentSigned  signingMode = "parentsigned"
    33  	baseSigned    signingMode = "basesigned"
    34  	headSigned    signingMode = "headsigned"
    35  	commitsSigned signingMode = "commitssigned"
    36  	approved      signingMode = "approved"
    37  	noKey         signingMode = "nokey"
    38  )
    39  
    40  func signingModeFromStrings(modeStrings []string) []signingMode {
    41  	returnable := make([]signingMode, 0, len(modeStrings))
    42  	for _, mode := range modeStrings {
    43  		signMode := signingMode(strings.ToLower(strings.TrimSpace(mode)))
    44  		switch signMode {
    45  		case never:
    46  			return []signingMode{never}
    47  		case always:
    48  			return []signingMode{always}
    49  		case pubkey:
    50  			fallthrough
    51  		case twofa:
    52  			fallthrough
    53  		case parentSigned:
    54  			fallthrough
    55  		case baseSigned:
    56  			fallthrough
    57  		case headSigned:
    58  			fallthrough
    59  		case approved:
    60  			fallthrough
    61  		case commitsSigned:
    62  			returnable = append(returnable, signMode)
    63  		}
    64  	}
    65  	if len(returnable) == 0 {
    66  		return []signingMode{never}
    67  	}
    68  	return returnable
    69  }
    70  
    71  // ErrWontSign explains the first reason why a commit would not be signed
    72  // There may be other reasons - this is just the first reason found
    73  type ErrWontSign struct {
    74  	Reason signingMode
    75  }
    76  
    77  func (e *ErrWontSign) Error() string {
    78  	return fmt.Sprintf("wont sign: %s", e.Reason)
    79  }
    80  
    81  // IsErrWontSign checks if an error is a ErrWontSign
    82  func IsErrWontSign(err error) bool {
    83  	_, ok := err.(*ErrWontSign)
    84  	return ok
    85  }
    86  
    87  // SigningKey returns the KeyID and git Signature for the repo
    88  func SigningKey(ctx context.Context, repoPath string) (string, *git.Signature) {
    89  	if setting.Repository.Signing.SigningKey == "none" {
    90  		return "", nil
    91  	}
    92  
    93  	if setting.Repository.Signing.SigningKey == "default" || setting.Repository.Signing.SigningKey == "" {
    94  		// Can ignore the error here as it means that commit.gpgsign is not set
    95  		value, _, _ := git.NewCommand(ctx, "config", "--get", "commit.gpgsign").RunStdString(&git.RunOpts{Dir: repoPath})
    96  		sign, valid := git.ParseBool(strings.TrimSpace(value))
    97  		if !sign || !valid {
    98  			return "", nil
    99  		}
   100  
   101  		signingKey, _, _ := git.NewCommand(ctx, "config", "--get", "user.signingkey").RunStdString(&git.RunOpts{Dir: repoPath})
   102  		signingName, _, _ := git.NewCommand(ctx, "config", "--get", "user.name").RunStdString(&git.RunOpts{Dir: repoPath})
   103  		signingEmail, _, _ := git.NewCommand(ctx, "config", "--get", "user.email").RunStdString(&git.RunOpts{Dir: repoPath})
   104  		return strings.TrimSpace(signingKey), &git.Signature{
   105  			Name:  strings.TrimSpace(signingName),
   106  			Email: strings.TrimSpace(signingEmail),
   107  		}
   108  	}
   109  
   110  	return setting.Repository.Signing.SigningKey, &git.Signature{
   111  		Name:  setting.Repository.Signing.SigningName,
   112  		Email: setting.Repository.Signing.SigningEmail,
   113  	}
   114  }
   115  
   116  // PublicSigningKey gets the public signing key within a provided repository directory
   117  func PublicSigningKey(ctx context.Context, repoPath string) (string, error) {
   118  	signingKey, _ := SigningKey(ctx, repoPath)
   119  	if signingKey == "" {
   120  		return "", nil
   121  	}
   122  
   123  	content, stderr, err := process.GetManager().ExecDir(ctx, -1, repoPath,
   124  		"gpg --export -a", "gpg", "--export", "-a", signingKey)
   125  	if err != nil {
   126  		log.Error("Unable to get default signing key in %s: %s, %s, %v", repoPath, signingKey, stderr, err)
   127  		return "", err
   128  	}
   129  	return content, nil
   130  }
   131  
   132  // SignInitialCommit determines if we should sign the initial commit to this repository
   133  func SignInitialCommit(ctx context.Context, repoPath string, u *user_model.User) (bool, string, *git.Signature, error) {
   134  	rules := signingModeFromStrings(setting.Repository.Signing.InitialCommit)
   135  	signingKey, sig := SigningKey(ctx, repoPath)
   136  	if signingKey == "" {
   137  		return false, "", nil, &ErrWontSign{noKey}
   138  	}
   139  
   140  Loop:
   141  	for _, rule := range rules {
   142  		switch rule {
   143  		case never:
   144  			return false, "", nil, &ErrWontSign{never}
   145  		case always:
   146  			break Loop
   147  		case pubkey:
   148  			keys, err := db.Find[asymkey_model.GPGKey](ctx, asymkey_model.FindGPGKeyOptions{
   149  				OwnerID:        u.ID,
   150  				IncludeSubKeys: true,
   151  			})
   152  			if err != nil {
   153  				return false, "", nil, err
   154  			}
   155  			if len(keys) == 0 {
   156  				return false, "", nil, &ErrWontSign{pubkey}
   157  			}
   158  		case twofa:
   159  			twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID)
   160  			if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
   161  				return false, "", nil, err
   162  			}
   163  			if twofaModel == nil {
   164  				return false, "", nil, &ErrWontSign{twofa}
   165  			}
   166  		}
   167  	}
   168  	return true, signingKey, sig, nil
   169  }
   170  
   171  // SignWikiCommit determines if we should sign the commits to this repository wiki
   172  func SignWikiCommit(ctx context.Context, repo *repo_model.Repository, u *user_model.User) (bool, string, *git.Signature, error) {
   173  	repoWikiPath := repo.WikiPath()
   174  	rules := signingModeFromStrings(setting.Repository.Signing.Wiki)
   175  	signingKey, sig := SigningKey(ctx, repoWikiPath)
   176  	if signingKey == "" {
   177  		return false, "", nil, &ErrWontSign{noKey}
   178  	}
   179  
   180  Loop:
   181  	for _, rule := range rules {
   182  		switch rule {
   183  		case never:
   184  			return false, "", nil, &ErrWontSign{never}
   185  		case always:
   186  			break Loop
   187  		case pubkey:
   188  			keys, err := db.Find[asymkey_model.GPGKey](ctx, asymkey_model.FindGPGKeyOptions{
   189  				OwnerID:        u.ID,
   190  				IncludeSubKeys: true,
   191  			})
   192  			if err != nil {
   193  				return false, "", nil, err
   194  			}
   195  			if len(keys) == 0 {
   196  				return false, "", nil, &ErrWontSign{pubkey}
   197  			}
   198  		case twofa:
   199  			twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID)
   200  			if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
   201  				return false, "", nil, err
   202  			}
   203  			if twofaModel == nil {
   204  				return false, "", nil, &ErrWontSign{twofa}
   205  			}
   206  		case parentSigned:
   207  			gitRepo, err := gitrepo.OpenWikiRepository(ctx, repo)
   208  			if err != nil {
   209  				return false, "", nil, err
   210  			}
   211  			defer gitRepo.Close()
   212  			commit, err := gitRepo.GetCommit("HEAD")
   213  			if err != nil {
   214  				return false, "", nil, err
   215  			}
   216  			if commit.Signature == nil {
   217  				return false, "", nil, &ErrWontSign{parentSigned}
   218  			}
   219  			verification := asymkey_model.ParseCommitWithSignature(ctx, commit)
   220  			if !verification.Verified {
   221  				return false, "", nil, &ErrWontSign{parentSigned}
   222  			}
   223  		}
   224  	}
   225  	return true, signingKey, sig, nil
   226  }
   227  
   228  // SignCRUDAction determines if we should sign a CRUD commit to this repository
   229  func SignCRUDAction(ctx context.Context, repoPath string, u *user_model.User, tmpBasePath, parentCommit string) (bool, string, *git.Signature, error) {
   230  	rules := signingModeFromStrings(setting.Repository.Signing.CRUDActions)
   231  	signingKey, sig := SigningKey(ctx, repoPath)
   232  	if signingKey == "" {
   233  		return false, "", nil, &ErrWontSign{noKey}
   234  	}
   235  
   236  Loop:
   237  	for _, rule := range rules {
   238  		switch rule {
   239  		case never:
   240  			return false, "", nil, &ErrWontSign{never}
   241  		case always:
   242  			break Loop
   243  		case pubkey:
   244  			keys, err := db.Find[asymkey_model.GPGKey](ctx, asymkey_model.FindGPGKeyOptions{
   245  				OwnerID:        u.ID,
   246  				IncludeSubKeys: true,
   247  			})
   248  			if err != nil {
   249  				return false, "", nil, err
   250  			}
   251  			if len(keys) == 0 {
   252  				return false, "", nil, &ErrWontSign{pubkey}
   253  			}
   254  		case twofa:
   255  			twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID)
   256  			if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
   257  				return false, "", nil, err
   258  			}
   259  			if twofaModel == nil {
   260  				return false, "", nil, &ErrWontSign{twofa}
   261  			}
   262  		case parentSigned:
   263  			gitRepo, err := git.OpenRepository(ctx, tmpBasePath)
   264  			if err != nil {
   265  				return false, "", nil, err
   266  			}
   267  			defer gitRepo.Close()
   268  			commit, err := gitRepo.GetCommit(parentCommit)
   269  			if err != nil {
   270  				return false, "", nil, err
   271  			}
   272  			if commit.Signature == nil {
   273  				return false, "", nil, &ErrWontSign{parentSigned}
   274  			}
   275  			verification := asymkey_model.ParseCommitWithSignature(ctx, commit)
   276  			if !verification.Verified {
   277  				return false, "", nil, &ErrWontSign{parentSigned}
   278  			}
   279  		}
   280  	}
   281  	return true, signingKey, sig, nil
   282  }
   283  
   284  // SignMerge determines if we should sign a PR merge commit to the base repository
   285  func SignMerge(ctx context.Context, pr *issues_model.PullRequest, u *user_model.User, tmpBasePath, baseCommit, headCommit string) (bool, string, *git.Signature, error) {
   286  	if err := pr.LoadBaseRepo(ctx); err != nil {
   287  		log.Error("Unable to get Base Repo for pull request")
   288  		return false, "", nil, err
   289  	}
   290  	repo := pr.BaseRepo
   291  
   292  	signingKey, signer := SigningKey(ctx, repo.RepoPath())
   293  	if signingKey == "" {
   294  		return false, "", nil, &ErrWontSign{noKey}
   295  	}
   296  	rules := signingModeFromStrings(setting.Repository.Signing.Merges)
   297  
   298  	var gitRepo *git.Repository
   299  	var err error
   300  
   301  Loop:
   302  	for _, rule := range rules {
   303  		switch rule {
   304  		case never:
   305  			return false, "", nil, &ErrWontSign{never}
   306  		case always:
   307  			break Loop
   308  		case pubkey:
   309  			keys, err := db.Find[asymkey_model.GPGKey](ctx, asymkey_model.FindGPGKeyOptions{
   310  				OwnerID:        u.ID,
   311  				IncludeSubKeys: true,
   312  			})
   313  			if err != nil {
   314  				return false, "", nil, err
   315  			}
   316  			if len(keys) == 0 {
   317  				return false, "", nil, &ErrWontSign{pubkey}
   318  			}
   319  		case twofa:
   320  			twofaModel, err := auth.GetTwoFactorByUID(ctx, u.ID)
   321  			if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
   322  				return false, "", nil, err
   323  			}
   324  			if twofaModel == nil {
   325  				return false, "", nil, &ErrWontSign{twofa}
   326  			}
   327  		case approved:
   328  			protectedBranch, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, pr.BaseBranch)
   329  			if err != nil {
   330  				return false, "", nil, err
   331  			}
   332  			if protectedBranch == nil {
   333  				return false, "", nil, &ErrWontSign{approved}
   334  			}
   335  			if issues_model.GetGrantedApprovalsCount(ctx, protectedBranch, pr) < 1 {
   336  				return false, "", nil, &ErrWontSign{approved}
   337  			}
   338  		case baseSigned:
   339  			if gitRepo == nil {
   340  				gitRepo, err = git.OpenRepository(ctx, tmpBasePath)
   341  				if err != nil {
   342  					return false, "", nil, err
   343  				}
   344  				defer gitRepo.Close()
   345  			}
   346  			commit, err := gitRepo.GetCommit(baseCommit)
   347  			if err != nil {
   348  				return false, "", nil, err
   349  			}
   350  			verification := asymkey_model.ParseCommitWithSignature(ctx, commit)
   351  			if !verification.Verified {
   352  				return false, "", nil, &ErrWontSign{baseSigned}
   353  			}
   354  		case headSigned:
   355  			if gitRepo == nil {
   356  				gitRepo, err = git.OpenRepository(ctx, tmpBasePath)
   357  				if err != nil {
   358  					return false, "", nil, err
   359  				}
   360  				defer gitRepo.Close()
   361  			}
   362  			commit, err := gitRepo.GetCommit(headCommit)
   363  			if err != nil {
   364  				return false, "", nil, err
   365  			}
   366  			verification := asymkey_model.ParseCommitWithSignature(ctx, commit)
   367  			if !verification.Verified {
   368  				return false, "", nil, &ErrWontSign{headSigned}
   369  			}
   370  		case commitsSigned:
   371  			if gitRepo == nil {
   372  				gitRepo, err = git.OpenRepository(ctx, tmpBasePath)
   373  				if err != nil {
   374  					return false, "", nil, err
   375  				}
   376  				defer gitRepo.Close()
   377  			}
   378  			commit, err := gitRepo.GetCommit(headCommit)
   379  			if err != nil {
   380  				return false, "", nil, err
   381  			}
   382  			verification := asymkey_model.ParseCommitWithSignature(ctx, commit)
   383  			if !verification.Verified {
   384  				return false, "", nil, &ErrWontSign{commitsSigned}
   385  			}
   386  			// need to work out merge-base
   387  			mergeBaseCommit, _, err := gitRepo.GetMergeBase("", baseCommit, headCommit)
   388  			if err != nil {
   389  				return false, "", nil, err
   390  			}
   391  			commitList, err := commit.CommitsBeforeUntil(mergeBaseCommit)
   392  			if err != nil {
   393  				return false, "", nil, err
   394  			}
   395  			for _, commit := range commitList {
   396  				verification := asymkey_model.ParseCommitWithSignature(ctx, commit)
   397  				if !verification.Verified {
   398  					return false, "", nil, &ErrWontSign{commitsSigned}
   399  				}
   400  			}
   401  		}
   402  	}
   403  	return true, signingKey, signer, nil
   404  }