Sign merges, CRUD, Wiki and Repository initialisation with gpg key (#7631)
This PR fixes #7598 by providing a configurable way of signing commits across the Gitea instance. Per repository configurability and import/generation of trusted secure keys is not provided by this PR - from a security PoV that's probably impossible to do properly. Similarly web-signing, that is asking the user to sign something, is not implemented - this could be done at a later stage however. ## Features - [x] If commit.gpgsign is set in .gitconfig sign commits and files created through repofiles. (merges should already have been signed.) - [x] Verify commits signed with the default gpg as valid - [x] Signer, Committer and Author can all be different - [x] Allow signer to be arbitrarily different - We still require the key to have an activated email on Gitea. A more complete implementation would be to use a keyserver and mark external-or-unactivated with an "unknown" trust level icon. - [x] Add a signing-key.gpg endpoint to get the default gpg pub key if available - Rather than add a fake web-flow user I've added this as an endpoint on /api/v1/signing-key.gpg - [x] Try to match the default key with a user on gitea - this is done at verification time - [x] Make things configurable? - app.ini configuration done - [x] when checking commits are signed need to check if they're actually verifiable too - [x] Add documentation I have decided that adjusting the docker to create a default gpg key is not the correct thing to do and therefore have not implemented this.
This commit is contained in:
		
					parent
					
						
							
								1b72690cb8
							
						
					
				
			
			
				commit
				
					
						fcb535c5c3
					
				
			
		
					 40 changed files with 1630 additions and 124 deletions
				
			
		
							
								
								
									
										4
									
								
								Makefile
									
										
									
									
									
								
							
							
						
						
									
										4
									
								
								Makefile
									
										
									
									
									
								
							| 
						 | 
				
			
			@ -168,6 +168,10 @@ fmt-check:
 | 
			
		|||
test:
 | 
			
		||||
	GO111MODULE=on $(GO) test -mod=vendor -tags='sqlite sqlite_unlock_notify' $(PACKAGES)
 | 
			
		||||
 | 
			
		||||
.PHONY: test\#%
 | 
			
		||||
test\#%:
 | 
			
		||||
	GO111MODULE=on $(GO) test -mod=vendor -tags='sqlite sqlite_unlock_notify' -run $* $(PACKAGES)
 | 
			
		||||
 | 
			
		||||
.PHONY: coverage
 | 
			
		||||
coverage:
 | 
			
		||||
	@hash gocovmerge > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -74,6 +74,37 @@ WORK_IN_PROGRESS_PREFIXES=WIP:,[WIP]
 | 
			
		|||
; List of reasons why a Pull Request or Issue can be locked
 | 
			
		||||
LOCK_REASONS=Too heated,Off-topic,Resolved,Spam
 | 
			
		||||
 | 
			
		||||
[repository.signing]
 | 
			
		||||
; GPG key to use to sign commits, Defaults to the default - that is the value of git config --get user.signingkey
 | 
			
		||||
; run in the context of the RUN_USER
 | 
			
		||||
; Switch to none to stop signing completely
 | 
			
		||||
SIGNING_KEY = default
 | 
			
		||||
; If a SIGNING_KEY ID is provided and is not set to default, use the provided Name and Email address as the signer.
 | 
			
		||||
; These should match a publicized name and email address for the key. (When SIGNING_KEY is default these are set to
 | 
			
		||||
; the results of git config --get user.name and git config --get user.email respectively and can only be overrided
 | 
			
		||||
; by setting the SIGNING_KEY ID to the correct ID.)
 | 
			
		||||
SIGNING_NAME =
 | 
			
		||||
SIGNING_EMAIL =
 | 
			
		||||
; Determines when gitea should sign the initial commit when creating a repository
 | 
			
		||||
; Either:
 | 
			
		||||
; - never
 | 
			
		||||
; - pubkey: only sign if the user has a pubkey
 | 
			
		||||
; - twofa: only sign if the user has logged in with twofa
 | 
			
		||||
; - always
 | 
			
		||||
; options other than none and always can be combined as comma separated list
 | 
			
		||||
INITIAL_COMMIT = always
 | 
			
		||||
; Determines when to sign for CRUD actions
 | 
			
		||||
; - as above
 | 
			
		||||
; - parentsigned: requires that the parent commit is signed.
 | 
			
		||||
CRUD_ACTIONS = pubkey, twofa, parentsigned
 | 
			
		||||
; Determines when to sign Wiki commits
 | 
			
		||||
; - as above
 | 
			
		||||
WIKI = never
 | 
			
		||||
; Determines when to sign on merges
 | 
			
		||||
; - basesigned: require that the parent of commit on the base repo is signed.
 | 
			
		||||
; - commitssigned: require that all the commits in the head branch are signed.
 | 
			
		||||
MERGES = pubkey, twofa, basesigned, commitssigned
 | 
			
		||||
 | 
			
		||||
[cors]
 | 
			
		||||
; More information about CORS can be found here: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#The_HTTP_response_headers
 | 
			
		||||
; enable cors headers (disabled by default)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -76,6 +76,25 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
 | 
			
		|||
 | 
			
		||||
- `LOCK_REASONS`: **Too heated,Off-topic,Resolved,Spam**: A list of reasons why a Pull Request or Issue can be locked
 | 
			
		||||
 | 
			
		||||
### Repository - Signing (`repository.signing`)
 | 
			
		||||
 | 
			
		||||
- `SIGNING_KEY`: **default**: \[none, KEYID, default \]: Key to sign with.
 | 
			
		||||
- `SIGNING_NAME` & `SIGNING_EMAIL`: if a KEYID is provided as the `SIGNING_KEY`, use these as the Name and Email address of the signer. These should match publicized name and email address for the key.
 | 
			
		||||
- `INITIAL_COMMIT`: **always**: \[never, pubkey, twofa, always\]: Sign initial commit.
 | 
			
		||||
  - `never`: Never sign
 | 
			
		||||
  - `pubkey`: Only sign if the user has a public key
 | 
			
		||||
  - `twofa`: Only sign if the user is logged in with twofa
 | 
			
		||||
  - `always`: Always sign
 | 
			
		||||
  - Options other than `never` and `always` can be combined as a comma separated list.
 | 
			
		||||
- `WIKI`: **never**: \[never, pubkey, twofa, always, parentsigned\]: Sign commits to wiki.
 | 
			
		||||
- `CRUD_ACTIONS`: **pubkey, twofa, parentsigned**: \[never, pubkey, twofa, parentsigned, always\]: Sign CRUD actions.
 | 
			
		||||
  - Options as above, with the addition of:
 | 
			
		||||
  - `parentsigned`: Only sign if the parent commit is signed.
 | 
			
		||||
- `MERGES`: **pubkey, twofa, basesigned, commitssigned**: \[never, pubkey, twofa, basesigned, commitssigned, always\]: Sign merges.
 | 
			
		||||
  - `basesigned`: Only sign if the parent commit in the base repo is signed.
 | 
			
		||||
  - `headsigned`: Only sign if the head commit in the head branch is signed.
 | 
			
		||||
  - `commitssigned`: Only sign if all the commits in the head branch to the merge point are signed.
 | 
			
		||||
 | 
			
		||||
## CORS (`cors`)
 | 
			
		||||
 | 
			
		||||
- `ENABLED`: **false**: enable cors headers (disabled by default)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										162
									
								
								docs/content/doc/advanced/signing.en-us.md
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										162
									
								
								docs/content/doc/advanced/signing.en-us.md
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,162 @@
 | 
			
		|||
---
 | 
			
		||||
date: "2019-08-17T10:20:00+01:00"
 | 
			
		||||
title: "GPG Commit Signatures"
 | 
			
		||||
slug: "signing"
 | 
			
		||||
weight: 20
 | 
			
		||||
toc: false
 | 
			
		||||
draft: false
 | 
			
		||||
menu:
 | 
			
		||||
  sidebar:
 | 
			
		||||
    parent: "advanced"
 | 
			
		||||
    name: "GPG Commit Signatures"
 | 
			
		||||
    weight: 20
 | 
			
		||||
    identifier: "signing"
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
# GPG Commit Signatures
 | 
			
		||||
 | 
			
		||||
Gitea will verify GPG commit signatures in the provided tree by
 | 
			
		||||
checking if the commits are signed by a key within the gitea database,
 | 
			
		||||
or if the commit matches the default key for git.
 | 
			
		||||
 | 
			
		||||
Keys are not checked to determine if they have expired or revoked.
 | 
			
		||||
Keys are also not checked with keyservers.
 | 
			
		||||
 | 
			
		||||
A commit will be marked with a grey unlocked icon if no key can be
 | 
			
		||||
found to verify it. If a commit is marked with a red unlocked icon,
 | 
			
		||||
it is reported to be signed with a key with an id.
 | 
			
		||||
 | 
			
		||||
Please note: The signer of a commit does not have to be an author or
 | 
			
		||||
committer of a commit.
 | 
			
		||||
 | 
			
		||||
This functionality requires git >= 1.7.9 but for full functionality
 | 
			
		||||
this requires git >= 2.0.0.
 | 
			
		||||
 | 
			
		||||
## Automatic Signing
 | 
			
		||||
 | 
			
		||||
There are a number of places where Gitea will generate commits itself:
 | 
			
		||||
 | 
			
		||||
* Repository Initialisation
 | 
			
		||||
* Wiki Changes
 | 
			
		||||
* CRUD actions using the editor or the API
 | 
			
		||||
* Merges from Pull Requests
 | 
			
		||||
 | 
			
		||||
Depending on configuration and server trust you may want Gitea to
 | 
			
		||||
sign these commits.
 | 
			
		||||
 | 
			
		||||
## General Configuration
 | 
			
		||||
 | 
			
		||||
Gitea's configuration for signing can be found with the
 | 
			
		||||
`[repository.signing]` section of `app.ini`:
 | 
			
		||||
 | 
			
		||||
```ini
 | 
			
		||||
...
 | 
			
		||||
[repository.signing]
 | 
			
		||||
SIGNING_KEY = default
 | 
			
		||||
SIGNING_NAME =
 | 
			
		||||
SIGNING_EMAIL =
 | 
			
		||||
INITIAL_COMMIT = always
 | 
			
		||||
CRUD_ACTIONS = pubkey, twofa, parentsigned
 | 
			
		||||
WIKI = never
 | 
			
		||||
MERGES = pubkey, twofa, basesigned, commitssigned
 | 
			
		||||
 | 
			
		||||
...
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### `SIGNING_KEY`
 | 
			
		||||
 | 
			
		||||
The first option to discuss is the `SIGNING_KEY`. There are three main
 | 
			
		||||
options:
 | 
			
		||||
 | 
			
		||||
* `none` - this prevents Gitea from signing any commits
 | 
			
		||||
* `default` - Gitea will default to the key configured within
 | 
			
		||||
`git config`
 | 
			
		||||
* `KEYID` - Gitea will sign commits with the gpg key with the ID
 | 
			
		||||
`KEYID`. In this case you should provide a `SIGNING_NAME` and
 | 
			
		||||
`SIGNING_EMAIL` to be displayed for this key.
 | 
			
		||||
 | 
			
		||||
The `default` option will interrogate `git config` for
 | 
			
		||||
`commit.gpgsign` option - if this is set, then it will use the results
 | 
			
		||||
of the `user.signingkey`, `user.name` and `user.email` as appropriate.
 | 
			
		||||
 | 
			
		||||
Please note: by adjusting git's `config` file within Gitea's
 | 
			
		||||
repositories, `SIGNING_KEY=default` could be used to provide different
 | 
			
		||||
signing keys on a per-repository basis. However, this is cleary not an
 | 
			
		||||
ideal UI and therefore subject to change.
 | 
			
		||||
 | 
			
		||||
### `INITIAL_COMMIT`
 | 
			
		||||
 | 
			
		||||
This option determines whether Gitea should sign the initial commit
 | 
			
		||||
when creating a repository. The possible values are:
 | 
			
		||||
 | 
			
		||||
* `never`: Never sign
 | 
			
		||||
* `pubkey`: Only sign if the user has a public key
 | 
			
		||||
* `twofa`: Only sign if the user logs in with two factor authentication
 | 
			
		||||
* `always`: Always sign
 | 
			
		||||
 | 
			
		||||
Options other than `never` and `always` can be combined as a comma
 | 
			
		||||
separated list.
 | 
			
		||||
 | 
			
		||||
### `WIKI`
 | 
			
		||||
 | 
			
		||||
This options determines if Gitea should sign commits to the Wiki.
 | 
			
		||||
The possible values are:
 | 
			
		||||
 | 
			
		||||
* `never`: Never sign
 | 
			
		||||
* `pubkey`: Only sign if the user has a public key
 | 
			
		||||
* `twofa`: Only sign if the user logs in with two factor authentication
 | 
			
		||||
* `parentsigned`: Only sign if the parent commit is signed.
 | 
			
		||||
* `always`: Always sign
 | 
			
		||||
 | 
			
		||||
Options other than `never` and `always` can be combined as a comma
 | 
			
		||||
separated list.
 | 
			
		||||
 | 
			
		||||
### `CRUD_ACTIONS`
 | 
			
		||||
 | 
			
		||||
This option determines if Gitea should sign commits from the web
 | 
			
		||||
editor or API CRUD actions. The possible values are:
 | 
			
		||||
 | 
			
		||||
* `never`: Never sign
 | 
			
		||||
* `pubkey`: Only sign if the user has a public key
 | 
			
		||||
* `twofa`: Only sign if the user logs in with two factor authentication
 | 
			
		||||
* `parentsigned`: Only sign if the parent commit is signed.
 | 
			
		||||
* `always`: Always sign
 | 
			
		||||
 | 
			
		||||
Options other than `never` and `always` can be combined as a comma
 | 
			
		||||
separated list.
 | 
			
		||||
 | 
			
		||||
### `MERGES`
 | 
			
		||||
 | 
			
		||||
This option determines if Gitea should sign merge commits from PRs.
 | 
			
		||||
The possible options are:
 | 
			
		||||
 | 
			
		||||
* `never`: Never sign
 | 
			
		||||
* `pubkey`: Only sign if the user has a public key
 | 
			
		||||
* `twofa`: Only sign if the user logs in with two factor authentication
 | 
			
		||||
* `basesigned`: Only sign if the parent commit in the base repo is signed.
 | 
			
		||||
* `headsigned`: Only sign if the head commit in the head branch is signed.
 | 
			
		||||
* `commitssigned`: Only sign if all the commits in the head branch to the merge point are signed.
 | 
			
		||||
* `always`: Always sign
 | 
			
		||||
 | 
			
		||||
Options other than `never` and `always` can be combined as a comma
 | 
			
		||||
separated list.
 | 
			
		||||
 | 
			
		||||
## Installing and generating a GPG key for Gitea
 | 
			
		||||
 | 
			
		||||
It is up to a server administrator to determine how best to install
 | 
			
		||||
a signing key. Gitea generates all its commits using the server `git`
 | 
			
		||||
command at present - and therefore the server `gpg` will be used for
 | 
			
		||||
signing (if configured.) Administrators should review best-practices
 | 
			
		||||
for gpg - in particular it is probably advisable to only install a
 | 
			
		||||
signing secret subkey without the master signing and certifying secret
 | 
			
		||||
key.
 | 
			
		||||
 | 
			
		||||
## Obtaining the Public Key of the Signing Key
 | 
			
		||||
 | 
			
		||||
The public key used to sign Gitea's commits can be obtained from the API at:
 | 
			
		||||
 | 
			
		||||
```/api/v1/signing-key.gpg```
 | 
			
		||||
 | 
			
		||||
In cases where there is a repository specific key this can be obtained from:
 | 
			
		||||
 | 
			
		||||
```/api/v1/repos/:username/:reponame/signing-key.gpg```
 | 
			
		||||
| 
						 | 
				
			
			@ -231,3 +231,38 @@ func doAPIMergePullRequest(ctx APITestContext, owner, repo string, index int64)
 | 
			
		|||
		ctx.Session.MakeRequest(t, req, 200)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func doAPIGetBranch(ctx APITestContext, branch string, callback ...func(*testing.T, api.Branch)) func(*testing.T) {
 | 
			
		||||
	return func(t *testing.T) {
 | 
			
		||||
		req := NewRequestf(t, "GET", "/api/v1/repos/%s/%s/branches/%s?token=%s", ctx.Username, ctx.Reponame, branch, ctx.Token)
 | 
			
		||||
		if ctx.ExpectedCode != 0 {
 | 
			
		||||
			ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		resp := ctx.Session.MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
 | 
			
		||||
		var branch api.Branch
 | 
			
		||||
		DecodeJSON(t, resp, &branch)
 | 
			
		||||
		if len(callback) > 0 {
 | 
			
		||||
			callback[0](t, branch)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func doAPICreateFile(ctx APITestContext, treepath string, options *api.CreateFileOptions, callback ...func(*testing.T, api.FileResponse)) func(*testing.T) {
 | 
			
		||||
	return func(t *testing.T) {
 | 
			
		||||
		url := fmt.Sprintf("/api/v1/repos/%s/%s/contents/%s?token=%s", ctx.Username, ctx.Reponame, treepath, ctx.Token)
 | 
			
		||||
		req := NewRequestWithJSON(t, "POST", url, &options)
 | 
			
		||||
		if ctx.ExpectedCode != 0 {
 | 
			
		||||
			ctx.Session.MakeRequest(t, req, ctx.ExpectedCode)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		resp := ctx.Session.MakeRequest(t, req, http.StatusCreated)
 | 
			
		||||
 | 
			
		||||
		var contents api.FileResponse
 | 
			
		||||
		DecodeJSON(t, resp, &contents)
 | 
			
		||||
		if len(callback) > 0 {
 | 
			
		||||
			callback[0](t, contents)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -91,7 +91,7 @@ func getExpectedFileResponseForCreate(commitID, treePath string) *api.FileRespon
 | 
			
		|||
		},
 | 
			
		||||
		Verification: &api.PayloadCommitVerification{
 | 
			
		||||
			Verified:  false,
 | 
			
		||||
			Reason:    "unsigned",
 | 
			
		||||
			Reason:    "gpg.error.not_signed_commit",
 | 
			
		||||
			Signature: "",
 | 
			
		||||
			Payload:   "",
 | 
			
		||||
		},
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -94,7 +94,7 @@ func getExpectedFileResponseForUpdate(commitID, treePath string) *api.FileRespon
 | 
			
		|||
		},
 | 
			
		||||
		Verification: &api.PayloadCommitVerification{
 | 
			
		||||
			Verified:  false,
 | 
			
		||||
			Reason:    "unsigned",
 | 
			
		||||
			Reason:    "gpg.error.not_signed_commit",
 | 
			
		||||
			Signature: "",
 | 
			
		||||
			Payload:   "",
 | 
			
		||||
		},
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										252
									
								
								integrations/gpg_git_test.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										252
									
								
								integrations/gpg_git_test.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,252 @@
 | 
			
		|||
// Copyright 2019 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 integrations
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"encoding/base64"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models"
 | 
			
		||||
	"code.gitea.io/gitea/modules/process"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	api "code.gitea.io/gitea/modules/structs"
 | 
			
		||||
	"github.com/stretchr/testify/assert"
 | 
			
		||||
	"golang.org/x/crypto/openpgp"
 | 
			
		||||
	"golang.org/x/crypto/openpgp/armor"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestGPGGit(t *testing.T) {
 | 
			
		||||
	onGiteaRun(t, testGPGGit)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func testGPGGit(t *testing.T, u *url.URL) {
 | 
			
		||||
	username := "user2"
 | 
			
		||||
	baseAPITestContext := NewAPITestContext(t, username, "repo1")
 | 
			
		||||
 | 
			
		||||
	u.Path = baseAPITestContext.GitPath()
 | 
			
		||||
 | 
			
		||||
	// OK Set a new GPG home
 | 
			
		||||
	tmpDir, err := ioutil.TempDir("", "temp-gpg")
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	defer os.RemoveAll(tmpDir)
 | 
			
		||||
 | 
			
		||||
	err = os.Chmod(tmpDir, 0700)
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	oldGNUPGHome := os.Getenv("GNUPGHOME")
 | 
			
		||||
	err = os.Setenv("GNUPGHOME", tmpDir)
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
	defer os.Setenv("GNUPGHOME", oldGNUPGHome)
 | 
			
		||||
 | 
			
		||||
	// Need to create a root key
 | 
			
		||||
	rootKeyPair, err := createGPGKey(tmpDir, "gitea", "gitea@fake.local")
 | 
			
		||||
	assert.NoError(t, err)
 | 
			
		||||
 | 
			
		||||
	rootKeyID := rootKeyPair.PrimaryKey.KeyIdShortString()
 | 
			
		||||
 | 
			
		||||
	oldKeyID := setting.Repository.Signing.SigningKey
 | 
			
		||||
	oldName := setting.Repository.Signing.SigningName
 | 
			
		||||
	oldEmail := setting.Repository.Signing.SigningEmail
 | 
			
		||||
	defer func() {
 | 
			
		||||
		setting.Repository.Signing.SigningKey = oldKeyID
 | 
			
		||||
		setting.Repository.Signing.SigningName = oldName
 | 
			
		||||
		setting.Repository.Signing.SigningEmail = oldEmail
 | 
			
		||||
	}()
 | 
			
		||||
 | 
			
		||||
	setting.Repository.Signing.SigningKey = rootKeyID
 | 
			
		||||
	setting.Repository.Signing.SigningName = "gitea"
 | 
			
		||||
	setting.Repository.Signing.SigningEmail = "gitea@fake.local"
 | 
			
		||||
	user := models.AssertExistsAndLoadBean(t, &models.User{Name: username}).(*models.User)
 | 
			
		||||
 | 
			
		||||
	t.Run("Unsigned-Initial", func(t *testing.T) {
 | 
			
		||||
		PrintCurrentTest(t)
 | 
			
		||||
		setting.Repository.Signing.InitialCommit = []string{"never"}
 | 
			
		||||
		testCtx := NewAPITestContext(t, username, "initial-unsigned")
 | 
			
		||||
		t.Run("CreateRepository", doAPICreateRepository(testCtx, false))
 | 
			
		||||
		t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) {
 | 
			
		||||
			assert.NotNil(t, branch.Commit)
 | 
			
		||||
			assert.NotNil(t, branch.Commit.Verification)
 | 
			
		||||
			assert.False(t, branch.Commit.Verification.Verified)
 | 
			
		||||
			assert.Empty(t, branch.Commit.Verification.Signature)
 | 
			
		||||
		}))
 | 
			
		||||
		setting.Repository.Signing.CRUDActions = []string{"never"}
 | 
			
		||||
		t.Run("CreateCRUDFile-Never", crudActionCreateFile(
 | 
			
		||||
			t, testCtx, user, "master", "never", "unsigned-never.txt", func(t *testing.T, response api.FileResponse) {
 | 
			
		||||
				assert.False(t, response.Verification.Verified)
 | 
			
		||||
			}))
 | 
			
		||||
		t.Run("CreateCRUDFile-Never", crudActionCreateFile(
 | 
			
		||||
			t, testCtx, user, "never", "never2", "unsigned-never2.txt", func(t *testing.T, response api.FileResponse) {
 | 
			
		||||
				assert.False(t, response.Verification.Verified)
 | 
			
		||||
			}))
 | 
			
		||||
		setting.Repository.Signing.CRUDActions = []string{"parentsigned"}
 | 
			
		||||
		t.Run("CreateCRUDFile-ParentSigned", crudActionCreateFile(
 | 
			
		||||
			t, testCtx, user, "master", "parentsigned", "signed-parent.txt", func(t *testing.T, response api.FileResponse) {
 | 
			
		||||
				assert.False(t, response.Verification.Verified)
 | 
			
		||||
			}))
 | 
			
		||||
		t.Run("CreateCRUDFile-ParentSigned", crudActionCreateFile(
 | 
			
		||||
			t, testCtx, user, "parentsigned", "parentsigned2", "signed-parent2.txt", func(t *testing.T, response api.FileResponse) {
 | 
			
		||||
				assert.False(t, response.Verification.Verified)
 | 
			
		||||
			}))
 | 
			
		||||
		setting.Repository.Signing.CRUDActions = []string{"never"}
 | 
			
		||||
		t.Run("CreateCRUDFile-Never", crudActionCreateFile(
 | 
			
		||||
			t, testCtx, user, "parentsigned", "parentsigned-never", "unsigned-never2.txt", func(t *testing.T, response api.FileResponse) {
 | 
			
		||||
				assert.False(t, response.Verification.Verified)
 | 
			
		||||
			}))
 | 
			
		||||
		setting.Repository.Signing.CRUDActions = []string{"always"}
 | 
			
		||||
		t.Run("CreateCRUDFile-Always", crudActionCreateFile(
 | 
			
		||||
			t, testCtx, user, "master", "always", "signed-always.txt", func(t *testing.T, response api.FileResponse) {
 | 
			
		||||
				assert.True(t, response.Verification.Verified)
 | 
			
		||||
				assert.Equal(t, "gitea@fake.local", response.Verification.Signer.Email)
 | 
			
		||||
			}))
 | 
			
		||||
		t.Run("CreateCRUDFile-ParentSigned-always", crudActionCreateFile(
 | 
			
		||||
			t, testCtx, user, "parentsigned", "parentsigned-always", "signed-parent2.txt", func(t *testing.T, response api.FileResponse) {
 | 
			
		||||
				assert.True(t, response.Verification.Verified)
 | 
			
		||||
				assert.Equal(t, "gitea@fake.local", response.Verification.Signer.Email)
 | 
			
		||||
			}))
 | 
			
		||||
		setting.Repository.Signing.CRUDActions = []string{"parentsigned"}
 | 
			
		||||
		t.Run("CreateCRUDFile-Always-ParentSigned", crudActionCreateFile(
 | 
			
		||||
			t, testCtx, user, "always", "always-parentsigned", "signed-always-parentsigned.txt", func(t *testing.T, response api.FileResponse) {
 | 
			
		||||
				assert.True(t, response.Verification.Verified)
 | 
			
		||||
				assert.Equal(t, "gitea@fake.local", response.Verification.Signer.Email)
 | 
			
		||||
			}))
 | 
			
		||||
	})
 | 
			
		||||
	t.Run("AlwaysSign-Initial", func(t *testing.T) {
 | 
			
		||||
		PrintCurrentTest(t)
 | 
			
		||||
		setting.Repository.Signing.InitialCommit = []string{"always"}
 | 
			
		||||
		testCtx := NewAPITestContext(t, username, "initial-always")
 | 
			
		||||
		t.Run("CreateRepository", doAPICreateRepository(testCtx, false))
 | 
			
		||||
		t.Run("CheckMasterBranchSigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) {
 | 
			
		||||
			assert.NotNil(t, branch.Commit)
 | 
			
		||||
			assert.NotNil(t, branch.Commit.Verification)
 | 
			
		||||
			assert.True(t, branch.Commit.Verification.Verified)
 | 
			
		||||
			assert.Equal(t, "gitea@fake.local", branch.Commit.Verification.Signer.Email)
 | 
			
		||||
		}))
 | 
			
		||||
		setting.Repository.Signing.CRUDActions = []string{"never"}
 | 
			
		||||
		t.Run("CreateCRUDFile-Never", crudActionCreateFile(
 | 
			
		||||
			t, testCtx, user, "master", "never", "unsigned-never.txt", func(t *testing.T, response api.FileResponse) {
 | 
			
		||||
				assert.False(t, response.Verification.Verified)
 | 
			
		||||
			}))
 | 
			
		||||
		setting.Repository.Signing.CRUDActions = []string{"parentsigned"}
 | 
			
		||||
		t.Run("CreateCRUDFile-ParentSigned", crudActionCreateFile(
 | 
			
		||||
			t, testCtx, user, "master", "parentsigned", "signed-parent.txt", func(t *testing.T, response api.FileResponse) {
 | 
			
		||||
				assert.True(t, response.Verification.Verified)
 | 
			
		||||
				assert.Equal(t, "gitea@fake.local", response.Verification.Signer.Email)
 | 
			
		||||
			}))
 | 
			
		||||
		setting.Repository.Signing.CRUDActions = []string{"always"}
 | 
			
		||||
		t.Run("CreateCRUDFile-Always", crudActionCreateFile(
 | 
			
		||||
			t, testCtx, user, "master", "always", "signed-always.txt", func(t *testing.T, response api.FileResponse) {
 | 
			
		||||
				assert.True(t, response.Verification.Verified)
 | 
			
		||||
				assert.Equal(t, "gitea@fake.local", response.Verification.Signer.Email)
 | 
			
		||||
			}))
 | 
			
		||||
 | 
			
		||||
	})
 | 
			
		||||
	t.Run("UnsignedMerging", func(t *testing.T) {
 | 
			
		||||
		PrintCurrentTest(t)
 | 
			
		||||
		testCtx := NewAPITestContext(t, username, "initial-unsigned")
 | 
			
		||||
		var pr api.PullRequest
 | 
			
		||||
		var err error
 | 
			
		||||
		t.Run("CreatePullRequest", func(t *testing.T) {
 | 
			
		||||
			pr, err = doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "never2")(t)
 | 
			
		||||
			assert.NoError(t, err)
 | 
			
		||||
		})
 | 
			
		||||
		setting.Repository.Signing.Merges = []string{"commitssigned"}
 | 
			
		||||
		t.Run("MergePR", doAPIMergePullRequest(testCtx, testCtx.Username, testCtx.Reponame, pr.Index))
 | 
			
		||||
		t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) {
 | 
			
		||||
			assert.NotNil(t, branch.Commit)
 | 
			
		||||
			assert.NotNil(t, branch.Commit.Verification)
 | 
			
		||||
			assert.False(t, branch.Commit.Verification.Verified)
 | 
			
		||||
			assert.Empty(t, branch.Commit.Verification.Signature)
 | 
			
		||||
		}))
 | 
			
		||||
		setting.Repository.Signing.Merges = []string{"basesigned"}
 | 
			
		||||
		t.Run("CreatePullRequest", func(t *testing.T) {
 | 
			
		||||
			pr, err = doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "parentsigned2")(t)
 | 
			
		||||
			assert.NoError(t, err)
 | 
			
		||||
		})
 | 
			
		||||
		t.Run("MergePR", doAPIMergePullRequest(testCtx, testCtx.Username, testCtx.Reponame, pr.Index))
 | 
			
		||||
		t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) {
 | 
			
		||||
			assert.NotNil(t, branch.Commit)
 | 
			
		||||
			assert.NotNil(t, branch.Commit.Verification)
 | 
			
		||||
			assert.False(t, branch.Commit.Verification.Verified)
 | 
			
		||||
			assert.Empty(t, branch.Commit.Verification.Signature)
 | 
			
		||||
		}))
 | 
			
		||||
		setting.Repository.Signing.Merges = []string{"commitssigned"}
 | 
			
		||||
		t.Run("CreatePullRequest", func(t *testing.T) {
 | 
			
		||||
			pr, err = doAPICreatePullRequest(testCtx, testCtx.Username, testCtx.Reponame, "master", "always-parentsigned")(t)
 | 
			
		||||
			assert.NoError(t, err)
 | 
			
		||||
		})
 | 
			
		||||
		t.Run("MergePR", doAPIMergePullRequest(testCtx, testCtx.Username, testCtx.Reponame, pr.Index))
 | 
			
		||||
		t.Run("CheckMasterBranchUnsigned", doAPIGetBranch(testCtx, "master", func(t *testing.T, branch api.Branch) {
 | 
			
		||||
			assert.NotNil(t, branch.Commit)
 | 
			
		||||
			assert.NotNil(t, branch.Commit.Verification)
 | 
			
		||||
			assert.True(t, branch.Commit.Verification.Verified)
 | 
			
		||||
		}))
 | 
			
		||||
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func crudActionCreateFile(t *testing.T, ctx APITestContext, user *models.User, from, to, path string, callback ...func(*testing.T, api.FileResponse)) func(*testing.T) {
 | 
			
		||||
	return doAPICreateFile(ctx, path, &api.CreateFileOptions{
 | 
			
		||||
		FileOptions: api.FileOptions{
 | 
			
		||||
			BranchName:    from,
 | 
			
		||||
			NewBranchName: to,
 | 
			
		||||
			Message:       fmt.Sprintf("from:%s to:%s path:%s", from, to, path),
 | 
			
		||||
			Author: api.Identity{
 | 
			
		||||
				Name:  user.FullName,
 | 
			
		||||
				Email: user.Email,
 | 
			
		||||
			},
 | 
			
		||||
			Committer: api.Identity{
 | 
			
		||||
				Name:  user.FullName,
 | 
			
		||||
				Email: user.Email,
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		Content: base64.StdEncoding.EncodeToString([]byte("This is new text")),
 | 
			
		||||
	}, callback...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func createGPGKey(tmpDir, name, email string) (*openpgp.Entity, error) {
 | 
			
		||||
	keyPair, err := openpgp.NewEntity(name, "test", email, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, id := range keyPair.Identities {
 | 
			
		||||
		err := id.SelfSignature.SignUserId(id.UserId.Id, keyPair.PrimaryKey, keyPair.PrivateKey, nil)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	keyFile := filepath.Join(tmpDir, "temporary.key")
 | 
			
		||||
	keyWriter, err := os.Create(keyFile)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	defer keyWriter.Close()
 | 
			
		||||
	defer os.Remove(keyFile)
 | 
			
		||||
 | 
			
		||||
	w, err := armor.Encode(keyWriter, openpgp.PrivateKeyType, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	defer w.Close()
 | 
			
		||||
 | 
			
		||||
	keyPair.SerializePrivate(w, nil)
 | 
			
		||||
	if err := w.Close(); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	if err := keyWriter.Close(); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if _, _, err := process.GetManager().Exec("gpg --import temporary.key", "gpg", "--import", keyFile); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return keyPair, nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -21,6 +21,9 @@ ROOT = integrations/gitea-integration-mssql/gitea-repositories
 | 
			
		|||
LOCAL_COPY_PATH = tmp/local-repo-mssql
 | 
			
		||||
LOCAL_WIKI_PATH = tmp/local-wiki-mssql
 | 
			
		||||
 | 
			
		||||
[repository.signing]
 | 
			
		||||
SIGNING_KEY = none
 | 
			
		||||
 | 
			
		||||
[server]
 | 
			
		||||
SSH_DOMAIN       = localhost
 | 
			
		||||
HTTP_PORT        = 3003
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,6 +21,9 @@ ROOT = integrations/gitea-integration-mysql/gitea-repositories
 | 
			
		|||
LOCAL_COPY_PATH = tmp/local-repo-mysql
 | 
			
		||||
LOCAL_WIKI_PATH = tmp/local-wiki-mysql
 | 
			
		||||
 | 
			
		||||
[repository.signing]
 | 
			
		||||
SIGNING_KEY = none
 | 
			
		||||
 | 
			
		||||
[server]
 | 
			
		||||
SSH_DOMAIN       = localhost
 | 
			
		||||
HTTP_PORT        = 3001
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,6 +21,9 @@ ROOT = integrations/gitea-integration-mysql8/gitea-repositories
 | 
			
		|||
LOCAL_COPY_PATH = tmp/local-repo-mysql8
 | 
			
		||||
LOCAL_WIKI_PATH = tmp/local-wiki-mysql8
 | 
			
		||||
 | 
			
		||||
[repository.signing]
 | 
			
		||||
SIGNING_KEY = none
 | 
			
		||||
 | 
			
		||||
[server]
 | 
			
		||||
SSH_DOMAIN       = localhost
 | 
			
		||||
HTTP_PORT        = 3004
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,6 +21,9 @@ ROOT = integrations/gitea-integration-pgsql/gitea-repositories
 | 
			
		|||
LOCAL_COPY_PATH = tmp/local-repo-pgsql
 | 
			
		||||
LOCAL_WIKI_PATH = tmp/local-wiki-pgsql
 | 
			
		||||
 | 
			
		||||
[repository.signing]
 | 
			
		||||
SIGNING_KEY = none
 | 
			
		||||
 | 
			
		||||
[server]
 | 
			
		||||
SSH_DOMAIN       = localhost
 | 
			
		||||
HTTP_PORT        = 3002
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -53,7 +53,7 @@ func getExpectedDeleteFileResponse(u *url.URL) *api.FileResponse {
 | 
			
		|||
		},
 | 
			
		||||
		Verification: &api.PayloadCommitVerification{
 | 
			
		||||
			Verified:  false,
 | 
			
		||||
			Reason:    "",
 | 
			
		||||
			Reason:    "gpg.error.not_signed_commit",
 | 
			
		||||
			Signature: "",
 | 
			
		||||
			Payload:   "",
 | 
			
		||||
		},
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -108,7 +108,7 @@ func getExpectedFileResponseForRepofilesCreate(commitID string) *api.FileRespons
 | 
			
		|||
		},
 | 
			
		||||
		Verification: &api.PayloadCommitVerification{
 | 
			
		||||
			Verified:  false,
 | 
			
		||||
			Reason:    "unsigned",
 | 
			
		||||
			Reason:    "gpg.error.not_signed_commit",
 | 
			
		||||
			Signature: "",
 | 
			
		||||
			Payload:   "",
 | 
			
		||||
		},
 | 
			
		||||
| 
						 | 
				
			
			@ -175,7 +175,7 @@ func getExpectedFileResponseForRepofilesUpdate(commitID, filename string) *api.F
 | 
			
		|||
		},
 | 
			
		||||
		Verification: &api.PayloadCommitVerification{
 | 
			
		||||
			Verified:  false,
 | 
			
		||||
			Reason:    "unsigned",
 | 
			
		||||
			Reason:    "gpg.error.not_signed_commit",
 | 
			
		||||
			Signature: "",
 | 
			
		||||
			Payload:   "",
 | 
			
		||||
		},
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,6 +17,9 @@ ROOT = integrations/gitea-integration-sqlite/gitea-repositories
 | 
			
		|||
LOCAL_COPY_PATH = tmp/local-repo-sqlite
 | 
			
		||||
LOCAL_WIKI_PATH = tmp/local-wiki-sqlite
 | 
			
		||||
 | 
			
		||||
[repository.signing]
 | 
			
		||||
SIGNING_KEY = none
 | 
			
		||||
 | 
			
		||||
[server]
 | 
			
		||||
SSH_DOMAIN       = localhost
 | 
			
		||||
HTTP_PORT        = 3003
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,6 +17,7 @@ import (
 | 
			
		|||
 | 
			
		||||
	"code.gitea.io/gitea/modules/git"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/timeutil"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-xorm/xorm"
 | 
			
		||||
| 
						 | 
				
			
			@ -80,6 +81,12 @@ func GetGPGKeyByID(keyID int64) (*GPGKey, error) {
 | 
			
		|||
	return key, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetGPGKeysByKeyID returns public key by given ID.
 | 
			
		||||
func GetGPGKeysByKeyID(keyID string) ([]*GPGKey, error) {
 | 
			
		||||
	keys := make([]*GPGKey, 0, 1)
 | 
			
		||||
	return keys, x.Where("key_id=?", keyID).Find(&keys)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetGPGImportByKeyID returns the import public armored key by given KeyID.
 | 
			
		||||
func GetGPGImportByKeyID(keyID string) (*GPGKeyImport, error) {
 | 
			
		||||
	key := new(GPGKeyImport)
 | 
			
		||||
| 
						 | 
				
			
			@ -355,10 +362,13 @@ func DeleteGPGKey(doer *User, id int64) (err error) {
 | 
			
		|||
 | 
			
		||||
// CommitVerification represents a commit validation of signature
 | 
			
		||||
type CommitVerification struct {
 | 
			
		||||
	Verified    bool
 | 
			
		||||
	Reason      string
 | 
			
		||||
	SigningUser *User
 | 
			
		||||
	SigningKey  *GPGKey
 | 
			
		||||
	Verified       bool
 | 
			
		||||
	Warning        bool
 | 
			
		||||
	Reason         string
 | 
			
		||||
	SigningUser    *User
 | 
			
		||||
	CommittingUser *User
 | 
			
		||||
	SigningEmail   string
 | 
			
		||||
	SigningKey     *GPGKey
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SignCommit represents a commit with validation of signature.
 | 
			
		||||
| 
						 | 
				
			
			@ -367,6 +377,17 @@ type SignCommit struct {
 | 
			
		|||
	*UserCommit
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	// BadSignature is used as the reason when the signature has a KeyID that is in the db
 | 
			
		||||
	// but no key that has that ID verifies the signature. This is a suspicious failure.
 | 
			
		||||
	BadSignature = "gpg.error.probable_bad_signature"
 | 
			
		||||
	// BadDefaultSignature is used as the reason when the signature has a KeyID that matches the
 | 
			
		||||
	// default Key but is not verified by the default key. This is a suspicious failure.
 | 
			
		||||
	BadDefaultSignature = "gpg.error.probable_bad_default_signature"
 | 
			
		||||
	// NoKeyFound is used as the reason when no key can be found to verify the signature.
 | 
			
		||||
	NoKeyFound = "gpg.error.no_gpg_keys_found"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func readerFromBase64(s string) (io.Reader, error) {
 | 
			
		||||
	bs, err := base64.StdEncoding.DecodeString(s)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
| 
						 | 
				
			
			@ -424,49 +445,207 @@ func verifySign(s *packet.Signature, h hash.Hash, k *GPGKey) error {
 | 
			
		|||
	return pkey.VerifySignature(h, s)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ParseCommitWithSignature check if signature is good against keystore.
 | 
			
		||||
func ParseCommitWithSignature(c *git.Commit) *CommitVerification {
 | 
			
		||||
	if c.Signature != nil && c.Committer != nil {
 | 
			
		||||
		//Parsing signature
 | 
			
		||||
		sig, err := extractSignature(c.Signature.Signature)
 | 
			
		||||
		if err != nil { //Skipping failed to extract sign
 | 
			
		||||
			log.Error("SignatureRead err: %v", err)
 | 
			
		||||
			return &CommitVerification{
 | 
			
		||||
				Verified: false,
 | 
			
		||||
				Reason:   "gpg.error.extract_sign",
 | 
			
		||||
func hashAndVerify(sig *packet.Signature, payload string, k *GPGKey, committer, signer *User, email string) *CommitVerification {
 | 
			
		||||
	//Generating hash of commit
 | 
			
		||||
	hash, err := populateHash(sig.Hash, []byte(payload))
 | 
			
		||||
	if err != nil { //Skipping failed to generate hash
 | 
			
		||||
		log.Error("PopulateHash: %v", err)
 | 
			
		||||
		return &CommitVerification{
 | 
			
		||||
			CommittingUser: committer,
 | 
			
		||||
			Verified:       false,
 | 
			
		||||
			Reason:         "gpg.error.generate_hash",
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := verifySign(sig, hash, k); err == nil {
 | 
			
		||||
		return &CommitVerification{ //Everything is ok
 | 
			
		||||
			CommittingUser: committer,
 | 
			
		||||
			Verified:       true,
 | 
			
		||||
			Reason:         fmt.Sprintf("%s <%s> / %s", signer.Name, signer.Email, k.KeyID),
 | 
			
		||||
			SigningUser:    signer,
 | 
			
		||||
			SigningKey:     k,
 | 
			
		||||
			SigningEmail:   email,
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func hashAndVerifyWithSubKeys(sig *packet.Signature, payload string, k *GPGKey, committer, signer *User, email string) *CommitVerification {
 | 
			
		||||
	commitVerification := hashAndVerify(sig, payload, k, committer, signer, email)
 | 
			
		||||
	if commitVerification != nil {
 | 
			
		||||
		return commitVerification
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	//And test also SubsKey
 | 
			
		||||
	for _, sk := range k.SubsKey {
 | 
			
		||||
		commitVerification := hashAndVerify(sig, payload, sk, committer, signer, email)
 | 
			
		||||
		if commitVerification != nil {
 | 
			
		||||
			return commitVerification
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func hashAndVerifyForKeyID(sig *packet.Signature, payload string, committer *User, keyID, name, email string) *CommitVerification {
 | 
			
		||||
	if keyID == "" {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	keys, err := GetGPGKeysByKeyID(keyID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error("GetGPGKeysByKeyID: %v", err)
 | 
			
		||||
		return &CommitVerification{
 | 
			
		||||
			CommittingUser: committer,
 | 
			
		||||
			Verified:       false,
 | 
			
		||||
			Reason:         "gpg.error.failed_retrieval_gpg_keys",
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if len(keys) == 0 {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
	for _, key := range keys {
 | 
			
		||||
		activated := false
 | 
			
		||||
		if len(email) != 0 {
 | 
			
		||||
			for _, e := range key.Emails {
 | 
			
		||||
				if e.IsActivated && strings.EqualFold(e.Email, email) {
 | 
			
		||||
					activated = true
 | 
			
		||||
					email = e.Email
 | 
			
		||||
					break
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			for _, e := range key.Emails {
 | 
			
		||||
				if e.IsActivated {
 | 
			
		||||
					activated = true
 | 
			
		||||
					email = e.Email
 | 
			
		||||
					break
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if !activated {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		signer := &User{
 | 
			
		||||
			Name:  name,
 | 
			
		||||
			Email: email,
 | 
			
		||||
		}
 | 
			
		||||
		if key.OwnerID != 0 {
 | 
			
		||||
			owner, err := GetUserByID(key.OwnerID)
 | 
			
		||||
			if err == nil {
 | 
			
		||||
				signer = owner
 | 
			
		||||
			} else if !IsErrUserNotExist(err) {
 | 
			
		||||
				log.Error("Failed to GetUserByID: %d for key ID: %d (%s) %v", key.OwnerID, key.ID, key.KeyID, err)
 | 
			
		||||
				return &CommitVerification{
 | 
			
		||||
					CommittingUser: committer,
 | 
			
		||||
					Verified:       false,
 | 
			
		||||
					Reason:         "gpg.error.no_committer_account",
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		commitVerification := hashAndVerifyWithSubKeys(sig, payload, key, committer, signer, email)
 | 
			
		||||
		if commitVerification != nil {
 | 
			
		||||
			return commitVerification
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	// This is a bad situation ... We have a key id that is in our database but the signature doesn't match.
 | 
			
		||||
	return &CommitVerification{
 | 
			
		||||
		CommittingUser: committer,
 | 
			
		||||
		Verified:       false,
 | 
			
		||||
		Warning:        true,
 | 
			
		||||
		Reason:         BadSignature,
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ParseCommitWithSignature check if signature is good against keystore.
 | 
			
		||||
func ParseCommitWithSignature(c *git.Commit) *CommitVerification {
 | 
			
		||||
	var committer *User
 | 
			
		||||
	if c.Committer != nil {
 | 
			
		||||
		var err error
 | 
			
		||||
		//Find Committer account
 | 
			
		||||
		committer, err := GetUserByEmail(c.Committer.Email) //This find the user by primary email or activated email so commit will not be valid if email is not
 | 
			
		||||
		if err != nil {                                     //Skipping not user for commiter
 | 
			
		||||
		committer, err = GetUserByEmail(c.Committer.Email) //This finds the user by primary email or activated email so commit will not be valid if email is not
 | 
			
		||||
		if err != nil {                                    //Skipping not user for commiter
 | 
			
		||||
			committer = &User{
 | 
			
		||||
				Name:  c.Committer.Name,
 | 
			
		||||
				Email: c.Committer.Email,
 | 
			
		||||
			}
 | 
			
		||||
			// We can expect this to often be an ErrUserNotExist. in the case
 | 
			
		||||
			// it is not, however, it is important to log it.
 | 
			
		||||
			if !IsErrUserNotExist(err) {
 | 
			
		||||
				log.Error("GetUserByEmail: %v", err)
 | 
			
		||||
				return &CommitVerification{
 | 
			
		||||
					CommittingUser: committer,
 | 
			
		||||
					Verified:       false,
 | 
			
		||||
					Reason:         "gpg.error.no_committer_account",
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			return &CommitVerification{
 | 
			
		||||
				Verified: false,
 | 
			
		||||
				Reason:   "gpg.error.no_committer_account",
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// If no signature just report the committer
 | 
			
		||||
	if c.Signature == nil {
 | 
			
		||||
		return &CommitVerification{
 | 
			
		||||
			CommittingUser: committer,
 | 
			
		||||
			Verified:       false,                         //Default value
 | 
			
		||||
			Reason:         "gpg.error.not_signed_commit", //Default value
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	//Parsing signature
 | 
			
		||||
	sig, err := extractSignature(c.Signature.Signature)
 | 
			
		||||
	if err != nil { //Skipping failed to extract sign
 | 
			
		||||
		log.Error("SignatureRead err: %v", err)
 | 
			
		||||
		return &CommitVerification{
 | 
			
		||||
			CommittingUser: committer,
 | 
			
		||||
			Verified:       false,
 | 
			
		||||
			Reason:         "gpg.error.extract_sign",
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	keyID := ""
 | 
			
		||||
	if sig.IssuerKeyId != nil && (*sig.IssuerKeyId) != 0 {
 | 
			
		||||
		keyID = fmt.Sprintf("%X", *sig.IssuerKeyId)
 | 
			
		||||
	}
 | 
			
		||||
	if keyID == "" && sig.IssuerFingerprint != nil && len(sig.IssuerFingerprint) > 0 {
 | 
			
		||||
		keyID = fmt.Sprintf("%X", sig.IssuerFingerprint[12:20])
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	defaultReason := NoKeyFound
 | 
			
		||||
 | 
			
		||||
	// First check if the sig has a keyID and if so just look at that
 | 
			
		||||
	if commitVerification := hashAndVerifyForKeyID(
 | 
			
		||||
		sig,
 | 
			
		||||
		c.Signature.Payload,
 | 
			
		||||
		committer,
 | 
			
		||||
		keyID,
 | 
			
		||||
		setting.AppName,
 | 
			
		||||
		""); commitVerification != nil {
 | 
			
		||||
		if commitVerification.Reason == BadSignature {
 | 
			
		||||
			defaultReason = BadSignature
 | 
			
		||||
		} else {
 | 
			
		||||
			return commitVerification
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Now try to associate the signature with the committer, if present
 | 
			
		||||
	if committer.ID != 0 {
 | 
			
		||||
		keys, err := ListGPGKeys(committer.ID)
 | 
			
		||||
		if err != nil { //Skipping failed to get gpg keys of user
 | 
			
		||||
			log.Error("ListGPGKeys: %v", err)
 | 
			
		||||
			return &CommitVerification{
 | 
			
		||||
				Verified: false,
 | 
			
		||||
				Reason:   "gpg.error.failed_retrieval_gpg_keys",
 | 
			
		||||
				CommittingUser: committer,
 | 
			
		||||
				Verified:       false,
 | 
			
		||||
				Reason:         "gpg.error.failed_retrieval_gpg_keys",
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for _, k := range keys {
 | 
			
		||||
			//Pre-check (& optimization) that emails attached to key can be attached to the commiter email and can validate
 | 
			
		||||
			canValidate := false
 | 
			
		||||
			lowerCommiterEmail := strings.ToLower(c.Committer.Email)
 | 
			
		||||
			email := ""
 | 
			
		||||
			for _, e := range k.Emails {
 | 
			
		||||
				if e.IsActivated && strings.ToLower(e.Email) == lowerCommiterEmail {
 | 
			
		||||
				if e.IsActivated && strings.EqualFold(e.Email, c.Committer.Email) {
 | 
			
		||||
					canValidate = true
 | 
			
		||||
					email = e.Email
 | 
			
		||||
					break
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
| 
						 | 
				
			
			@ -474,56 +653,102 @@ func ParseCommitWithSignature(c *git.Commit) *CommitVerification {
 | 
			
		|||
				continue //Skip this key
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			//Generating hash of commit
 | 
			
		||||
			hash, err := populateHash(sig.Hash, []byte(c.Signature.Payload))
 | 
			
		||||
			if err != nil { //Skipping ailed to generate hash
 | 
			
		||||
				log.Error("PopulateHash: %v", err)
 | 
			
		||||
				return &CommitVerification{
 | 
			
		||||
					Verified: false,
 | 
			
		||||
					Reason:   "gpg.error.generate_hash",
 | 
			
		||||
				}
 | 
			
		||||
			commitVerification := hashAndVerifyWithSubKeys(sig, c.Signature.Payload, k, committer, committer, email)
 | 
			
		||||
			if commitVerification != nil {
 | 
			
		||||
				return commitVerification
 | 
			
		||||
			}
 | 
			
		||||
			//We get PK
 | 
			
		||||
			if err := verifySign(sig, hash, k); err == nil {
 | 
			
		||||
				return &CommitVerification{ //Everything is ok
 | 
			
		||||
					Verified:    true,
 | 
			
		||||
					Reason:      fmt.Sprintf("%s <%s> / %s", c.Committer.Name, c.Committer.Email, k.KeyID),
 | 
			
		||||
					SigningUser: committer,
 | 
			
		||||
					SigningKey:  k,
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			//And test also SubsKey
 | 
			
		||||
			for _, sk := range k.SubsKey {
 | 
			
		||||
 | 
			
		||||
				//Generating hash of commit
 | 
			
		||||
				hash, err := populateHash(sig.Hash, []byte(c.Signature.Payload))
 | 
			
		||||
				if err != nil { //Skipping ailed to generate hash
 | 
			
		||||
					log.Error("PopulateHash: %v", err)
 | 
			
		||||
					return &CommitVerification{
 | 
			
		||||
						Verified: false,
 | 
			
		||||
						Reason:   "gpg.error.generate_hash",
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				if err := verifySign(sig, hash, sk); err == nil {
 | 
			
		||||
					return &CommitVerification{ //Everything is ok
 | 
			
		||||
						Verified:    true,
 | 
			
		||||
						Reason:      fmt.Sprintf("%s <%s> / %s", c.Committer.Name, c.Committer.Email, sk.KeyID),
 | 
			
		||||
						SigningUser: committer,
 | 
			
		||||
						SigningKey:  sk,
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		return &CommitVerification{ //Default at this stage
 | 
			
		||||
			Verified: false,
 | 
			
		||||
			Reason:   "gpg.error.no_gpg_keys_found",
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &CommitVerification{
 | 
			
		||||
		Verified: false,                         //Default value
 | 
			
		||||
		Reason:   "gpg.error.not_signed_commit", //Default value
 | 
			
		||||
	if setting.Repository.Signing.SigningKey != "" && setting.Repository.Signing.SigningKey != "default" && setting.Repository.Signing.SigningKey != "none" {
 | 
			
		||||
		// OK we should try the default key
 | 
			
		||||
		gpgSettings := git.GPGSettings{
 | 
			
		||||
			Sign:  true,
 | 
			
		||||
			KeyID: setting.Repository.Signing.SigningKey,
 | 
			
		||||
			Name:  setting.Repository.Signing.SigningName,
 | 
			
		||||
			Email: setting.Repository.Signing.SigningEmail,
 | 
			
		||||
		}
 | 
			
		||||
		if err := gpgSettings.LoadPublicKeyContent(); err != nil {
 | 
			
		||||
			log.Error("Error getting default signing key: %s %v", gpgSettings.KeyID, err)
 | 
			
		||||
		} else if commitVerification := verifyWithGPGSettings(&gpgSettings, sig, c.Signature.Payload, committer, keyID); commitVerification != nil {
 | 
			
		||||
			if commitVerification.Reason == BadSignature {
 | 
			
		||||
				defaultReason = BadSignature
 | 
			
		||||
			} else {
 | 
			
		||||
				return commitVerification
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	defaultGPGSettings, err := c.GetRepositoryDefaultPublicGPGKey(false)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error("Error getting default public gpg key: %v", err)
 | 
			
		||||
	} else if defaultGPGSettings.Sign {
 | 
			
		||||
		if commitVerification := verifyWithGPGSettings(defaultGPGSettings, sig, c.Signature.Payload, committer, keyID); commitVerification != nil {
 | 
			
		||||
			if commitVerification.Reason == BadSignature {
 | 
			
		||||
				defaultReason = BadSignature
 | 
			
		||||
			} else {
 | 
			
		||||
				return commitVerification
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return &CommitVerification{ //Default at this stage
 | 
			
		||||
		CommittingUser: committer,
 | 
			
		||||
		Verified:       false,
 | 
			
		||||
		Warning:        defaultReason != NoKeyFound,
 | 
			
		||||
		Reason:         defaultReason,
 | 
			
		||||
		SigningKey: &GPGKey{
 | 
			
		||||
			KeyID: keyID,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func verifyWithGPGSettings(gpgSettings *git.GPGSettings, sig *packet.Signature, payload string, committer *User, keyID string) *CommitVerification {
 | 
			
		||||
	// First try to find the key in the db
 | 
			
		||||
	if commitVerification := hashAndVerifyForKeyID(sig, payload, committer, gpgSettings.KeyID, gpgSettings.Name, gpgSettings.Email); commitVerification != nil {
 | 
			
		||||
		return commitVerification
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Otherwise we have to parse the key
 | 
			
		||||
	ekey, err := checkArmoredGPGKeyString(gpgSettings.PublicKeyContent)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error("Unable to get default signing key: %v", err)
 | 
			
		||||
		return &CommitVerification{
 | 
			
		||||
			CommittingUser: committer,
 | 
			
		||||
			Verified:       false,
 | 
			
		||||
			Reason:         "gpg.error.generate_hash",
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	pubkey := ekey.PrimaryKey
 | 
			
		||||
	content, err := base64EncPubKey(pubkey)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return &CommitVerification{
 | 
			
		||||
			CommittingUser: committer,
 | 
			
		||||
			Verified:       false,
 | 
			
		||||
			Reason:         "gpg.error.generate_hash",
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	k := &GPGKey{
 | 
			
		||||
		Content: content,
 | 
			
		||||
		CanSign: pubkey.CanSign(),
 | 
			
		||||
		KeyID:   pubkey.KeyIdString(),
 | 
			
		||||
	}
 | 
			
		||||
	if commitVerification := hashAndVerifyWithSubKeys(sig, payload, k, committer, &User{
 | 
			
		||||
		Name:  gpgSettings.Name,
 | 
			
		||||
		Email: gpgSettings.Email,
 | 
			
		||||
	}, gpgSettings.Email); commitVerification != nil {
 | 
			
		||||
		return commitVerification
 | 
			
		||||
	}
 | 
			
		||||
	if keyID == k.KeyID {
 | 
			
		||||
		// This is a bad situation ... We have a key id that matches our default key but the signature doesn't match.
 | 
			
		||||
		return &CommitVerification{
 | 
			
		||||
			CommittingUser: committer,
 | 
			
		||||
			Verified:       false,
 | 
			
		||||
			Warning:        true,
 | 
			
		||||
			Reason:         BadSignature,
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ParseCommitsWithSignature checks if signaute of commits are corresponding to users gpg keys.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -38,6 +38,7 @@ import (
 | 
			
		|||
	"code.gitea.io/gitea/modules/timeutil"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-xorm/xorm"
 | 
			
		||||
	"github.com/mcuadros/go-version"
 | 
			
		||||
	"github.com/unknwon/com"
 | 
			
		||||
	ini "gopkg.in/ini.v1"
 | 
			
		||||
	"xorm.io/builder"
 | 
			
		||||
| 
						 | 
				
			
			@ -1126,7 +1127,20 @@ func CleanUpMigrateInfo(repo *Repository) (*Repository, error) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
// initRepoCommit temporarily changes with work directory.
 | 
			
		||||
func initRepoCommit(tmpPath string, sig *git.Signature) (err error) {
 | 
			
		||||
func initRepoCommit(tmpPath string, u *User) (err error) {
 | 
			
		||||
	commitTimeStr := time.Now().Format(time.RFC3339)
 | 
			
		||||
 | 
			
		||||
	sig := u.NewGitSig()
 | 
			
		||||
	// Because this may call hooks we should pass in the environment
 | 
			
		||||
	env := append(os.Environ(),
 | 
			
		||||
		"GIT_AUTHOR_NAME="+sig.Name,
 | 
			
		||||
		"GIT_AUTHOR_EMAIL="+sig.Email,
 | 
			
		||||
		"GIT_AUTHOR_DATE="+commitTimeStr,
 | 
			
		||||
		"GIT_COMMITTER_NAME="+sig.Name,
 | 
			
		||||
		"GIT_COMMITTER_EMAIL="+sig.Email,
 | 
			
		||||
		"GIT_COMMITTER_DATE="+commitTimeStr,
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	var stderr string
 | 
			
		||||
	if _, stderr, err = process.GetManager().ExecDir(-1,
 | 
			
		||||
		tmpPath, fmt.Sprintf("initRepoCommit (git add): %s", tmpPath),
 | 
			
		||||
| 
						 | 
				
			
			@ -1134,10 +1148,29 @@ func initRepoCommit(tmpPath string, sig *git.Signature) (err error) {
 | 
			
		|||
		return fmt.Errorf("git add: %s", stderr)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if _, stderr, err = process.GetManager().ExecDir(-1,
 | 
			
		||||
	binVersion, err := git.BinVersion()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("Unable to get git version: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	args := []string{
 | 
			
		||||
		"commit", fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email),
 | 
			
		||||
		"-m", "Initial commit",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if version.Compare(binVersion, "1.7.9", ">=") {
 | 
			
		||||
		sign, keyID := SignInitialCommit(tmpPath, u)
 | 
			
		||||
		if sign {
 | 
			
		||||
			args = append(args, "-S"+keyID)
 | 
			
		||||
		} else if version.Compare(binVersion, "2.0.0", ">=") {
 | 
			
		||||
			args = append(args, "--no-gpg-sign")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if _, stderr, err = process.GetManager().ExecDirEnv(-1,
 | 
			
		||||
		tmpPath, fmt.Sprintf("initRepoCommit (git commit): %s", tmpPath),
 | 
			
		||||
		git.GitExecutable, "commit", fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email),
 | 
			
		||||
		"-m", "Initial commit"); err != nil {
 | 
			
		||||
		env,
 | 
			
		||||
		git.GitExecutable, args...); err != nil {
 | 
			
		||||
		return fmt.Errorf("git commit: %s", stderr)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -1189,9 +1222,24 @@ func getRepoInitFile(tp, name string) ([]byte, error) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
func prepareRepoCommit(e Engine, repo *Repository, tmpDir, repoPath string, opts CreateRepoOptions) error {
 | 
			
		||||
	commitTimeStr := time.Now().Format(time.RFC3339)
 | 
			
		||||
	authorSig := repo.Owner.NewGitSig()
 | 
			
		||||
 | 
			
		||||
	// Because this may call hooks we should pass in the environment
 | 
			
		||||
	env := append(os.Environ(),
 | 
			
		||||
		"GIT_AUTHOR_NAME="+authorSig.Name,
 | 
			
		||||
		"GIT_AUTHOR_EMAIL="+authorSig.Email,
 | 
			
		||||
		"GIT_AUTHOR_DATE="+commitTimeStr,
 | 
			
		||||
		"GIT_COMMITTER_NAME="+authorSig.Name,
 | 
			
		||||
		"GIT_COMMITTER_EMAIL="+authorSig.Email,
 | 
			
		||||
		"GIT_COMMITTER_DATE="+commitTimeStr,
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	// Clone to temporary path and do the init commit.
 | 
			
		||||
	_, stderr, err := process.GetManager().Exec(
 | 
			
		||||
	_, stderr, err := process.GetManager().ExecDirEnv(
 | 
			
		||||
		-1, "",
 | 
			
		||||
		fmt.Sprintf("initRepository(git clone): %s", repoPath),
 | 
			
		||||
		env,
 | 
			
		||||
		git.GitExecutable, "clone", repoPath, tmpDir,
 | 
			
		||||
	)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
| 
						 | 
				
			
			@ -1282,7 +1330,7 @@ func initRepository(e Engine, repoPath string, u *User, repo *Repository, opts C
 | 
			
		|||
		}
 | 
			
		||||
 | 
			
		||||
		// Apply changes and commit.
 | 
			
		||||
		if err = initRepoCommit(tmpDir, u.NewGitSig()); err != nil {
 | 
			
		||||
		if err = initRepoCommit(tmpDir, u); err != nil {
 | 
			
		||||
			return fmt.Errorf("initRepoCommit: %v", err)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										303
									
								
								models/repo_sign.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										303
									
								
								models/repo_sign.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,303 @@
 | 
			
		|||
// Copyright 2019 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 models
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"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"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func signingModeFromStrings(modeStrings []string) []signingMode {
 | 
			
		||||
	returnable := make([]signingMode, 0, len(modeStrings))
 | 
			
		||||
	for _, mode := range modeStrings {
 | 
			
		||||
		signMode := signingMode(strings.ToLower(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 commitsSigned:
 | 
			
		||||
			returnable = append(returnable, signMode)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if len(returnable) == 0 {
 | 
			
		||||
		return []signingMode{never}
 | 
			
		||||
	}
 | 
			
		||||
	return returnable
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func signingKey(repoPath string) string {
 | 
			
		||||
	if setting.Repository.Signing.SigningKey == "none" {
 | 
			
		||||
		return ""
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	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.NewCommand("config", "--get", "commit.gpgsign").RunInDir(repoPath)
 | 
			
		||||
		sign, valid := git.ParseBool(strings.TrimSpace(value))
 | 
			
		||||
		if !sign || !valid {
 | 
			
		||||
			return ""
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		signingKey, _ := git.NewCommand("config", "--get", "user.signingkey").RunInDir(repoPath)
 | 
			
		||||
		return strings.TrimSpace(signingKey)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return setting.Repository.Signing.SigningKey
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PublicSigningKey gets the public signing key within a provided repository directory
 | 
			
		||||
func PublicSigningKey(repoPath string) (string, error) {
 | 
			
		||||
	signingKey := signingKey(repoPath)
 | 
			
		||||
	if signingKey == "" {
 | 
			
		||||
		return "", nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	content, stderr, err := process.GetManager().ExecDir(-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(repoPath string, u *User) (bool, string) {
 | 
			
		||||
	rules := signingModeFromStrings(setting.Repository.Signing.InitialCommit)
 | 
			
		||||
	signingKey := signingKey(repoPath)
 | 
			
		||||
	if signingKey == "" {
 | 
			
		||||
		return false, ""
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, rule := range rules {
 | 
			
		||||
		switch rule {
 | 
			
		||||
		case never:
 | 
			
		||||
			return false, ""
 | 
			
		||||
		case always:
 | 
			
		||||
			break
 | 
			
		||||
		case pubkey:
 | 
			
		||||
			keys, err := ListGPGKeys(u.ID)
 | 
			
		||||
			if err != nil || len(keys) == 0 {
 | 
			
		||||
				return false, ""
 | 
			
		||||
			}
 | 
			
		||||
		case twofa:
 | 
			
		||||
			twofa, err := GetTwoFactorByUID(u.ID)
 | 
			
		||||
			if err != nil || twofa == nil {
 | 
			
		||||
				return false, ""
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return true, signingKey
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SignWikiCommit determines if we should sign the commits to this repository wiki
 | 
			
		||||
func (repo *Repository) SignWikiCommit(u *User) (bool, string) {
 | 
			
		||||
	rules := signingModeFromStrings(setting.Repository.Signing.Wiki)
 | 
			
		||||
	signingKey := signingKey(repo.WikiPath())
 | 
			
		||||
	if signingKey == "" {
 | 
			
		||||
		return false, ""
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, rule := range rules {
 | 
			
		||||
		switch rule {
 | 
			
		||||
		case never:
 | 
			
		||||
			return false, ""
 | 
			
		||||
		case always:
 | 
			
		||||
			break
 | 
			
		||||
		case pubkey:
 | 
			
		||||
			keys, err := ListGPGKeys(u.ID)
 | 
			
		||||
			if err != nil || len(keys) == 0 {
 | 
			
		||||
				return false, ""
 | 
			
		||||
			}
 | 
			
		||||
		case twofa:
 | 
			
		||||
			twofa, err := GetTwoFactorByUID(u.ID)
 | 
			
		||||
			if err != nil || twofa == nil {
 | 
			
		||||
				return false, ""
 | 
			
		||||
			}
 | 
			
		||||
		case parentSigned:
 | 
			
		||||
			gitRepo, err := git.OpenRepository(repo.WikiPath())
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return false, ""
 | 
			
		||||
			}
 | 
			
		||||
			commit, err := gitRepo.GetCommit("HEAD")
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return false, ""
 | 
			
		||||
			}
 | 
			
		||||
			if commit.Signature == nil {
 | 
			
		||||
				return false, ""
 | 
			
		||||
			}
 | 
			
		||||
			verification := ParseCommitWithSignature(commit)
 | 
			
		||||
			if !verification.Verified {
 | 
			
		||||
				return false, ""
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return true, signingKey
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SignCRUDAction determines if we should sign a CRUD commit to this repository
 | 
			
		||||
func (repo *Repository) SignCRUDAction(u *User, tmpBasePath, parentCommit string) (bool, string) {
 | 
			
		||||
	rules := signingModeFromStrings(setting.Repository.Signing.CRUDActions)
 | 
			
		||||
	signingKey := signingKey(repo.RepoPath())
 | 
			
		||||
	if signingKey == "" {
 | 
			
		||||
		return false, ""
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, rule := range rules {
 | 
			
		||||
		switch rule {
 | 
			
		||||
		case never:
 | 
			
		||||
			return false, ""
 | 
			
		||||
		case always:
 | 
			
		||||
			break
 | 
			
		||||
		case pubkey:
 | 
			
		||||
			keys, err := ListGPGKeys(u.ID)
 | 
			
		||||
			if err != nil || len(keys) == 0 {
 | 
			
		||||
				return false, ""
 | 
			
		||||
			}
 | 
			
		||||
		case twofa:
 | 
			
		||||
			twofa, err := GetTwoFactorByUID(u.ID)
 | 
			
		||||
			if err != nil || twofa == nil {
 | 
			
		||||
				return false, ""
 | 
			
		||||
			}
 | 
			
		||||
		case parentSigned:
 | 
			
		||||
			gitRepo, err := git.OpenRepository(tmpBasePath)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return false, ""
 | 
			
		||||
			}
 | 
			
		||||
			commit, err := gitRepo.GetCommit(parentCommit)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return false, ""
 | 
			
		||||
			}
 | 
			
		||||
			if commit.Signature == nil {
 | 
			
		||||
				return false, ""
 | 
			
		||||
			}
 | 
			
		||||
			verification := ParseCommitWithSignature(commit)
 | 
			
		||||
			if !verification.Verified {
 | 
			
		||||
				return false, ""
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return true, signingKey
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SignMerge determines if we should sign a merge commit to this repository
 | 
			
		||||
func (repo *Repository) SignMerge(u *User, tmpBasePath, baseCommit, headCommit string) (bool, string) {
 | 
			
		||||
	rules := signingModeFromStrings(setting.Repository.Signing.Merges)
 | 
			
		||||
	signingKey := signingKey(repo.RepoPath())
 | 
			
		||||
	if signingKey == "" {
 | 
			
		||||
		return false, ""
 | 
			
		||||
	}
 | 
			
		||||
	var gitRepo *git.Repository
 | 
			
		||||
	var err error
 | 
			
		||||
 | 
			
		||||
	for _, rule := range rules {
 | 
			
		||||
		switch rule {
 | 
			
		||||
		case never:
 | 
			
		||||
			return false, ""
 | 
			
		||||
		case always:
 | 
			
		||||
			break
 | 
			
		||||
		case pubkey:
 | 
			
		||||
			keys, err := ListGPGKeys(u.ID)
 | 
			
		||||
			if err != nil || len(keys) == 0 {
 | 
			
		||||
				return false, ""
 | 
			
		||||
			}
 | 
			
		||||
		case twofa:
 | 
			
		||||
			twofa, err := GetTwoFactorByUID(u.ID)
 | 
			
		||||
			if err != nil || twofa == nil {
 | 
			
		||||
				return false, ""
 | 
			
		||||
			}
 | 
			
		||||
		case baseSigned:
 | 
			
		||||
			if gitRepo == nil {
 | 
			
		||||
				gitRepo, err = git.OpenRepository(tmpBasePath)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return false, ""
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			commit, err := gitRepo.GetCommit(baseCommit)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return false, ""
 | 
			
		||||
			}
 | 
			
		||||
			verification := ParseCommitWithSignature(commit)
 | 
			
		||||
			if !verification.Verified {
 | 
			
		||||
				return false, ""
 | 
			
		||||
			}
 | 
			
		||||
		case headSigned:
 | 
			
		||||
			if gitRepo == nil {
 | 
			
		||||
				gitRepo, err = git.OpenRepository(tmpBasePath)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return false, ""
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			commit, err := gitRepo.GetCommit(headCommit)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return false, ""
 | 
			
		||||
			}
 | 
			
		||||
			verification := ParseCommitWithSignature(commit)
 | 
			
		||||
			if !verification.Verified {
 | 
			
		||||
				return false, ""
 | 
			
		||||
			}
 | 
			
		||||
		case commitsSigned:
 | 
			
		||||
			if gitRepo == nil {
 | 
			
		||||
				gitRepo, err = git.OpenRepository(tmpBasePath)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return false, ""
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			commit, err := gitRepo.GetCommit(headCommit)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return false, ""
 | 
			
		||||
			}
 | 
			
		||||
			verification := ParseCommitWithSignature(commit)
 | 
			
		||||
			if !verification.Verified {
 | 
			
		||||
				return false, ""
 | 
			
		||||
			}
 | 
			
		||||
			// need to work out merge-base
 | 
			
		||||
			mergeBaseCommit, _, err := gitRepo.GetMergeBase("", baseCommit, headCommit)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return false, ""
 | 
			
		||||
			}
 | 
			
		||||
			commitList, err := commit.CommitsBeforeUntil(mergeBaseCommit)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return false, ""
 | 
			
		||||
			}
 | 
			
		||||
			for e := commitList.Front(); e != nil; e = e.Next() {
 | 
			
		||||
				commit = e.Value.(*git.Commit)
 | 
			
		||||
				verification := ParseCommitWithSignature(commit)
 | 
			
		||||
				if !verification.Verified {
 | 
			
		||||
					return false, ""
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return true, signingKey
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -205,6 +205,13 @@ func (repo *Repository) updateWikiPage(doer *User, oldWikiName, newWikiName, con
 | 
			
		|||
	commitTreeOpts := git.CommitTreeOpts{
 | 
			
		||||
		Message: message,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	sign, signingKey := repo.SignWikiCommit(doer)
 | 
			
		||||
	if sign {
 | 
			
		||||
		commitTreeOpts.KeyID = signingKey
 | 
			
		||||
	} else {
 | 
			
		||||
		commitTreeOpts.NoGPGSign = true
 | 
			
		||||
	}
 | 
			
		||||
	if hasMasterBranch {
 | 
			
		||||
		commitTreeOpts.Parents = []string{"HEAD"}
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			@ -307,11 +314,19 @@ func (repo *Repository) DeleteWikiPage(doer *User, wikiName string) (err error)
 | 
			
		|||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	message := "Delete page '" + wikiName + "'"
 | 
			
		||||
 | 
			
		||||
	commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), tree, git.CommitTreeOpts{
 | 
			
		||||
	commitTreeOpts := git.CommitTreeOpts{
 | 
			
		||||
		Message: message,
 | 
			
		||||
		Parents: []string{"HEAD"},
 | 
			
		||||
	})
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	sign, signingKey := repo.SignWikiCommit(doer)
 | 
			
		||||
	if sign {
 | 
			
		||||
		commitTreeOpts.KeyID = signingKey
 | 
			
		||||
	} else {
 | 
			
		||||
		commitTreeOpts.NoGPGSign = true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), tree, commitTreeOpts)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -498,3 +498,11 @@ func GetFullCommitID(repoPath, shortID string) (string, error) {
 | 
			
		|||
	}
 | 
			
		||||
	return strings.TrimSpace(commitID), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetRepositoryDefaultPublicGPGKey returns the default public key for this commit
 | 
			
		||||
func (c *Commit) GetRepositoryDefaultPublicGPGKey(forceUpdate bool) (*GPGSettings, error) {
 | 
			
		||||
	if c.repo == nil {
 | 
			
		||||
		return nil, nil
 | 
			
		||||
	}
 | 
			
		||||
	return c.repo.GetDefaultPublicGPGKey(forceUpdate)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -32,6 +32,16 @@ type Repository struct {
 | 
			
		|||
 | 
			
		||||
	gogitRepo    *gogit.Repository
 | 
			
		||||
	gogitStorage *filesystem.Storage
 | 
			
		||||
	gpgSettings  *GPGSettings
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GPGSettings represents the default GPG settings for this repository
 | 
			
		||||
type GPGSettings struct {
 | 
			
		||||
	Sign             bool
 | 
			
		||||
	KeyID            string
 | 
			
		||||
	Email            string
 | 
			
		||||
	Name             string
 | 
			
		||||
	PublicKeyContent string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const prettyLogFormat = `--pretty=format:%H`
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										59
									
								
								modules/git/repo_gpg.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								modules/git/repo_gpg.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,59 @@
 | 
			
		|||
// Copyright 2015 The Gogs Authors. All rights reserved.
 | 
			
		||||
// Copyright 2017 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 git
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/modules/process"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// LoadPublicKeyContent will load the key from gpg
 | 
			
		||||
func (gpgSettings *GPGSettings) LoadPublicKeyContent() error {
 | 
			
		||||
	content, stderr, err := process.GetManager().Exec(
 | 
			
		||||
		"gpg -a --export",
 | 
			
		||||
		"gpg", "-a", "--export", gpgSettings.KeyID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("Unable to get default signing key: %s, %s, %v", gpgSettings.KeyID, stderr, err)
 | 
			
		||||
	}
 | 
			
		||||
	gpgSettings.PublicKeyContent = content
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetDefaultPublicGPGKey will return and cache the default public GPG settings for this repository
 | 
			
		||||
func (repo *Repository) GetDefaultPublicGPGKey(forceUpdate bool) (*GPGSettings, error) {
 | 
			
		||||
	if repo.gpgSettings != nil && !forceUpdate {
 | 
			
		||||
		return repo.gpgSettings, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	gpgSettings := &GPGSettings{
 | 
			
		||||
		Sign: true,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	value, _ := NewCommand("config", "--get", "commit.gpgsign").RunInDir(repo.Path)
 | 
			
		||||
	sign, valid := ParseBool(strings.TrimSpace(value))
 | 
			
		||||
	if !sign || !valid {
 | 
			
		||||
		gpgSettings.Sign = false
 | 
			
		||||
		repo.gpgSettings = gpgSettings
 | 
			
		||||
		return gpgSettings, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	signingKey, _ := NewCommand("config", "--get", "user.signingkey").RunInDir(repo.Path)
 | 
			
		||||
	gpgSettings.KeyID = strings.TrimSpace(signingKey)
 | 
			
		||||
 | 
			
		||||
	defaultEmail, _ := NewCommand("config", "--get", "user.email").RunInDir(repo.Path)
 | 
			
		||||
	gpgSettings.Email = strings.TrimSpace(defaultEmail)
 | 
			
		||||
 | 
			
		||||
	defaultName, _ := NewCommand("config", "--get", "user.name").RunInDir(repo.Path)
 | 
			
		||||
	gpgSettings.Name = strings.TrimSpace(defaultName)
 | 
			
		||||
 | 
			
		||||
	if err := gpgSettings.LoadPublicKeyContent(); err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	repo.gpgSettings = gpgSettings
 | 
			
		||||
	return repo.gpgSettings, nil
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -56,10 +56,11 @@ func (repo *Repository) GetTree(idStr string) (*Tree, error) {
 | 
			
		|||
 | 
			
		||||
// CommitTreeOpts represents the possible options to CommitTree
 | 
			
		||||
type CommitTreeOpts struct {
 | 
			
		||||
	Parents   []string
 | 
			
		||||
	Message   string
 | 
			
		||||
	KeyID     string
 | 
			
		||||
	NoGPGSign bool
 | 
			
		||||
	Parents    []string
 | 
			
		||||
	Message    string
 | 
			
		||||
	KeyID      string
 | 
			
		||||
	NoGPGSign  bool
 | 
			
		||||
	AlwaysSign bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CommitTree creates a commit from a given tree id for the user with provided message
 | 
			
		||||
| 
						 | 
				
			
			@ -90,7 +91,7 @@ func (repo *Repository) CommitTree(sig *Signature, tree *Tree, opts CommitTreeOp
 | 
			
		|||
	_, _ = messageBytes.WriteString(opts.Message)
 | 
			
		||||
	_, _ = messageBytes.WriteString("\n")
 | 
			
		||||
 | 
			
		||||
	if opts.KeyID != "" {
 | 
			
		||||
	if version.Compare(binVersion, "1.7.9", ">=") && (opts.KeyID != "" || opts.AlwaysSign) {
 | 
			
		||||
		cmd.AddArguments(fmt.Sprintf("-S%s", opts.KeyID))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,6 +7,7 @@ package git
 | 
			
		|||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync"
 | 
			
		||||
)
 | 
			
		||||
| 
						 | 
				
			
			@ -86,3 +87,30 @@ func RefEndName(refStr string) string {
 | 
			
		|||
 | 
			
		||||
	return refStr
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ParseBool returns the boolean value represented by the string as per git's git_config_bool
 | 
			
		||||
// true will be returned for the result if the string is empty, but valid will be false.
 | 
			
		||||
// "true", "yes", "on" are all true, true
 | 
			
		||||
// "false", "no", "off" are all false, true
 | 
			
		||||
// 0 is false, true
 | 
			
		||||
// Any other integer is true, true
 | 
			
		||||
// Anything else will return false, false
 | 
			
		||||
func ParseBool(value string) (result bool, valid bool) {
 | 
			
		||||
	// Empty strings are true but invalid
 | 
			
		||||
	if len(value) == 0 {
 | 
			
		||||
		return true, false
 | 
			
		||||
	}
 | 
			
		||||
	// These are the git expected true and false values
 | 
			
		||||
	if strings.EqualFold(value, "true") || strings.EqualFold(value, "yes") || strings.EqualFold(value, "on") {
 | 
			
		||||
		return true, true
 | 
			
		||||
	}
 | 
			
		||||
	if strings.EqualFold(value, "false") || strings.EqualFold(value, "no") || strings.EqualFold(value, "off") {
 | 
			
		||||
		return false, true
 | 
			
		||||
	}
 | 
			
		||||
	// Try a number
 | 
			
		||||
	intValue, err := strconv.ParseInt(value, 10, 32)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false, false
 | 
			
		||||
	}
 | 
			
		||||
	return intValue != 0, true
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -73,7 +73,7 @@ func getExpectedFileResponse() *api.FileResponse {
 | 
			
		|||
		},
 | 
			
		||||
		Verification: &api.PayloadCommitVerification{
 | 
			
		||||
			Verified:  false,
 | 
			
		||||
			Reason:    "",
 | 
			
		||||
			Reason:    "gpg.error.not_signed_commit",
 | 
			
		||||
			Signature: "",
 | 
			
		||||
			Payload:   "",
 | 
			
		||||
		},
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -261,7 +261,6 @@ func (t *TemporaryUploadRepository) CommitTree(author, committer *models.User, t
 | 
			
		|||
		return "", fmt.Errorf("Unable to get git version: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// FIXME: Should we add SSH_ORIGINAL_COMMAND to this
 | 
			
		||||
	// Because this may call hooks we should pass in the environment
 | 
			
		||||
	env := append(os.Environ(),
 | 
			
		||||
		"GIT_AUTHOR_NAME="+authorSig.Name,
 | 
			
		||||
| 
						 | 
				
			
			@ -271,13 +270,21 @@ func (t *TemporaryUploadRepository) CommitTree(author, committer *models.User, t
 | 
			
		|||
		"GIT_COMMITTER_EMAIL="+committerSig.Email,
 | 
			
		||||
		"GIT_COMMITTER_DATE="+commitTimeStr,
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	messageBytes := new(bytes.Buffer)
 | 
			
		||||
	_, _ = messageBytes.WriteString(message)
 | 
			
		||||
	_, _ = messageBytes.WriteString("\n")
 | 
			
		||||
 | 
			
		||||
	args := []string{"commit-tree", treeHash, "-p", "HEAD"}
 | 
			
		||||
	if version.Compare(binVersion, "2.0.0", ">=") {
 | 
			
		||||
		args = append(args, "--no-gpg-sign")
 | 
			
		||||
 | 
			
		||||
	// Determine if we should sign
 | 
			
		||||
	if version.Compare(binVersion, "1.7.9", ">=") {
 | 
			
		||||
		sign, keyID := t.repo.SignCRUDAction(author, t.basePath, "HEAD")
 | 
			
		||||
		if sign {
 | 
			
		||||
			args = append(args, "-S"+keyID)
 | 
			
		||||
		} else if version.Compare(binVersion, "2.0.0", ">=") {
 | 
			
		||||
			args = append(args, "--no-gpg-sign")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	commitHash, stderr, err := process.GetManager().ExecDirEnvStdIn(5*time.Minute,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,10 +18,16 @@ func GetPayloadCommitVerification(commit *git.Commit) *structs.PayloadCommitVeri
 | 
			
		|||
		verification.Signature = commit.Signature.Signature
 | 
			
		||||
		verification.Payload = commit.Signature.Payload
 | 
			
		||||
	}
 | 
			
		||||
	if verification.Reason != "" {
 | 
			
		||||
		verification.Reason = commitVerification.Reason
 | 
			
		||||
	} else if verification.Verified {
 | 
			
		||||
		verification.Reason = "unsigned"
 | 
			
		||||
	if commitVerification.SigningUser != nil {
 | 
			
		||||
		verification.Signer = &structs.PayloadUser{
 | 
			
		||||
			Name:  commitVerification.SigningUser.Name,
 | 
			
		||||
			Email: commitVerification.SigningUser.Email,
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	verification.Verified = commitVerification.Verified
 | 
			
		||||
	verification.Reason = commitVerification.Reason
 | 
			
		||||
	if verification.Reason == "" && !verification.Verified {
 | 
			
		||||
		verification.Reason = "gpg.error.not_signed_commit"
 | 
			
		||||
	}
 | 
			
		||||
	return verification
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -65,6 +65,16 @@ var (
 | 
			
		|||
		Issue struct {
 | 
			
		||||
			LockReasons []string
 | 
			
		||||
		} `ini:"repository.issue"`
 | 
			
		||||
 | 
			
		||||
		Signing struct {
 | 
			
		||||
			SigningKey    string
 | 
			
		||||
			SigningName   string
 | 
			
		||||
			SigningEmail  string
 | 
			
		||||
			InitialCommit []string
 | 
			
		||||
			CRUDActions   []string `ini:"CRUD_ACTIONS"`
 | 
			
		||||
			Merges        []string
 | 
			
		||||
			Wiki          []string
 | 
			
		||||
		} `ini:"repository.signing"`
 | 
			
		||||
	}{
 | 
			
		||||
		AnsiCharset:                             "",
 | 
			
		||||
		ForcePrivate:                            false,
 | 
			
		||||
| 
						 | 
				
			
			@ -122,6 +132,25 @@ var (
 | 
			
		|||
		}{
 | 
			
		||||
			LockReasons: strings.Split("Too heated,Off-topic,Spam,Resolved", ","),
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		// Signing settings
 | 
			
		||||
		Signing: struct {
 | 
			
		||||
			SigningKey    string
 | 
			
		||||
			SigningName   string
 | 
			
		||||
			SigningEmail  string
 | 
			
		||||
			InitialCommit []string
 | 
			
		||||
			CRUDActions   []string `ini:"CRUD_ACTIONS"`
 | 
			
		||||
			Merges        []string
 | 
			
		||||
			Wiki          []string
 | 
			
		||||
		}{
 | 
			
		||||
			SigningKey:    "default",
 | 
			
		||||
			SigningName:   "",
 | 
			
		||||
			SigningEmail:  "",
 | 
			
		||||
			InitialCommit: []string{"always"},
 | 
			
		||||
			CRUDActions:   []string{"pubkey", "twofa", "parentsigned"},
 | 
			
		||||
			Merges:        []string{"pubkey", "twofa", "basesigned", "commitssigned"},
 | 
			
		||||
			Wiki:          []string{"never"},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	RepoRootPath string
 | 
			
		||||
	ScriptType   = "bash"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -91,10 +91,11 @@ type PayloadCommit struct {
 | 
			
		|||
 | 
			
		||||
// PayloadCommitVerification represents the GPG verification of a commit
 | 
			
		||||
type PayloadCommitVerification struct {
 | 
			
		||||
	Verified  bool   `json:"verified"`
 | 
			
		||||
	Reason    string `json:"reason"`
 | 
			
		||||
	Signature string `json:"signature"`
 | 
			
		||||
	Payload   string `json:"payload"`
 | 
			
		||||
	Verified  bool         `json:"verified"`
 | 
			
		||||
	Reason    string       `json:"reason"`
 | 
			
		||||
	Signature string       `json:"signature"`
 | 
			
		||||
	Signer    *PayloadUser `json:"signer"`
 | 
			
		||||
	Payload   string       `json:"payload"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1974,12 +1974,15 @@ mark_as_unread = Mark as unread
 | 
			
		|||
mark_all_as_read = Mark all as read
 | 
			
		||||
 | 
			
		||||
[gpg]
 | 
			
		||||
default_key=Signed with default key
 | 
			
		||||
error.extract_sign = Failed to extract signature
 | 
			
		||||
error.generate_hash = Failed to generate hash of commit
 | 
			
		||||
error.no_committer_account = No account linked to committer's email address
 | 
			
		||||
error.no_gpg_keys_found = "No known key found for this signature in database"
 | 
			
		||||
error.not_signed_commit = "Not a signed commit"
 | 
			
		||||
error.failed_retrieval_gpg_keys = "Failed to retrieve any key attached to the committer's account"
 | 
			
		||||
error.probable_bad_signature = "WARNING! Although there is a key with this ID in the database it does not verify this commit! This commit is SUSPICIOUS."
 | 
			
		||||
error.probable_bad_default_signature = "WARNING! Although the default key has this ID it does not verify this commit! This commit is SUSPICIOUS."
 | 
			
		||||
 | 
			
		||||
[units]
 | 
			
		||||
error.no_unit_allowed_repo = You are not allowed to access any section of this repository.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -225,6 +225,10 @@ footer .ui.left,footer .ui.right{line-height:40px}
 | 
			
		|||
.inline-grouped-list{display:inline-block;vertical-align:top}
 | 
			
		||||
.inline-grouped-list>.ui{display:block;margin-top:5px;margin-bottom:10px}
 | 
			
		||||
.inline-grouped-list>.ui:first-child{margin-top:1px}
 | 
			
		||||
i.icons .icon:first-child{margin-right:0}
 | 
			
		||||
i.icon.centerlock{top:1.5em}
 | 
			
		||||
.ui.label>.detail .icons{margin-right:.25em}
 | 
			
		||||
.ui.label>.detail .icons .icon{margin-right:0}
 | 
			
		||||
.lines-num{vertical-align:top;text-align:right!important;color:#999;background:#f5f5f5;width:1%;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}
 | 
			
		||||
.lines-num span:before{content:attr(data-line-number);line-height:20px!important;padding:0 10px;cursor:pointer;display:block}
 | 
			
		||||
.lines-code,.lines-num{padding:0!important}
 | 
			
		||||
| 
						 | 
				
			
			@ -654,6 +658,8 @@ footer .ui.left,footer .ui.right{line-height:40px}
 | 
			
		|||
.repository #commits-table.ui.basic.striped.table tbody tr:nth-child(2n){background-color:rgba(0,0,0,.02)!important}
 | 
			
		||||
.repository #commits-table td.sha .sha.label,.repository #repo-files-table .sha.label{border:1px solid #bbb}
 | 
			
		||||
.repository #commits-table td.sha .sha.label .detail.icon,.repository #repo-files-table .sha.label .detail.icon{background:#fafafa;margin:-6px -10px -4px 0;padding:5px 3px 5px 6px;border-left:1px solid #bbb;border-top-left-radius:0;border-bottom-left-radius:0}
 | 
			
		||||
.repository #commits-table td.sha .sha.label.isSigned.isWarning,.repository #repo-files-table .sha.label.isSigned.isWarning{border:1px solid #db2828;background:rgba(219,40,40,.1)}
 | 
			
		||||
.repository #commits-table td.sha .sha.label.isSigned.isWarning .detail.icon,.repository #repo-files-table .sha.label.isSigned.isWarning .detail.icon{border-left:1px solid rgba(219,40,40,.5)}
 | 
			
		||||
.repository #commits-table td.sha .sha.label.isSigned.isVerified,.repository #repo-files-table .sha.label.isSigned.isVerified{border:1px solid #21ba45;background:rgba(33,186,69,.1)}
 | 
			
		||||
.repository #commits-table td.sha .sha.label.isSigned.isVerified .detail.icon,.repository #repo-files-table .sha.label.isSigned.isVerified .detail.icon{border-left:1px solid #21ba45}
 | 
			
		||||
.repository #commits-table td.sha .sha.label.isSigned.isVerified:hover,.repository #repo-files-table .sha.label.isSigned.isVerified:hover{background:rgba(33,186,69,.3)!important}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -950,6 +950,22 @@ footer {
 | 
			
		|||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
i.icons .icon:first-child {
 | 
			
		||||
    margin-right: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
i.icon.centerlock {
 | 
			
		||||
    top: 1.5em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ui.label > .detail .icons {
 | 
			
		||||
    margin-right: 0.25em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.ui.label > .detail .icons .icon {
 | 
			
		||||
    margin-right: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.lines-num {
 | 
			
		||||
    vertical-align: top;
 | 
			
		||||
    text-align: right !important;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1212,6 +1212,15 @@
 | 
			
		|||
            border-bottom-left-radius: 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        &.isSigned.isWarning {
 | 
			
		||||
            border: 1px solid #db2828;
 | 
			
		||||
            background: fade(#db2828, 10%);
 | 
			
		||||
 | 
			
		||||
            .detail.icon {
 | 
			
		||||
                border-left: 1px solid fade(#db2828, 50%);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        &.isSigned.isVerified {
 | 
			
		||||
            border: 1px solid #21ba45;
 | 
			
		||||
            background: fade(#21ba45, 10%);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -507,6 +507,7 @@ func RegisterRoutes(m *macaron.Macaron) {
 | 
			
		|||
			m.Get("/swagger", misc.Swagger)
 | 
			
		||||
		}
 | 
			
		||||
		m.Get("/version", misc.Version)
 | 
			
		||||
		m.Get("/signing-key.gpg", misc.SigningKey)
 | 
			
		||||
		m.Post("/markdown", bind(api.MarkdownOption{}), misc.Markdown)
 | 
			
		||||
		m.Post("/markdown/raw", misc.MarkdownRaw)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -771,6 +772,7 @@ func RegisterRoutes(m *macaron.Macaron) {
 | 
			
		|||
						m.Delete("", bind(api.DeleteFileOptions{}), repo.DeleteFile)
 | 
			
		||||
					}, reqRepoWriter(models.UnitTypeCode), reqToken())
 | 
			
		||||
				}, reqRepoReader(models.UnitTypeCode))
 | 
			
		||||
				m.Get("/signing-key.gpg", misc.SigningKey)
 | 
			
		||||
				m.Group("/topics", func() {
 | 
			
		||||
					m.Combo("").Get(repo.ListTopics).
 | 
			
		||||
						Put(reqToken(), reqAdmin(), bind(api.RepoTopicOptions{}), repo.UpdateTopics)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,6 +12,7 @@ import (
 | 
			
		|||
	"code.gitea.io/gitea/modules/git"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
	"code.gitea.io/gitea/modules/markup"
 | 
			
		||||
	"code.gitea.io/gitea/modules/structs"
 | 
			
		||||
	api "code.gitea.io/gitea/modules/structs"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -84,17 +85,21 @@ func ToCommit(repo *models.Repository, c *git.Commit) *api.PayloadCommit {
 | 
			
		|||
// ToVerification convert a git.Commit.Signature to an api.PayloadCommitVerification
 | 
			
		||||
func ToVerification(c *git.Commit) *api.PayloadCommitVerification {
 | 
			
		||||
	verif := models.ParseCommitWithSignature(c)
 | 
			
		||||
	var signature, payload string
 | 
			
		||||
	commitVerification := &api.PayloadCommitVerification{
 | 
			
		||||
		Verified: verif.Verified,
 | 
			
		||||
		Reason:   verif.Reason,
 | 
			
		||||
	}
 | 
			
		||||
	if c.Signature != nil {
 | 
			
		||||
		signature = c.Signature.Signature
 | 
			
		||||
		payload = c.Signature.Payload
 | 
			
		||||
		commitVerification.Signature = c.Signature.Signature
 | 
			
		||||
		commitVerification.Payload = c.Signature.Payload
 | 
			
		||||
	}
 | 
			
		||||
	return &api.PayloadCommitVerification{
 | 
			
		||||
		Verified:  verif.Verified,
 | 
			
		||||
		Reason:    verif.Reason,
 | 
			
		||||
		Signature: signature,
 | 
			
		||||
		Payload:   payload,
 | 
			
		||||
	if verif.SigningUser != nil {
 | 
			
		||||
		commitVerification.Signer = &structs.PayloadUser{
 | 
			
		||||
			Name:  verif.SigningUser.Name,
 | 
			
		||||
			Email: verif.SigningUser.Email,
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return commitVerification
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ToPublicKey convert models.PublicKey to api.PublicKey
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										62
									
								
								routers/api/v1/misc/signing.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								routers/api/v1/misc/signing.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,62 @@
 | 
			
		|||
package misc
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"net/http"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models"
 | 
			
		||||
	"code.gitea.io/gitea/modules/context"
 | 
			
		||||
	"code.gitea.io/gitea/modules/log"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// SigningKey returns the public key of the default signing key if it exists
 | 
			
		||||
func SigningKey(ctx *context.Context) {
 | 
			
		||||
	// swagger:operation GET /signing-key.gpg miscellaneous getSigningKey
 | 
			
		||||
	// ---
 | 
			
		||||
	// summary: Get default signing-key.gpg
 | 
			
		||||
	// produces:
 | 
			
		||||
	//     - text/plain
 | 
			
		||||
	// responses:
 | 
			
		||||
	//   "200":
 | 
			
		||||
	//     description: "GPG armored public key"
 | 
			
		||||
	//     schema:
 | 
			
		||||
	//       type: string
 | 
			
		||||
 | 
			
		||||
	// swagger:operation GET /repos/{owner}/{repo}/signing-key.gpg repository repoSigningKey
 | 
			
		||||
	// ---
 | 
			
		||||
	// summary: Get signing-key.gpg for given repository
 | 
			
		||||
	// produces:
 | 
			
		||||
	//     - text/plain
 | 
			
		||||
	// parameters:
 | 
			
		||||
	// - name: owner
 | 
			
		||||
	//   in: path
 | 
			
		||||
	//   description: owner of the repo
 | 
			
		||||
	//   type: string
 | 
			
		||||
	//   required: true
 | 
			
		||||
	// - name: repo
 | 
			
		||||
	//   in: path
 | 
			
		||||
	//   description: name of the repo
 | 
			
		||||
	//   type: string
 | 
			
		||||
	//   required: true
 | 
			
		||||
	// responses:
 | 
			
		||||
	//   "200":
 | 
			
		||||
	//     description: "GPG armored public key"
 | 
			
		||||
	//     schema:
 | 
			
		||||
	//       type: string
 | 
			
		||||
 | 
			
		||||
	path := ""
 | 
			
		||||
	if ctx.Repo != nil && ctx.Repo.Repository != nil {
 | 
			
		||||
		path = ctx.Repo.Repository.RepoPath()
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	content, err := models.PublicSigningKey(path)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.ServerError("gpg export", err)
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
	_, err = ctx.Write([]byte(content))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Error("Error writing key content %v", err)
 | 
			
		||||
		ctx.Error(http.StatusInternalServerError, fmt.Sprintf("%v", err))
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -13,6 +13,7 @@ import (
 | 
			
		|||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"code.gitea.io/gitea/models"
 | 
			
		||||
	"code.gitea.io/gitea/modules/cache"
 | 
			
		||||
| 
						 | 
				
			
			@ -28,6 +29,11 @@ import (
 | 
			
		|||
// Merge merges pull request to base repository.
 | 
			
		||||
// FIXME: add repoWorkingPull make sure two merges does not happen at same time.
 | 
			
		||||
func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repository, mergeStyle models.MergeStyle, message string) (err error) {
 | 
			
		||||
	binVersion, err := git.BinVersion()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("Unable to get git version: %v", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err = pr.GetHeadRepo(); err != nil {
 | 
			
		||||
		return fmt.Errorf("GetHeadRepo: %v", err)
 | 
			
		||||
	} else if err = pr.GetBaseRepo(); err != nil {
 | 
			
		||||
| 
						 | 
				
			
			@ -176,6 +182,30 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor
 | 
			
		|||
		return fmt.Errorf("git read-tree HEAD: %s", errbuf.String())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Determine if we should sign
 | 
			
		||||
	signArg := ""
 | 
			
		||||
	if version.Compare(binVersion, "1.7.9", ">=") {
 | 
			
		||||
		sign, keyID := pr.BaseRepo.SignMerge(doer, tmpBasePath, "HEAD", trackingBranch)
 | 
			
		||||
		if sign {
 | 
			
		||||
			signArg = "-S" + keyID
 | 
			
		||||
		} else if version.Compare(binVersion, "2.0.0", ">=") {
 | 
			
		||||
			signArg = "--no-gpg-sign"
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	sig := doer.NewGitSig()
 | 
			
		||||
	commitTimeStr := time.Now().Format(time.RFC3339)
 | 
			
		||||
 | 
			
		||||
	// Because this may call hooks we should pass in the environment
 | 
			
		||||
	env := append(os.Environ(),
 | 
			
		||||
		"GIT_AUTHOR_NAME="+sig.Name,
 | 
			
		||||
		"GIT_AUTHOR_EMAIL="+sig.Email,
 | 
			
		||||
		"GIT_AUTHOR_DATE="+commitTimeStr,
 | 
			
		||||
		"GIT_COMMITTER_NAME="+sig.Name,
 | 
			
		||||
		"GIT_COMMITTER_EMAIL="+sig.Email,
 | 
			
		||||
		"GIT_COMMITTER_DATE="+commitTimeStr,
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	// Merge commits.
 | 
			
		||||
	switch mergeStyle {
 | 
			
		||||
	case models.MergeStyleMerge:
 | 
			
		||||
| 
						 | 
				
			
			@ -183,9 +213,14 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor
 | 
			
		|||
			return fmt.Errorf("git merge --no-ff --no-commit [%s]: %v - %s", tmpBasePath, err, errbuf.String())
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		sig := doer.NewGitSig()
 | 
			
		||||
		if err := git.NewCommand("commit", fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email), "-m", message).RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil {
 | 
			
		||||
			return fmt.Errorf("git commit [%s]: %v - %s", tmpBasePath, err, errbuf.String())
 | 
			
		||||
		if signArg == "" {
 | 
			
		||||
			if err := git.NewCommand("commit", "-m", message).RunInDirTimeoutEnvPipeline(env, -1, tmpBasePath, nil, &errbuf); err != nil {
 | 
			
		||||
				return fmt.Errorf("git commit [%s]: %v - %s", tmpBasePath, err, errbuf.String())
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			if err := git.NewCommand("commit", signArg, "-m", message).RunInDirTimeoutEnvPipeline(env, -1, tmpBasePath, nil, &errbuf); err != nil {
 | 
			
		||||
				return fmt.Errorf("git commit [%s]: %v - %s", tmpBasePath, err, errbuf.String())
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	case models.MergeStyleRebase:
 | 
			
		||||
		// Checkout head branch
 | 
			
		||||
| 
						 | 
				
			
			@ -223,9 +258,14 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor
 | 
			
		|||
		}
 | 
			
		||||
 | 
			
		||||
		// Set custom message and author and create merge commit
 | 
			
		||||
		sig := doer.NewGitSig()
 | 
			
		||||
		if err := git.NewCommand("commit", fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email), "-m", message).RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil {
 | 
			
		||||
			return fmt.Errorf("git commit [%s]: %v - %s", tmpBasePath, err, errbuf.String())
 | 
			
		||||
		if signArg == "" {
 | 
			
		||||
			if err := git.NewCommand("commit", "-m", message).RunInDirTimeoutEnvPipeline(env, -1, tmpBasePath, nil, &errbuf); err != nil {
 | 
			
		||||
				return fmt.Errorf("git commit [%s]: %v - %s", tmpBasePath, err, errbuf.String())
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			if err := git.NewCommand("commit", signArg, "-m", message).RunInDirTimeoutEnvPipeline(env, -1, tmpBasePath, nil, &errbuf); err != nil {
 | 
			
		||||
				return fmt.Errorf("git commit [%s]: %v - %s", tmpBasePath, err, errbuf.String())
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
	case models.MergeStyleSquash:
 | 
			
		||||
| 
						 | 
				
			
			@ -234,8 +274,14 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor
 | 
			
		|||
			return fmt.Errorf("git merge --squash [%s -> %s]: %s", headRepoPath, tmpBasePath, errbuf.String())
 | 
			
		||||
		}
 | 
			
		||||
		sig := pr.Issue.Poster.NewGitSig()
 | 
			
		||||
		if err := git.NewCommand("commit", fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email), "-m", message).RunInDirPipeline(tmpBasePath, nil, &errbuf); err != nil {
 | 
			
		||||
			return fmt.Errorf("git commit [%s]: %v - %s", tmpBasePath, err, errbuf.String())
 | 
			
		||||
		if signArg == "" {
 | 
			
		||||
			if err := git.NewCommand("commit", fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email), "-m", message).RunInDirTimeoutEnvPipeline(env, -1, tmpBasePath, nil, &errbuf); err != nil {
 | 
			
		||||
				return fmt.Errorf("git commit [%s]: %v - %s", tmpBasePath, err, errbuf.String())
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			if err := git.NewCommand("commit", signArg, fmt.Sprintf("--author='%s <%s>'", sig.Name, sig.Email), "-m", message).RunInDirTimeoutEnvPipeline(env, -1, tmpBasePath, nil, &errbuf); err != nil {
 | 
			
		||||
				return fmt.Errorf("git commit [%s]: %v - %s", tmpBasePath, err, errbuf.String())
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	default:
 | 
			
		||||
		return models.ErrInvalidMergeStyle{ID: pr.BaseRepo.ID, Style: mergeStyle}
 | 
			
		||||
| 
						 | 
				
			
			@ -270,7 +316,7 @@ func Merge(pr *models.PullRequest, doer *models.User, baseGitRepo *git.Repositor
 | 
			
		|||
		headUser = doer
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	env := models.FullPushingEnvironment(
 | 
			
		||||
	env = models.FullPushingEnvironment(
 | 
			
		||||
		headUser,
 | 
			
		||||
		doer,
 | 
			
		||||
		pr.BaseRepo,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,6 +26,16 @@
 | 
			
		|||
						<img class="ui avatar image" src="{{AvatarLink .Commit.Author.Email}}" />
 | 
			
		||||
						<strong>{{.Commit.Author.Name}}</strong>
 | 
			
		||||
					{{end}}
 | 
			
		||||
					{{if or (ne .Commit.Committer.Name .Commit.Author.Name) (ne .Commit.Committer.Email .Commit.Author.Email)}}
 | 
			
		||||
						<span> </span>
 | 
			
		||||
						{{if ne .Verification.CommittingUser.ID 0}}
 | 
			
		||||
							<img class="ui avatar image" src="{{.Verification.CommittingUser.RelAvatarLink}}" />
 | 
			
		||||
							<a href="{{.Verification.CommittingUser.HomeLink}}"><strong>{{.Commit.Committer.Name}}</strong></a> <{{.Commit.Committer.Email}}>
 | 
			
		||||
						{{else}}
 | 
			
		||||
							<img class="ui avatar image" src="{{AvatarLink .Commit.Committer.Email}}" />
 | 
			
		||||
							<strong>{{.Commit.Committer.Name}}</strong>
 | 
			
		||||
						{{end}}
 | 
			
		||||
					{{end}}
 | 
			
		||||
					<span class="text grey" id="authored-time">{{TimeSince .Commit.Author.When $.Lang}}</span>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div class="seven wide right aligned column">
 | 
			
		||||
| 
						 | 
				
			
			@ -50,15 +60,36 @@
 | 
			
		|||
		{{if .Commit.Signature}}
 | 
			
		||||
			{{if .Verification.Verified }}
 | 
			
		||||
				<div class="ui bottom attached positive message">
 | 
			
		||||
				  <i class="green lock icon"></i>
 | 
			
		||||
					<span>{{.i18n.Tr "repo.commits.signed_by"}}:</span>
 | 
			
		||||
					<a href="{{.Verification.SigningUser.HomeLink}}"><strong>{{.Commit.Committer.Name}}</strong></a> <{{.Commit.Committer.Email}}>
 | 
			
		||||
					<span class="pull-right"><span>{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span> {{.Verification.SigningKey.KeyID}}</span>
 | 
			
		||||
					{{if ne .Verification.SigningUser.ID 0}}
 | 
			
		||||
						<i class="green lock icon"></i>
 | 
			
		||||
						<span>{{.i18n.Tr "repo.commits.signed_by"}}:</span>
 | 
			
		||||
						<img class="ui avatar image" src="{{.Verification.SigningUser.RelAvatarLink}}" />
 | 
			
		||||
						<a href="{{.Verification.SigningUser.HomeLink}}"><strong>{{.Verification.SigningUser.Name}}</strong></a> <{{.Verification.SigningEmail}}>
 | 
			
		||||
						<span class="pull-right"><span>{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span> {{.Verification.SigningKey.KeyID}}</span>
 | 
			
		||||
					{{else}}
 | 
			
		||||
						<i class="icons" title="{{.i18n.Tr "gpg.default_key"}}">
 | 
			
		||||
							<i class="green lock icon"></i>
 | 
			
		||||
							<i class="tiny inverted cog icon centerlock"></i>
 | 
			
		||||
						</i>
 | 
			
		||||
						<span>{{.i18n.Tr "repo.commits.signed_by"}}:</span>
 | 
			
		||||
						<img class="ui avatar image" src="{{AvatarLink .Verification.SigningEmail}}" />
 | 
			
		||||
						<strong>{{.Verification.SigningUser.Name}}</strong> <{{.Verification.SigningEmail}}>
 | 
			
		||||
						<span class="pull-right"><span>{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span> <i class="cogs icon" title="{{.i18n.Tr "gpg.default_key"}}"></i>{{.Verification.SigningKey.KeyID}}</span>
 | 
			
		||||
					{{end}}
 | 
			
		||||
				</div>
 | 
			
		||||
			{{else if .Verification.Warning}}
 | 
			
		||||
				<div class="ui bottom attached message">
 | 
			
		||||
				  <i class="red unlock icon"></i>
 | 
			
		||||
				  <span class="red text">{{.i18n.Tr .Verification.Reason}}</span>
 | 
			
		||||
				  <span class="pull-right"><span class="red text">{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span> <i class="red warning icon"></i>{{.Verification.SigningKey.KeyID}}</span>
 | 
			
		||||
				</div>
 | 
			
		||||
			{{else}}
 | 
			
		||||
				<div class="ui bottom attached message">
 | 
			
		||||
				  <i class="grey unlock icon"></i>
 | 
			
		||||
				  {{.i18n.Tr .Verification.Reason}}
 | 
			
		||||
				  {{if and .Verification.SigningKey (ne .Verification.SigningKey.KeyID "")}}
 | 
			
		||||
					<span class="pull-right"><span class="red text">{{.i18n.Tr "repo.commits.gpg_key_id"}}:</span> <i class="red warning icon"></i>{{.Verification.SigningKey.KeyID}}</span>
 | 
			
		||||
				  {{end}}
 | 
			
		||||
				</div>
 | 
			
		||||
			{{end}}
 | 
			
		||||
		{{end}}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -56,12 +56,21 @@
 | 
			
		|||
							{{end}}
 | 
			
		||||
						</td>
 | 
			
		||||
						<td class="sha">
 | 
			
		||||
							<a rel="nofollow" class="ui sha label {{if .Signature}} isSigned {{if .Verification.Verified }} isVerified {{end}}{{end}}" href="{{AppSubUrl}}/{{$.Username}}/{{$.Reponame}}/commit/{{.ID}}">
 | 
			
		||||
							<a rel="nofollow" class="ui sha label {{if .Signature}} isSigned {{if .Verification.Verified }} isVerified {{else if .Verification.Warning}} isWarning {{end}}{{end}}" href="{{AppSubUrl}}/{{$.Username}}/{{$.Reponame}}/commit/{{.ID}}">
 | 
			
		||||
								{{ShortSha .ID.String}}
 | 
			
		||||
								{{if .Signature}}
 | 
			
		||||
									<div class="ui detail icon button">
 | 
			
		||||
										{{if .Verification.Verified}}
 | 
			
		||||
											<i title="{{.Verification.Reason}}" class="lock green icon"></i>
 | 
			
		||||
											{{if ne .Verification.SigningUser.ID 0}}
 | 
			
		||||
												<i title="{{.Verification.Reason}}" class="lock green icon"></i>
 | 
			
		||||
											{{else}}
 | 
			
		||||
												<i title="{{.Verification.Reason}}" class="icons">
 | 
			
		||||
													<i class="green lock icon"></i>
 | 
			
		||||
													<i class="tiny inverted cog icon centerlock"></i>
 | 
			
		||||
												</i>
 | 
			
		||||
											{{end}}
 | 
			
		||||
										{{else if .Verification.Warning}}
 | 
			
		||||
											<i title="{{$.i18n.Tr .Verification.Reason}}" class="red unlock icon"></i>
 | 
			
		||||
										{{else}}
 | 
			
		||||
											<i title="{{$.i18n.Tr .Verification.Reason}}" class="unlock icon"></i>
 | 
			
		||||
										{{end}}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5140,6 +5140,42 @@
 | 
			
		|||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "/repos/{owner}/{repo}/signing-key.gpg": {
 | 
			
		||||
      "get": {
 | 
			
		||||
        "produces": [
 | 
			
		||||
          "text/plain"
 | 
			
		||||
        ],
 | 
			
		||||
        "tags": [
 | 
			
		||||
          "repository"
 | 
			
		||||
        ],
 | 
			
		||||
        "summary": "Get signing-key.gpg for given repository",
 | 
			
		||||
        "operationId": "repoSigningKey",
 | 
			
		||||
        "parameters": [
 | 
			
		||||
          {
 | 
			
		||||
            "type": "string",
 | 
			
		||||
            "description": "owner of the repo",
 | 
			
		||||
            "name": "owner",
 | 
			
		||||
            "in": "path",
 | 
			
		||||
            "required": true
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            "type": "string",
 | 
			
		||||
            "description": "name of the repo",
 | 
			
		||||
            "name": "repo",
 | 
			
		||||
            "in": "path",
 | 
			
		||||
            "required": true
 | 
			
		||||
          }
 | 
			
		||||
        ],
 | 
			
		||||
        "responses": {
 | 
			
		||||
          "200": {
 | 
			
		||||
            "description": "GPG armored public key",
 | 
			
		||||
            "schema": {
 | 
			
		||||
              "type": "string"
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "/repos/{owner}/{repo}/stargazers": {
 | 
			
		||||
      "get": {
 | 
			
		||||
        "produces": [
 | 
			
		||||
| 
						 | 
				
			
			@ -5691,6 +5727,26 @@
 | 
			
		|||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "/signing-key.gpg": {
 | 
			
		||||
      "get": {
 | 
			
		||||
        "produces": [
 | 
			
		||||
          "text/plain"
 | 
			
		||||
        ],
 | 
			
		||||
        "tags": [
 | 
			
		||||
          "miscellaneous"
 | 
			
		||||
        ],
 | 
			
		||||
        "summary": "Get default signing-key.gpg",
 | 
			
		||||
        "operationId": "getSigningKey",
 | 
			
		||||
        "responses": {
 | 
			
		||||
          "200": {
 | 
			
		||||
            "description": "GPG armored public key",
 | 
			
		||||
            "schema": {
 | 
			
		||||
              "type": "string"
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "/teams/{id}": {
 | 
			
		||||
      "get": {
 | 
			
		||||
        "produces": [
 | 
			
		||||
| 
						 | 
				
			
			@ -9525,6 +9581,9 @@
 | 
			
		|||
          "type": "string",
 | 
			
		||||
          "x-go-name": "Signature"
 | 
			
		||||
        },
 | 
			
		||||
        "signer": {
 | 
			
		||||
          "$ref": "#/definitions/PayloadUser"
 | 
			
		||||
        },
 | 
			
		||||
        "verified": {
 | 
			
		||||
          "type": "boolean",
 | 
			
		||||
          "x-go-name": "Verified"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue