forgejo/services/asymkey/sign.go
zeripath 5cb0c9aa0d
Propagate context and ensure git commands run in request context ()
This PR continues the work in  by progressively ensuring that git
commands run within the request context.

This now means that the if there is a git repo already open in the context it will be used instead of reopening it.

Signed-off-by: Andrew Thornton <art27@cantab.net>
2022-01-19 23:26:57 +00:00

389 lines
11 KiB
Go

// Copyright 2021 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 asymkey
import (
"context"
"fmt"
"strings"
"code.gitea.io/gitea/models"
asymkey_model "code.gitea.io/gitea/models/asymkey"
"code.gitea.io/gitea/models/auth"
"code.gitea.io/gitea/models/db"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/process"
"code.gitea.io/gitea/modules/setting"
)
type signingMode string
const (
never signingMode = "never"
always signingMode = "always"
pubkey signingMode = "pubkey"
twofa signingMode = "twofa"
parentSigned signingMode = "parentsigned"
baseSigned signingMode = "basesigned"
headSigned signingMode = "headsigned"
commitsSigned signingMode = "commitssigned"
approved signingMode = "approved"
noKey signingMode = "nokey"
)
func signingModeFromStrings(modeStrings []string) []signingMode {
returnable := make([]signingMode, 0, len(modeStrings))
for _, mode := range modeStrings {
signMode := signingMode(strings.ToLower(strings.TrimSpace(mode)))
switch signMode {
case never:
return []signingMode{never}
case always:
return []signingMode{always}
case pubkey:
fallthrough
case twofa:
fallthrough
case parentSigned:
fallthrough
case baseSigned:
fallthrough
case headSigned:
fallthrough
case approved:
fallthrough
case commitsSigned:
returnable = append(returnable, signMode)
}
}
if len(returnable) == 0 {
return []signingMode{never}
}
return returnable
}
// ErrWontSign explains the first reason why a commit would not be signed
// There may be other reasons - this is just the first reason found
type ErrWontSign struct {
Reason signingMode
}
func (e *ErrWontSign) Error() string {
return fmt.Sprintf("wont sign: %s", e.Reason)
}
// IsErrWontSign checks if an error is a ErrWontSign
func IsErrWontSign(err error) bool {
_, ok := err.(*ErrWontSign)
return ok
}
// SigningKey returns the KeyID and git Signature for the repo
func SigningKey(ctx context.Context, repoPath string) (string, *git.Signature) {
if setting.Repository.Signing.SigningKey == "none" {
return "", nil
}
if setting.Repository.Signing.SigningKey == "default" || setting.Repository.Signing.SigningKey == "" {
// Can ignore the error here as it means that commit.gpgsign is not set
value, _ := git.NewCommandContext(ctx, "config", "--get", "commit.gpgsign").RunInDir(repoPath)
sign, valid := git.ParseBool(strings.TrimSpace(value))
if !sign || !valid {
return "", nil
}
signingKey, _ := git.NewCommandContext(ctx, "config", "--get", "user.signingkey").RunInDir(repoPath)
signingName, _ := git.NewCommandContext(ctx, "config", "--get", "user.name").RunInDir(repoPath)
signingEmail, _ := git.NewCommandContext(ctx, "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, &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(ctx context.Context, repoPath string) (string, error) {
signingKey, _ := SigningKey(ctx, repoPath)
if signingKey == "" {
return "", nil
}
content, stderr, err := process.GetManager().ExecDir(ctx, -1, repoPath,
"gpg --export -a", "gpg", "--export", "-a", signingKey)
if err != nil {
log.Error("Unable to get default signing key in %s: %s, %s, %v", repoPath, signingKey, stderr, err)
return "", err
}
return content, nil
}
// SignInitialCommit determines if we should sign the initial commit to this repository
func SignInitialCommit(ctx context.Context, repoPath string, u *user_model.User) (bool, string, *git.Signature, error) {
rules := signingModeFromStrings(setting.Repository.Signing.InitialCommit)
signingKey, sig := SigningKey(ctx, repoPath)
if signingKey == "" {
return false, "", nil, &ErrWontSign{noKey}
}
Loop:
for _, rule := range rules {
switch rule {
case never:
return false, "", nil, &ErrWontSign{never}
case always:
break Loop
case pubkey:
keys, err := asymkey_model.ListGPGKeys(db.DefaultContext, u.ID, db.ListOptions{})
if err != nil {
return false, "", nil, err
}
if len(keys) == 0 {
return false, "", nil, &ErrWontSign{pubkey}
}
case twofa:
twofaModel, err := auth.GetTwoFactorByUID(u.ID)
if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
return false, "", nil, err
}
if twofaModel == nil {
return false, "", nil, &ErrWontSign{twofa}
}
}
}
return true, signingKey, sig, nil
}
// SignWikiCommit determines if we should sign the commits to this repository wiki
func SignWikiCommit(ctx context.Context, repoWikiPath string, u *user_model.User) (bool, string, *git.Signature, error) {
rules := signingModeFromStrings(setting.Repository.Signing.Wiki)
signingKey, sig := SigningKey(ctx, repoWikiPath)
if signingKey == "" {
return false, "", nil, &ErrWontSign{noKey}
}
Loop:
for _, rule := range rules {
switch rule {
case never:
return false, "", nil, &ErrWontSign{never}
case always:
break Loop
case pubkey:
keys, err := asymkey_model.ListGPGKeys(db.DefaultContext, u.ID, db.ListOptions{})
if err != nil {
return false, "", nil, err
}
if len(keys) == 0 {
return false, "", nil, &ErrWontSign{pubkey}
}
case twofa:
twofaModel, err := auth.GetTwoFactorByUID(u.ID)
if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
return false, "", nil, err
}
if twofaModel == nil {
return false, "", nil, &ErrWontSign{twofa}
}
case parentSigned:
gitRepo, err := git.OpenRepositoryCtx(ctx, repoWikiPath)
if err != nil {
return false, "", nil, err
}
defer gitRepo.Close()
commit, err := gitRepo.GetCommit("HEAD")
if err != nil {
return false, "", nil, err
}
if commit.Signature == nil {
return false, "", nil, &ErrWontSign{parentSigned}
}
verification := asymkey_model.ParseCommitWithSignature(commit)
if !verification.Verified {
return false, "", nil, &ErrWontSign{parentSigned}
}
}
}
return true, signingKey, sig, nil
}
// SignCRUDAction determines if we should sign a CRUD commit to this repository
func SignCRUDAction(ctx context.Context, repoPath string, u *user_model.User, tmpBasePath, parentCommit string) (bool, string, *git.Signature, error) {
rules := signingModeFromStrings(setting.Repository.Signing.CRUDActions)
signingKey, sig := SigningKey(ctx, repoPath)
if signingKey == "" {
return false, "", nil, &ErrWontSign{noKey}
}
Loop:
for _, rule := range rules {
switch rule {
case never:
return false, "", nil, &ErrWontSign{never}
case always:
break Loop
case pubkey:
keys, err := asymkey_model.ListGPGKeys(db.DefaultContext, u.ID, db.ListOptions{})
if err != nil {
return false, "", nil, err
}
if len(keys) == 0 {
return false, "", nil, &ErrWontSign{pubkey}
}
case twofa:
twofaModel, err := auth.GetTwoFactorByUID(u.ID)
if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
return false, "", nil, err
}
if twofaModel == nil {
return false, "", nil, &ErrWontSign{twofa}
}
case parentSigned:
gitRepo, err := git.OpenRepositoryCtx(ctx, tmpBasePath)
if err != nil {
return false, "", nil, err
}
defer gitRepo.Close()
commit, err := gitRepo.GetCommit(parentCommit)
if err != nil {
return false, "", nil, err
}
if commit.Signature == nil {
return false, "", nil, &ErrWontSign{parentSigned}
}
verification := asymkey_model.ParseCommitWithSignature(commit)
if !verification.Verified {
return false, "", nil, &ErrWontSign{parentSigned}
}
}
}
return true, signingKey, sig, nil
}
// SignMerge determines if we should sign a PR merge commit to the base repository
func SignMerge(ctx context.Context, pr *models.PullRequest, u *user_model.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, "", nil, err
}
repo := pr.BaseRepo
signingKey, signer := SigningKey(ctx, repo.RepoPath())
if signingKey == "" {
return false, "", nil, &ErrWontSign{noKey}
}
rules := signingModeFromStrings(setting.Repository.Signing.Merges)
var gitRepo *git.Repository
var err error
Loop:
for _, rule := range rules {
switch rule {
case never:
return false, "", nil, &ErrWontSign{never}
case always:
break Loop
case pubkey:
keys, err := asymkey_model.ListGPGKeys(db.DefaultContext, u.ID, db.ListOptions{})
if err != nil {
return false, "", nil, err
}
if len(keys) == 0 {
return false, "", nil, &ErrWontSign{pubkey}
}
case twofa:
twofaModel, err := auth.GetTwoFactorByUID(u.ID)
if err != nil && !auth.IsErrTwoFactorNotEnrolled(err) {
return false, "", nil, err
}
if twofaModel == nil {
return false, "", nil, &ErrWontSign{twofa}
}
case approved:
protectedBranch, err := models.GetProtectedBranchBy(repo.ID, pr.BaseBranch)
if err != nil {
return false, "", nil, err
}
if protectedBranch == nil {
return false, "", nil, &ErrWontSign{approved}
}
if protectedBranch.GetGrantedApprovalsCount(pr) < 1 {
return false, "", nil, &ErrWontSign{approved}
}
case baseSigned:
if gitRepo == nil {
gitRepo, err = git.OpenRepositoryCtx(ctx, tmpBasePath)
if err != nil {
return false, "", nil, err
}
defer gitRepo.Close()
}
commit, err := gitRepo.GetCommit(baseCommit)
if err != nil {
return false, "", nil, err
}
verification := asymkey_model.ParseCommitWithSignature(commit)
if !verification.Verified {
return false, "", nil, &ErrWontSign{baseSigned}
}
case headSigned:
if gitRepo == nil {
gitRepo, err = git.OpenRepositoryCtx(ctx, tmpBasePath)
if err != nil {
return false, "", nil, err
}
defer gitRepo.Close()
}
commit, err := gitRepo.GetCommit(headCommit)
if err != nil {
return false, "", nil, err
}
verification := asymkey_model.ParseCommitWithSignature(commit)
if !verification.Verified {
return false, "", nil, &ErrWontSign{headSigned}
}
case commitsSigned:
if gitRepo == nil {
gitRepo, err = git.OpenRepositoryCtx(ctx, tmpBasePath)
if err != nil {
return false, "", nil, err
}
defer gitRepo.Close()
}
commit, err := gitRepo.GetCommit(headCommit)
if err != nil {
return false, "", nil, err
}
verification := asymkey_model.ParseCommitWithSignature(commit)
if !verification.Verified {
return false, "", nil, &ErrWontSign{commitsSigned}
}
// need to work out merge-base
mergeBaseCommit, _, err := gitRepo.GetMergeBase("", baseCommit, headCommit)
if err != nil {
return false, "", nil, err
}
commitList, err := commit.CommitsBeforeUntil(mergeBaseCommit)
if err != nil {
return false, "", nil, err
}
for _, commit := range commitList {
verification := asymkey_model.ParseCommitWithSignature(commit)
if !verification.Verified {
return false, "", nil, &ErrWontSign{commitsSigned}
}
}
}
}
return true, signingKey, signer, nil
}