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